Hacking Android apps with FRIDA II - Crackme

After the introduction to Frida in the first part of this post, we are now bringing Frida to use for solving a little crackme. After what we have already learned about Frida, this is going to be easy (- in theory). If you want to follow along, please download

Of course, I also assume that you have successfully installed Frida (version 9.1.16 or later) on your computer and started the corresponding server binary on the (rooted) device. I’m going to use an Android 7.1.1 ARM image in an emulator for this tutorial.

Install the Uncrackable Crackme Level 1 app on your device:

adb install sg.vantagepoint.uncrackable1.apk

Wait until it is installed, then start it from the menu on the emulator (orange icon in bottom right corner):

Unbreakable Crackme menu

Once you start the app you will notice that it doesn’t want to run on a rooted device:

Unbreakable Crackme root dialog

If you press “OK”, the app exists immediately. Hm. Not so nice. Seems we cannot solve the crackme this way. Really? Let’s see what is going on and take a look at the internals of the app.

Convert the apk to a jar with dex2jar:

michael@sixtyseven:/opt/dex2jar/dex2jar-2.0$ ./d2j-dex2jar.sh -o /home/michael/UnCrackable-Level1.jar /home/michael/UnCrackable-Level1.apk 

dex2jar /home/michael/UnCrackable-Level1.apk -> /home/michael/UnCrackable-Level1.jar

And load it into BytecodeViewer (or another disassembler of your choice that supports Java). You might also try to load the APK directly into BytecodeViewer or just extract the classes.dex, but that didn’t work for me, so I converted it with dex2jar before.

In BytecodeViewer, chose View->Pane1->CFR->Java to use the CFR Decompiler. You can set Pane2 to Smali code if you like to compare the results of the decompiler to the Smali disassembly (which is usually a little more accurate than the decompilation).

Unbreakable Bytecode Viewer

Here is the output of the CFR Decompiler for the app’s MainActivity:

package sg.vantagepoint.uncrackable1;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.uncrackable1.a;
import sg.vantagepoint.uncrackable1.b;
import sg.vantagepoint.uncrackable1.c;

public class MainActivity
extends Activity {
    private void a(String string) {
        AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
        alertDialog.setTitle((CharSequence)string);
        alertDialog.setMessage((CharSequence)"This in unacceptable. The app is now going to exit.");
        alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
        alertDialog.show();
    }

    protected void onCreate(Bundle bundle) {
        if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) {
            this.a("Root detected!"); //This is the message we are looking for
        }
        if (sg.vantagepoint.a.b.a((Context)this.getApplicationContext())) {
            this.a("App is debuggable!");
        }
        super.onCreate(bundle);
        this.setContentView(2130903040);
    }

    public void verify(View object) {
        object = ((EditText)this.findViewById(2131230720)).getText().toString();
        AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
        if (a.a((String)object)) {
            alertDialog.setTitle((CharSequence)"Success!");
            alertDialog.setMessage((CharSequence)"This is the correct secret.");
        } else {
            alertDialog.setTitle((CharSequence)"Nope...");
            alertDialog.setMessage((CharSequence)"That's not it. Try again.");
        }
        alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new c(this));
        alertDialog.show();
    }
}

Looking at the other decompiled class files, we see that it is a small app and we could probably also solve the crackme by reverse engineering the decryption and string modification routines. However, since we know Frida, we have some more convenient possibilities. Let’s look where the app checks if the device is rooted. Right above the “Root detected” message, we see:

if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())

If you have a look at the sg.vantagepoint.a.c class you see the various checks for root:

public static boolean a()
    {
        String[] a = System.getenv("PATH").split(":");
        int i = a.length;
        int i0 = 0;
        while(true)
        {
            boolean b = false;
            if (i0 >= i)
            {
                b = false;
            }
            else
            {
                if (!new java.io.File(a[i0], "su").exists())
                {
                    i0 = i0 + 1;
                    continue;
                }
                b = true;
            }
            return b;
        }
    }
    
    public static boolean b()
    {
        String s = android.os.Build.TAGS;
        if (s != null && s.contains((CharSequence)(Object)"test-keys"))
        {
            return true;
        }
        return false;
    }
    
    public static boolean c()
    {
        String[] a = new String[7];
        a[0] = "/system/app/Superuser.apk";
        a[1] = "/system/xbin/daemonsu";
        a[2] = "/system/etc/init.d/99SuperSUDaemon";
        a[3] = "/system/bin/.ext/.su";
        a[4] = "/system/etc/.has_su_daemon";
        a[5] = "/system/etc/.installed_su_daemon";
        a[6] = "/dev/com.koushikdutta.superuser.daemon/";
        int i = a.length;
        int i0 = 0;
        while(i0 < i)
        {
            if (new java.io.File(a[i0]).exists())
            {
                return true;
            }
            i0 = i0 + 1;
        }
        return false;
    }

Using Frida, we could make all of these methods return false by overwriting them as we have seen in part I of this tutorial. But what actually happens when one of the function return true because it discovers root? As we have seen in MainActivity function a it opens a dialog. It also sets an onClickListener that gets triggered when we press the OK button:

alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));

This onClickListener implementation doesn’t do much:

package sg.vantagepoint.uncrackable1;

class b implements android.content.DialogInterface$OnClickListener {
    final sg.vantagepoint.uncrackable1.MainActivity a;
    
    b(sg.vantagepoint.uncrackable1.MainActivity a0)
    {
        this.a = a0;
        super();
    }
    
    public void onClick(android.content.DialogInterface a0, int i)
    {
        System.exit(0);
    }
}

It just exits the app with System.exit(0). So all we have to do is to prevent the app from exiting. Let’s overwrite the onClick method with Frida. Create a file uncrackable1.js and put your code inside:

setImmediate(function() { //prevent timeout
    console.log("[*] Starting script");

    Java.perform(function() {

      bClass = Java.use("sg.vantagepoint.uncrackable1.b");
      bClass.onClick.implementation = function(v) {
         console.log("[*] onClick called");
      }
      console.log("[*] onClick handler modified")

    })
})

If you have read part I of this tutorial, the script should be quite self explanatory: We wrap our Code in a setImmediate function to prevent timeouts (you may or may not need this), then call Java.perform to make use of Frida’s methods for dealing with Java. Then the actual magic follows: We retreive a wrapper for the class the implements the OnClickListener interface and overwrite its onClick method. In our version, this function just writes some console output. Unlike the original, it does not exit the app. Since the original onClickHandler is replaced by our Frida injected function and never gets called, the app should not exit anymore when we click the OK button of the dialog. Let’s try it. Open the app (let it display the “Root detected” dialog)

Unbreakable Crackme root dialog

and inject the script:

frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1

Give Frida a few seconds to inject its code until you see the “onClick handler modified” message (you might get a shell before since we put our code in an setImmediate wrapper so Frida executes it in the background).

Unbreakable Frida output

Then click on the OK button in the app. If everything went well, the app does not exit anymore.

Unbreakable Crackme input box

Great: The dialog vanishes and we can enter a password. Let’s enter something, press Verify and see what happens:

Unbreakable Crackme wrong code dialog

Wrong code, as was to be expected. But we have an idea what we are looking for: Some kind of encryption / decryption routines and a comparison of result and input.

Checking MainActivity again, we see in the function

public void verify(View object) {

that it calls method a from class sg.vantagepoint.uncrackable1.a:

if (a.a((String)object)) {

This is the decompilation of the sg.vantagepoint.uncrackable1.a class:

package sg.vantagepoint.uncrackable1;

import android.util.Base64;
import android.util.Log;

/*
 * Exception performing whole class analysis ignored.
 */
public class a {
    public static boolean a(String string) {
        byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
        byte[] arrby2 = new byte[]{};
        try {
            arrby2 = arrby = sg.vantagepoint.a.a.a((byte[])a.b((String)"8d127684cbc37c17616d806cf50473cc"), (byte[])arrby);
        }
        catch (Exception var2_2) {
            Log.d((String)"CodeCheck", (String)("AES error:" + var2_2.getMessage()));
        }
        if (!string.equals(new String(arrby2))) return false;
        return true;
    }

    public static byte[] b(String string) {
        int n = string.length();
        byte[] arrby = new byte[n / 2];
        int n2 = 0;
        while (n2 < n) {
            arrby[n2 / 2] = (byte)((Character.digit(string.charAt(n2), 16) << 4) + Character.digit(string.charAt(n2 + 1), 16));
            n2 += 2;
        }
        return arrby;
    }
}

Notice the string.equals comparison at the end of the a method and the creation of the string arrby2 in the try block above. arrby2 is the return value of the function sg.vantagepoint.a.a.a. The string.equals comparison compares our input to arrby2. So what we are after is the return value of sg.vantagepoint.a.a.a.

We could now start to reverse engineer the string manipulation and decryption functions and work on the original encrypted strings, that are also contained in the code above. Or we let the app do all its maniplation and encryption that we really don’t care for and just hook the sg.vantagepoint.a.a.a function to catch its return value. The return value is the decrypted string (in form of a byte array) that our input gets compared to. This is what the following script does:

        aaClass = Java.use("sg.vantagepoint.a.a");
        aaClass.a.implementation = function(arg1, arg2) {
            retval = this.a(arg1, arg2);
            password = ''
            for(i = 0; i < retval.length; i++) {
               password += String.fromCharCode(retval[i]);
            }

            console.log("[*] Decrypted: " + password);
            return retval;
        }
        console.log("[*] sg.vantagepoint.a.a.a modified");

We overwrite the sg.vantagepoint.a.a.a function, catch its return value and convert it into a readable string. This is the decrypted string we are looking for, so we print it out to the console and hopefully get our solution.

Putting the pieces together, here is the complete script:

setImmediate(function() {
    console.log("[*] Starting script");

    Java.perform(function() {
        
        bClass = Java.use("sg.vantagepoint.uncrackable1.b");
        bClass.onClick.implementation = function(v) {
         console.log("[*] onClick called.");
        }
        console.log("[*] onClick handler modified")


        aaClass = Java.use("sg.vantagepoint.a.a");
        aaClass.a.implementation = function(arg1, arg2) {
            retval = this.a(arg1, arg2);
            password = ''
            for(i = 0; i < retval.length; i++) {
               password += String.fromCharCode(retval[i]);
            }

            console.log("[*] Decrypted: " + password);
            return retval;
        }
        console.log("[*] sg.vantagepoint.a.a.a modified");


    });

});

Let’s run this script. As before, save it as uncrackable1.js and do (if Frida doesn’t rerun it automatically)

frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1

Wait until you see the message sg.vantagepoint.a.a.a modified, then click OK in the Root detected dialog, enter something in the secret code field and press Verify. Still no luck in the emulator.

But notice the Frida output:

michael@sixtyseven:~/Development/frida$ frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
     ____
    / _  |   Frida 9.1.16 - A world-class dynamic instrumentation framework
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at http://www.frida.re/docs/home/
                                                                                
[*] Starting script
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] onClick handler modified
[*] sg.vantagepoint.a.a.a modified
[*] onClick called.
[*] Decrypted: I want to believe

Nice. We actually got the decrypted string: I want to believe. That’s it. Let’s check if it works:

Unbreakable Crackme success

By now, I hope you are at least a little impressed by what you can do with Frida and it’s dynamic binary instrumentation capabilities.

As always, for comments, critique etc. contact me on Twitter.