Hello everyone! today I’m going to speak about how i’ve managed to break metaforic compiler from inside secure. It was once again an huge and awesome challenge. Like Arxan, metaforic is used nowadays to protect banking, gambling and government applications.
I’m in contact with the company protected with the compiler, but I’ve not received anything back from inside secure, so I’ll keep private antything that is related to the application (which is still not available in the store (at least from italy) – offsets will just change at the first application update which could be already happened) but give an abstract on how metaforic works in my understanding.
There are several protections layer provided by metaforic, here are some of them which I’ve defeat to achieve code execution in my target application.
- checks for system debug flags
- checks for frida and substrate
- checks for ptracer
- checks for root
- checks on all local installed packages
- checks and maps all binaries in common binary paths (/system/bin|xbin, /vendor/bin etc.)
- runtime crc (bypassed but not cracked at all)
- moar checks here and there (/proc/self/maps – /proc/net/unix and so on)
- obfuscation doesn’t look llvm based at all but i could be wrong
- bonus: screenshot prevention cracked to prof the crack at the 2 companies
- tricks which totally messup IDA decompiler and even assembly, hopper does a pretty good job there and binja as well.
All the patches in the final exploit code had to be chained in order to debug properly. In example, the ptracer checks are always invoked before frida one, so it was a bit painful to figure out the right spots to detach from code execution and start an strace right after ptracer checks.
To debug and achieve this i used a mix of Dwarf and strace.
The initial checks are living in JNI_OnLoad while most of the runtime checks just re-use nested functions used in initial checks.
Down the business
Debugging phase:
- Step the system to get a callback when the target module is loaded. For this purpose I used Dwarf but i’ve recently released a standalone frida module to do so, which is living here.
- Callback received, we can attach to JNI_OnLoad offset and debug whatever happens there.
Cracking phase:
Cracking phase is divided in chained stages.
Stage 1: crack the initial frida/substrace checks. I debugged this in the deep. What they are doing is basically reading /proc/self/maps, iterate all the maps and checks for the presence of strings “frida_agent_main” and another one I can’t remember for substrate.
.text:0000E682 BL sub_1AD588 .text:0000E686 ADDS R0, R5, #1 .text:0000E688 BEQ loc_E638
function startStage1() { const postMapChecks = base.add(0x1AD588).add(1); Interceptor.attach(postMapChecks, function () { // we patch the second sub entrance with the result of the previous sub (supposed to be a check for frida this.context.r5 = 0; console.log('stage 1 *patched*'); startStage2(); }); }
Once stage1 is achieved we jump to stage2. In this stage they are checking for common su binaries in system paths. This stage was a bit tricky because I had to use trampolines to skip some sort of crc running which was crashing the proc if I hook streight at the target offset.
trampoline 1
.text:001AFE08 loc_1AFE08 ; CODE XREF: sub_1AFDEE+C↑j .text:001AFE08 STR R1, [SP,#arg_C] .text:001AFE0A BIC.W R3, R3, #1 .text:001AFE0E SUB.W R1, R6, R1,ROR#9
root checks routine used as a trampoline to target offset
.text:001DC684 ; __unwind { .text:001DC684 PUSH {R4-R7,LR} .text:001DC686 ADD R7, SP, #0xC .text:001DC688 PUSH.W {R8-R11}
target offset to patch the return, stored later in the stack
.text:001AFED4 AND.W R1, R6, R10,LSR#30 .text:001AFED8 SUB.W R3, R11, R8 .text:001AFEDC AND.W R2, R1, #1 .text:001AFEE0 SUBS R1, R5, #2 .text:001AFEE2 MOVW R5, #0xEDBC .text:001AFEE6 SUBS R1, R1, R2 .text:001AFEE8 SUBS R3, #2 .text:001AFEEA MOVT.W R5, #0x1D95 .text:001AFEEE CMP R1, R3 .text:001AFEF0 STR R0, [SP,#arg_28]
code for stage2
function startStage2() { const rootChecks = base.add(0x1DC684).add(1); // we need some trampolines to get to the right one Interceptor.attach(base.add(0x1AFE08).add(1), function () { Interceptor.attach(rootChecks, function () { Interceptor.attach(base.add(0x001AFED4).add(1), function () { // original return 0xa3, incremented by N for each check met this.context.r0 = 0; console.log('stage 2 *patched*'); // clean Interceptor.detachAll(); startStage3(); }); }); }); }
Once stage2 is achieved we jump into stage3 which is meant to patch the runtime checks and the screenshot prevention. Runtime checks also re-check for root presence, frida attached and ptracer. I initially patched 3 subroutine which was making the application act in a weird way, probably something is stored in memory which overlap something else, but anyway, i’ve figured out later another unique sub which return a bool. I named this bool something as “developerMode” since altering it’s return, tango downed everything making the application runs as expcted.
That stage needs some better details. Let’s take a look at the obfuscated assembly. I’ve added comments to explain what’s going on
.text:001ADC8E ; sub_1ADA8C+1F0↑j .text:001ADC8E MOV R4, R1 // this is one check. if it fails - aka return -1 - it jump and call 0x1bcbd0 which is that "developerMode()". if developerMode return -1 as well, this sub will return err and crash later .text:001ADC90 BL sub_1AB24C .text:001ADC94 CBZ R4, loc_1ADCAA .text:001ADC96 CMP.W R0, #0xFFFFFFFF .text:001ADC9A BGT loc_1ADCAA .text:001ADC9C MOVS R0, #4 .text:001ADC9E MOVS R1, #0 // that actual developerMode(). true? jump to next check .text:001ADCA0 BL sub_1BCBD0 .text:001ADCA4 CMP R0, #0 .text:001ADCA6 BLT.W loc_1AF4D2 .text:001ADCAA .text:001ADCAA loc_1ADCAA ; CODE XREF: sub_1ADA8C+208↑j .text:001ADCAA ; sub_1ADA8C+20E↑j // this is one other check. same as before. fail? call developerMode() .text:001ADCAA BL sub_1AB5B8 .text:001ADCAE LDR R1, [SP,#0x26B8+var_26A0] .text:001ADCB0 CBZ R1, loc_1ADCC8 .text:001ADCB2 CMP.W R0, #0xFFFFFFFF .text:001ADCB6 BGT loc_1ADCC8 .text:001ADCB8 MOVS R0, #0x10 .text:001ADCBA MOVS R1, #0 // that developerMode once again .text:001ADCBC BL sub_1BCBD0 .text:001ADCC0 LDR R1, [SP,#0x26B8+var_26A0] .text:001ADCC2 CMP R0, #0 .text:001ADCC4 BLT.W loc_1AF4D2 .text:001ADCC8 .text:001ADCC8 loc_1ADCC8 ; CODE XREF: sub_1ADA8C+224↑j .text:001ADCC8 ; sub_1ADA8C+22A↑j .text:001ADCC8 CBZ R1, loc_1ADCD4 .text:001ADCCA BL sub_1AF5B4 .text:001ADCCE CMP R0, #0 .text:001ADCD0 BEQ.W loc_1AF4CE
This cycle is repeated a couple of time for the various different checks. We can resume the code with something like:
function checkWrapper(checkFn) { // patching ret here to return 0 was giving a lot of troubles let ret = checkFn(); if (ret === -1) { ret = developerMode(); } return ret; }
Here is the relevant code for stage3.
function startStage3() { Java.performNow(function () { const Activity = Java.use('android.app.Activity'); const Window = Java.use('android.view.Window'); // screenshot killer const set_flags = Window.setFlags.overload('int', 'int'); const layout_params = Java.use('android.view.WindowManager$LayoutParams'); set_flags.implementation = function(flags, mask){ flags =(flags.value & ~layout_params.FLAG_SECURE.value); set_flags.call(this, flags, mask); }; let patched = false; Activity.onCreate.overloads[0].implementation = function () { const whoami = this.toString(); if (whoami.indexOf('com.xxx.package.LoginActivity') >= 0) { console.log('spawn login activity'); // goodbye xD //fuck(0x1B6740); //fuck(0x1AB24C); //fuck(0x1ABDD8); if (!patched) { patched = true; fuck(0x1BCBD0); } startStage4(); } return this.onCreate.apply(this, arguments); }; }) } function fuck(off) { console.log('*patching* ' + base.add(off).add(1)); Interceptor.replace(base.add(off).add(1), new NativeCallback(function(a, b) { return 0; }, 'int', ['int', 'int'])); }
In stage4 we have full code execution in the target app without any more troubles.
Conclusions
The target application, as said, is a government application which will have various features. One of that feature is to prof the identity of the person. If the design of the overall project is well done, on the otherside, who is checking the identity of the person should receive the data from an attestation server since personal user information are stored on the backend side. Anyway, this doesn’t prevent an hacker to clear local application storage, force the user to re-authenticate through the various login ways and spoof that data sent to the server. An hacker can eventually access super private informations such as name, surname, date of birth and picture. An hacker can build a local application to show the same data from a victim and so on.