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

OWASP MSTG Crackme 2 writeup (Android)

The application

To understand more about the application, we extract the source code from the APK and look into interesting classes.
 
The structure of the app is the following:
				
					├── owasp
│   └── mstg
│       └── uncrackable2
│           └── R.java
└── sg
    └── vantagepoint
        ├── a
        │   ├── a.java
        │   └── b.java
        └── uncrackable2
            ├── CodeCheck.java
            └── MainActivity.java
				
			

As we can see, the structure is very similar to the previous challenge, but there is a new file CodeCheck.java. When we inspect the content of MainActivity.java we can see how the root detection is handled (same as in the previous challenge), and how the secret is checked. The function verify handles the secret checks

				
					 //MainActivity.java
 public void verify(View view) {
        String str;
        String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
        AlertDialog create = new AlertDialog.Builder(this).create();
        if (this.m.a(obj)) {
            create.setTitle("Success!");
            str = "This is the correct secret.";
        } else {
            create.setTitle("Nope...");
            str = "That's not it. Try again.";
        }
        create.setMessage(str);
        create.setButton(-3, "OK", new DialogInterface.OnClickListener() {
            /* class sg.vantagepoint.uncrackable2.MainActivity.AnonymousClass3 */

            public void onClick(DialogInterface dialogInterface, int i) {
                dialogInterface.dismiss();
            }
        });
        create.show();
    }
				
			

where this.m.a(obj) is the function that will check whether the secret is the right one.

Who is this.m?

If we look right after the MainActivity class definition, we see
				
					 private CodeCheck m;

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

where the CodeCheck class declares a function that is implemented from the native library foo.

				
					private native boolean bar(byte[] bArr);
				
			
So, our next step is to deep dive into the native module.

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

Analyze the libfoo.so
Looking inside the binary we can identify the native function Java_sg_vantagepoint_uncrackable2_CodeCheck_bar that will check whether – the input string has 23 chars (0x17) – the string in input matches the secret using the strncmp function.
Now let’s get to Frida, to see how we can intercept and read the inputs passed to the strncmp function used in libfoo.so.

Frida

Because we need to trigger the strcmp function, we need first to get rid of the root detection block.

Root detection control bypass

There are different ways of bypassing the root detection controls that will shut down the app once the OK button is clicked. A “dirty” way is to overload the onClick event of the OK button, to avoid that the application will call System.exit(0).
 
We can achieve this using the following Frida snippet
				
					/*

    Dirty way of bypassing the root detection, avoiding the app to close.

*/

Java.perform(function() {
   
   
    console.log("[*] Hijacking the onClick button")
    var clazz_main = Java.use('sg.vantagepoint.uncrackable2.MainActivity$1')

     clazz_main.onClick.implementation = function () {
        console.log('onCLick() is replaced ');
        
    };
    
});
				
			

Clicking OK will close the dialog, while the app will still run.

Exploit

The strncmp function has the following signature: 

				
					int strncmp(char *__s1,char *__s2,size_t __n)
				
			

and is used in our Java_sg_vantagepoint_uncrackable2_CodeCheck_bar function in this way

				
					iVar1 = strncmp(__s1,(char *)&local_30,0x17);
				
			
where
 
  • *__s1 is the text passed in input from the user
  • (char *)&local_30 is the secret we are looking for
  • 0x17 is the length (23 bytes)
 
To extract the secret we can read the inputs of the compare function and print them out when *__s1 matches our string I want your secret asap
The final script looks like
				
					function extractSecret(){

    /*
    
        To use this function, we need to pass in input an argument with 23 chars. We chose: I want your secret asap

    */

    console.log()
    console.log('[*] ACTION NEEDED: Insert the string "I want your secret asap" as input')
    console.log()
    setTimeout(function(){
        Interceptor.attach(Module.findExportByName('libfoo.so', 'strncmp'),{

            onEnter: function(args){

                if( Memory.readUtf8String(args[1]).length == 23 && Memory.readUtf8String(args[0]).includes("I want your secret asap")){
                    console.log()
                    console.log()
                    console.log("*******SECRET********")
                    console.log(Memory.readUtf8String(args[1]))
                    console.log("*******SECRET********")
                    console.log()
                    console.log()
                }

            },

            onLeave: function(retval){

            }

        });
    },2000);
}
				
			

Once we call the function via Frida, and insert our magic string, the secret will be printed in the console

and when we insert the new secret in the input field we see

The full script can be downloaded from our repo https://github.com/dcodx/owasp-mstg-crackme