Join our in person Smart Contract Hacking training at BlackHat Asia 2025

OWASP MSTG Crackme 3 writeup (Android)

The application

The application has the same look as the others and the same message when root detection kicks in.
Let’s have a deeper look. As usual, we extract the source code from the APK and look into interesting classes.
The main structure of the app is the following
				
					├── owasp
│   └── mstg
│       └── uncrackable3
│           └── R.java
└── sg
    └── vantagepoint
        ├── uncrackable3
        │   ├── BuildConfig.java
        │   ├── CodeCheck.java
        │   ├── MainActivity.java
        │   └── R.java
        └── util
            ├── IntegrityCheck.java
            └── RootDetection.java
				
			

When we inspect the content of MainActivity.java we can quickly see that things are getting spicier and the root detection is not the only control anymore, but integrity checks are added to prevent code tampering. 

On line 101 we see

				
					if (RootDetection.checkRoot1() || RootDetection.checkRoot2() || RootDetection.checkRoot3() || IntegrityCheck.isDebuggable(getApplicationContext()) || tampered != 0) {
            showDialog("Rooting or tampering detected.");
        }
				
			
where tampered is set to 31337 from the function verifyLibs when the native libraries are hooked or manipulated.
When we try to hook the functions and instrument the application we can see the following message in the log files
				
					06-15 08:57:46.680  2817  2841 V UnCrackable3: Tampering detected! Terminating...
--------- beginning of crash
				
			

and the app miserably crashes.

Native library

The secret is still hidden in the same native library libfoo.so
				
					 private CodeCheck m;

    static {
        System.loadLibrary("foo");
    }
				
			

that implements the bar() function

				
					private native boolean bar(byte[] bArr);
				
			

We investigate further whether the native code changed.

CodeCheck native function

To find the logic of the bar function we will:
 
  • rename the .apk in .zip
  • extract the native module lib/libfoo.so
  • reverse it using Ghidra
 

Ghidra

Looking inside the binary we right away, see the native function Java_sg_vantagepoint_uncrackable2_CodeCheck_bar being exported and including few small changes from the previous challenge.
In the new version:
 
  • the input string has 24 chars (line 27: if (iVar2 == 0x18))
  • every character of the string inserted by the user is XORed with each character of the xorkey (private static final String xorkey = "pizzapizzapizzapizzapizz";)
 
The specific instruction pseudocode looks like:
				
					if (*(byte *)(iVar1 + uVar3) != (*(byte *)puVar4 ^ local_40[uVar3])) goto LAB_00013456;
				
			
If the chars do not match, function LAB_00013456 is called and 0 is returned, making the check fail.
 
Something to note is that this challenge includes a new function Java_sg_vantagepoint_uncrackable3_MainActivity_init that is the implementation of the init(byte[] Array) used to initialize the XOR key(init(xorkey.getBytes());)

Frida

Root detection bypass
Unfortunately (as expected) the script from the previous challenge does not work anymore, due to the more complex checks we mentioned above. On top of that, more checks are also implemented in the native library. When we try to bypass the root detection we receive the following Stacktrace:

We can see that a goodbye() function is defined in libfoo.so and is making the app crash. Using Ghidra we note that there is a new functionality able to detect whether Frida or Xposed are used.

In case at least one is found, the app will exit calling the goodbye() function. To bypass all the controls we are going two use two different techniques:
 
  • overload of the fgets function to avoid file detection
  • overload of System.exit(int) to stop the app to close when clicking OK
 
The script will look like the following:
				
					function rootAndTamperingDetectionBypass(){

    console.log("[*] Start removing root and tampering detection");

    var System = Java.use('java.lang.System');        
    System.exit.implementation = function(var0) {

        console.log("Ciao bella, I'm sorry but today you are not exiting");    
    };

    console.log("[*] fgets overloading to avoid Frida detection")
    var fgetsPtr = Module.findExportByName("libc.so", "fgets");
    var fgets = new NativeFunction(fgetsPtr, 'pointer', ['pointer', 'int', 'pointer']);

    Interceptor.replace(fgetsPtr, new NativeCallback(function (buffer, size, fp) {        
        var retval = fgets(buffer, size, fp);
        var bufstr = Memory.readUtf8String(buffer);
        if (bufstr.indexOf("frida") > -1) {
            Memory.writeUtf8String(buffer, "ByeByeFrida:\t0");
        }
        return retval;
    }, 'pointer', ['pointer', 'int', 'pointer']));

    console.log("[*] Root and tampering detection removed, you can safely click OK");    

}
				
			

At this point we can run our application using the following command:

				
					frida -U -l hook.js -f owasp.mstg.uncrackable3 --no-pause
				
			

where hook.js will call the function rootAndTamperingDetectionBypass() at execution time, preventing the app to crash.

Exploit

Now it’s time to extract the secret
 
To extract the secret we are going to use Frida, but with a slightly different approach than in the previous challenges. Looking at the flow, we can see that the function Java_sg_vantagepoint_uncrackable2_CodeCheck_bar on line 24, uses another function that we call FUN_00010fa0, that is used to load the secrets in memory, but unfortunately is not exported and therefore we cannot directly hook it.
 
This function is quite complex, but we know, that once executed, the secret will be loaded in memory. So how do we extract the secret?
 
First of all, we need to find a way to hook into this function. We are going to use a different method, that will calculate the base address of the libfoo.so library and the offset of the function.
From Ghidra we can see the offset of the function being mentioned in the name. Again, we are using an x86 device and therefore using the x86 version of the library. In case an ARM device is used, the offset and the instruction will be different. The offset of the function is 0xFA0. Let’s see how we can hook it. These are the steps to extract the secret:
 
  • Calculate the base address of libfoo.so using Module.findBaseAddress('libfoo.so')
  • Add the offset to the base address to get to the function using base_address.add(0xFA0)
  • Get the buffer onEnter
  • Get the first 24 bytes onLeave
  • XOR each byte of the secret with each byte of the well known XOR key
 
We introduced a timeout to make sure that the library libfoo.so is loaded when calling the function
				
					function extractSecret(){

    setTimeout(function(){
        var base_address = Module.findBaseAddress('libfoo.so')
        var extract_secret_function = base_address.add(0xFA0)

        Interceptor.attach(extract_secret_function,{

            onEnter(args) {
                console.log('Base address libfoo.so: ' + base_address)
                console.log('Base address secret_function: ' + extract_secret_function)
                console.log('Reading buffer args[0]') 
                this.buf = args[0]
                console.log('Buffer reading completed')
              },

              onLeave(result) {
                console.log('---------------------')
                var numBytes = 24
                var buff = Memory.readByteArray(this.buf,numBytes)           
                console.log('[*] Secret key hexdump')
                console.log('---------------------')
                console.log(hexdump(buff, { length: numBytes, ansi: true }));
                   var secret_key = new Uint8Array(buff)
                var str = "";
                for(var i = 0; i < secret_key.length; i++) {
                    str += (secret_key[i].toString(16) + " ");
                }
                console.log('---------------------')
                console.log('[*] Secret key ')
                console.log('---------------------')
                console.log(secret_key)
                   console.log('---------------------')
                   console.log('[*] XOR key ')
                   console.log('---------------------')
                   var xor_key = 'pizzapizzapizzapizzapizz';
                console.log(xor_key);
                   console.log('---------------------')
                   console.log('[*] Plaintext secret')
                   console.log('---------------------')
                   var secret = []
                   for (var i =0;i<numBytes;i++){
                       secret[i] = String.fromCharCode(secret_key[i] ^ xor_key.charCodeAt(i));
                   }
                   console.log(secret.join(''))
                   console.log('---------------------')
            }
        });
    },2000);
}
				
			

When we run the full script hook.js, using the command 

				
					frida -U -l hook.js -f owasp.mstg.uncrackable3 --no-pause
				
			

the secret will be printed out as soon as the button Verify is clicked.

And voilá, the secret making owasp great again is printed in the console and the challenge is completed
The full Frida script can be downloaded from our repo https://github.com/dcodx/owasp-mstg-crackme