Inside Secure – Metaforic – Reverse engineering and cracking

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:

  1. 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.
  2. 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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.