Hacking Android apps with FRIDA III - OWASP UnCrackable 2

Shortly after my second blogpost on Frida, @muellerberndt decided to publish another OWASP Android crackme and I was tempted to see whether I could solve it with Frida again. If you want to follow along, you need

If you need some introduction on how to install Frida, please check the Frida documentation. For Frida usage, you might also check part I of this tutorial. I suppose you have all the stuff up and running before you continue and are also basically familiar with Frida. Also, make sure that Frida can connect to your device / emulator (e.g. by using frida-ps -U).

A word of warning: This is not only a quick walkthrough to solve the crackme. Instead, I’m going to show you various ways to overcome specific problems. If you are only looking for a quick solution, just check the Frida script at the end of this tutorial.

Note: If you run into an

Error: access violation accessing 0xebad8082

or similiar error when using Frida it might help to wipe userdata from the emulator, restart it and install the apk again.

Be prepared to try things multiple times. The app might crash, the emulator might restart, everything might get messed up, but yes, it works.

First run

We start doing the same thing as before with UnCrackable1 and just run the app: Again, when you run it in an emulator, it detects that it is running on a rooted device.

Uncrackable root

We might try to proceed as in UnCrackable 1 hooking the OnClickListener. But first let’s check if we can connect Frida to start tampering:

michael@sixtyseven:~/Development$ frida -U sg.vantagepoint.uncrackable2
     ____
    / _  |   Frida 9.1.27 - 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/
Failed to attach: ambiguous name; it matches: sg.vantagepoint.uncrackable2 (pid: 5184), sg.vantagepoint.uncrackable2 (pid: 5201)

What is this? There are two processes with the same name. We can verify that with frida-ps -U:

5184  sg.vantagepoint.uncrackable2
5201  sg.vantagepoint.uncrackable2

Strange. Let’s try to inject Frida to the parent process:

michael@sixtyseven:~/Development$ frida -U 5184
     ____
    / _  |   Frida 9.1.27 - 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/
Failed to attach: unable to access process with pid 5184 due to system restrictions; try `sudo sysctl kernel.yama.ptrace_scope=0`, or run Frida as root

It doesn’t work and since we get the same result when running Frida as root, the proposed solution isn’t of much help. What is going on here? Let’s have a closer look at the app. Unzip the apk and decompile classes.dex with bytecode viewer (e.g. CFR-Decompiler):

package sg.vantagepoint.uncrackable2;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.c;
import android.text.Editable;
import android.view.View;
import android.widget.EditText;
import sg.vantagepoint.a.a;
import sg.vantagepoint.a.b;
import sg.vantagepoint.uncrackable2.CodeCheck;
import sg.vantagepoint.uncrackable2.MainActivity;

public class MainActivity
extends c {
    private CodeCheck m;

    static {
        System.loadLibrary("foo"); //[1]
    }

    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 /* Unavailable Anonymous Inner Class!! */);
        alertDialog.setCancelable(false);
        alertDialog.show();
    }

    static /* synthetic */ void a(MainActivity mainActivity, String string) {
        mainActivity.a(string);
    }

    private native void init(); //[2]

    protected void onCreate(Bundle bundle) {
        this.init(); //[3]
        if (b.a() || b.b() || b.c()) {
            this.a("Root detected!");
        }
        if (a.a((Context)this.getApplicationContext())) {
            this.a("App is debuggable!");
        }
        new /* Unavailable Anonymous Inner Class!! */.execute((Object[])new Void[]{null, null, null});
        this.m = new CodeCheck();
        super.onCreate(bundle);
        this.setContentView(2130968603);
    }

    public void verify(View view) {
        String string = ((EditText)this.findViewById(2131427422)).getText().toString();
        AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
        if (this.m.a(string)) {
            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 /* Unavailable Anonymous Inner Class!! */);
        alertDialog.show();
    }
}

We notice the static block with a call to System.load that loads library foo (see [1]). The app also calls this.init() on the first line of its onCreate method (see [3]) which is declared a native method (see [2]) so it’s probably part of foo.

Now let us examine the foo library. In radare2, open the library (you find the variants for various architectures in the lib folder, I’m using lib/x86_64 here), analyse it and list its exports:

michael@sixtyseven:~/Development/UnCrackable2/lib/x86_64$ r2 libfoo.so 
 -- Don't look at the code. Don't look.
[0x000007a0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[ ] [*] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan))

[0x000007a0]> iE
[Exports]
vaddr=0x00001060 paddr=0x00001060 ord=004 fwd=NONE sz=183 bind=GLOBAL type=FUNC name=Java_sg_vantagepoint_uncrackable2_CodeCheck_bar
vaddr=0x00001050 paddr=0x00001050 ord=006 fwd=NONE sz=15 bind=GLOBAL type=FUNC name=Java_sg_vantagepoint_uncrackable2_MainActivity_init
vaddr=0x00004008 paddr=0x00003008 ord=014 fwd=NONE sz=0 bind=GLOBAL type=NOTYPE name=__bss_start
vaddr=0x00004008 paddr=0x00003008 ord=015 fwd=NONE sz=0 bind=GLOBAL type=NOTYPE name=__bss_start
vaddr=0x0000400d paddr=0x0000400d ord=016 fwd=NONE sz=0 bind=GLOBAL type=NOTYPE name=_end

5 exports

[0x000007a0]> 

We notice that the library exports 2 interesting functions: Java_sg_vantagepoint_uncrackable2_MainActivity_init and Java_sg_vantagepoint_uncrackable2_CodeCheck_bar (for the specific naming of these methods check the Java nativ interface JNI). We will have a look at Java_sg_vantagepoint_uncrackable2_MainActivity_init:

[0x000007a0]> s 0x00001050
[0x00001050]> V

It is a rather short function:

[0x00001050 29% 848 libfoo.so]> pd $r @ sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init                                                                                                     
/ (fcn) sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init 15                                                                                                                                  
|   sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init ();                                                                                                                                     
|           0x00001050      50             push rax                                                                                                                                                 
|           0x00001051      e8caf7ffff     call sub.fork_820           ;[1]                                                                                                                         
|           0x00001056      c605af2f0000.  mov byte [0x0000400c], 1    ; [0x400c:1]=58 ; ": (GNU) 4.9.x 20150123 (prerelease)"                                                                      
|           0x0000105d      58             pop rax                                                                                                                                                  
\           0x0000105e      c3             ret                                                                                                                                                      
            0x0000105f      90             nop                       

It calls another function sub.fork_820 where a lot more is going on:

[0x00000820 14% 265 libfoo.so]> pd $r @ sub.fork_820                                                                                                                                                
/ (fcn) sub.fork_820 242                                                                                                                                                                            
|   sub.fork_820 ();                                                                                                                                                                                
|           ; var int local_8h @ rsp+0x8                                                                                                                                                            
|           ; var int local_10h @ rsp+0x10                                                                                                                                                          
|              ; CALL XREF from 0x00001051 (sym.Java_sg_vantagepoint_uncrackable2_MainActivity_init)                                                                                                
|           0x00000820      4156           push r14                                                                                                                                                 
|           0x00000822      53             push rbx                                                                                                                                                 
|           0x00000823      4883ec18       sub rsp, 0x18                                                                                                                                            
|           0x00000827      64488b042528.  mov rax, qword fs:[0x28]    ; [0x28:8]=0x3180 ; '('                                                                                                      
|           0x00000830      4889442410     mov qword [local_10h], rax                                                                                                                               
|           0x00000835      e806ffffff     call sym.imp.fork           ;[1]                                                                                                                         
|           0x0000083a      8905c8370000   mov dword loc.__bss_start, eax ; [0x4008:4]=0x43434700 ; loc.__bss_start                                                                                 
|           0x00000840      85c0           test eax, eax                                                                                                                                            
|       ,=< 0x00000842      741a           je 0x85e                    ;[2]                                                                                                                         
|       |   0x00000844      488d15a5ffff.  lea rdx, 0x000007f0         ; 0x7f0                                                                                                                      
|       |   0x0000084b      488d7c2408     lea rdi, [local_8h]         ; 0x8                                                                                                                        
|       |   0x00000850      31f6           xor esi, esi                                                                                                                                             
|       |   0x00000852      31c9           xor ecx, ecx                                                                                                                                             
|       |   0x00000854      e8f7feffff     call sym.imp.pthread_create ;[3]; ssize_t read(int fildes, void *buf, size_t nbyte)                                                                      
|      ,==< 0x00000859      e990000000     jmp 0x8ee                   ;[4]                                                                                                                         
|      ||      ; JMP XREF from 0x00000842 (sub.fork_820)                                                                                                                                            
|      |`-> 0x0000085e      e8fdfeffff     call sym.imp.getppid        ;[5]                                                                                                                         
|      |    0x00000863      89c3           mov ebx, eax                                                                                                                                             
|      |    0x00000865      bf10000000     mov edi, 0x10                                                                                                                                            
|      |    0x0000086a      31d2           xor edx, edx                                                                                                                                             
|      |    0x0000086c      31c9           xor ecx, ecx                                                                                                                                             
|      |    0x0000086e      31c0           xor eax, eax                                                                                                                                             
|      |    0x00000870      89de           mov esi, ebx                                                                                                                                             
|      |    0x00000872      e8f9feffff     call sym.imp.ptrace         ;[6]                                                                                                                         
|      |    0x00000877      4885c0         test rax, rax                                                                                                                                            
|      |,=< 0x0000087a      7572           jne 0x8ee                   ;[4]                                                                                                                         
|      ||   0x0000087c      4c8d742408     lea r14, [local_8h]         ; 0x8                                                                                                                        
|      ||   0x00000881      31d2           xor edx, edx                                                                                                                                             
|      ||   0x00000883      89df           mov edi, ebx                                                                                                                                             
|      ||   0x00000885      4c89f6         mov rsi, r14                                                                                                                                             
|      ||   0x00000888      e883feffff     call sym.imp.waitpid        ;[7]         

We see calls fork, to pthread_create, getppid, ptrace and waitpid. Without spending too much time with the disassembly we can guess that the main process forks a child process that attaches to it as a debugger using ptrace. This is a basic anti-debugging technique and you can read more about it here.

Since Frida uses ptrace for its initial injection, this explains why we don’t have luck connecting to the parent process: Attaching a debugging process is blocked, because there is already another process connected as debugger.

Anti Anti Debugging Solution 1: Frida

Frida to the rescue. Instead of injecting Frida into the running process, we can let it spawn the process for us. Using the -f option, we tell Frida to inject into Zygote and start the application afterwards. Close the app on the device and see what happens when we start Frida:

frida -U -f sg.vantagepoint.uncrackable2

We get:

michael@sixtyseven:~/Development/UnCrackable2/lib/x86_64$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause
     ____
    / _  |   Frida 9.1.27 - 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/
Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread!           
[USB::Android Emulator 5554::['sg.vantagepoint.uncrackable2']]-> 

Hooray! Frida injects into Zygote, spawns our process and waits for input. (I admit, this was quite a long intro to tell you to add the -f option to Frida, but you have been warned…)

We are now ready to proceed. But before we continue, we are going to check another solution to overcome the debugging protection of this crackme.

Anti Anti Debugging Solution 2: Patching

Besides letting Frida do the spawning, we can also get rid of our problem by patching the app. This means basically disassembling the app, rebuilding and signing the modified apk. However, in the case of this crackme this will later cause us trouble. I’m still going to show you how to do it and we take care of problems later.

We can achieve the patching with apktool:

michael@sixtyseven:~/Disassembly$ /opt/apktool/apktool.sh -r d UnCrackable-Level2.apk 
I: Using Apktool 2.2.0 on UnCrackable-Level2.apk
I: Copying raw resources...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

(I skip the extraction of ressources with -r since it caused problems on recompiling the apk. We don’t need the ressources here anyway.)

Have a look at the smali code in smali/sg/vantagepoint/uncrackable2/MainActivity.smali. You find the call to init around line 82 and can comment it out:

# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
    .locals 4

    const/4 v3, 0x0

#    invoke-direct {p0}, Lsg/vantagepoint/uncrackable2/MainActivity;->init()V

    invoke-static {}, Lsg/vantagepoint/a/b;->a()Z

Recompile the apk (ignoring the fatal error…):

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ /opt/apktool/apktool.sh b
I: Using Apktool 2.2.0
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
[Fatal Error] AndroidManifest.xml:1:1: Content ist nicht zulässig in Prolog.
I: Checking whether resources has changed...
I: Copying raw resources...
I: Copying libs... (/lib)
I: Building apk file...
I: Copying unknown files/dir...

Align it:

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ zipalign -v 4 dist/UnCrackable-Level2.apk  UnCrackable2.recompiled.aligned.apk
Verifying alignment of UnCrackable2.recompiled.aligned.apk (4)...
      49 AndroidManifest.xml (OK - compressed)
     914 classes.dex (OK - compressed)
  269899 lib/arm64-v8a/libfoo.so (OK - compressed)
  273297 lib/armeabi-v7a/libfoo.so (OK - compressed)
  279346 lib/armeabi/libfoo.so (OK - compressed)

Sign it (note: you need to have a key and keystore for this):

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ jarsigner -verbose -keystore ~/.android/debug.keystore  UnCrackable2.recompiled.aligned.apk signkey
Enter Passphrase for keystore: 
   adding: META-INF/MANIFEST.MF
   adding: META-INF/SIGNKEY.SF
   adding: META-INF/SIGNKEY.RSA
  signing: AndroidManifest.xml
  signing: classes.dex
  signing: lib/arm64-v8a/libfoo.so
  signing: lib/armeabi-v7a/libfoo.so
  signing: lib/armeabi/libfoo.so
  signing: lib/mips/libfoo.so
[...]

You find a more extensive description in the OWASP Mobile Security Testing Guide. Uninstall the original apk and install the patched apk:

adb uninstall sg.vantagepoint.uncrackable2
adb install UnCrackable2.recompiled.aligned.apk

Start the app again. Running frida-ps we now get only one process:

29996  sg.vantagepoint.uncrackable2

And Frida connects without problems:

michael@sixtyseven:~/Disassembly/UnCrackable-Level2$ frida -U sg.vantagepoint.uncrackable2
     ____
    / _  |   Frida 9.1.27 - 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/
                                                                                
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]-> 

A little more cumbersome than just adding the -r option to Frida but also more universal.

As noted earlier, we will later not be able to extract the secret string easily when we use the patched version (though I will show you how to overcome this, so don’t throw it away). But for now we continue using the original apk. Make sure you have the original apk installed when you follow along.

Continuing the puzzle

After we have found some possibilities to get rid of anti debugging, let’s see how we can proceed. The app does a root detection and exits when run in the emulator as soon as we press the OK button. We already know that behaviour from UnCrackable1. Again, we could patch that behavior, removing calls to System.exit, but we try to solve it with Frida this time. Looking at the decompilation again, we see that there is no OnClickListener class, just an anonymous inner class. Since the onClickListener implementation calls System.exit we can can simply hook that function and render it useless.

Here is a Frida-Script to do that:

setImmediate(function() {
    console.log("[*] Starting script");
    Java.perform(function() {
        exitClass = Java.use("java.lang.System");
        exitClass.exit.implementation = function() {
            console.log("[*] System.exit called");
        }
        console.log("[*] Hooking calls to System.exit");
    });
});

Again, close any running instance of UnCrackable2 and start it with the help of Frida again:

frida -U -f sg.vantagepoint.uncrackable2 -l uncrackable2.js --no-pause 

Wait until the App started and Frida displays the Hooking calls... message in the console. Then press “Ok”. You should get something like this:

michael@sixtyseven:~/Development/frida$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js 
     ____
    / _  |   Frida 9.1.27 - 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/
Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread!           
[USB::Android Emulator 5554::['sg.vantagepoint.uncrackable2']]-> [*] Hooking calls to System.exit
[*] System.exit called

The app does not exit anymore. We are able to enter a secret string:

Uncrackable root

But what do we enter here? Have a look at the Android code in MainActivity that checks for the correct input:

this.m = new CodeCheck();

[...]

//in method: public void verify
if (this.m.a(string)) {
            alertDialog.setTitle((CharSequence)"Success!");
            alertDialog.setMessage((CharSequence)"This is the correct secret.");
}

This is the CodeCheck class:

package sg.vantagepoint.uncrackable2;

public class CodeCheck {
    private native boolean bar(byte[] var1);

    public boolean a(String string) {
        return this.bar(string.getBytes()); //Call to a native function
    }
}

We notice that the input of our text field - our “secret string” - gets passed to a native function called bar. Again, we find this function in the libfoo.so library. Search for the function address (as we did before with the init function) and disassemble it with radare2:

Uncrackable root Enlarge +

Having a look at the disassembly we notice that there is some string comparison happening and we notice an interesting plaintext string Thanks for all t. After testing that string as a solution to our crackme we notice that it doesn’t work. We have to keep going.

Looking at the disassembly at address 0x000010d8 we see

    0x000010d8      83f817         cmp eax, 0x17                                                                                                                                            
    0x000010db      7519           jne 0x10f6                  ;[1]  

so there is an comparison of eax to 0x17, which is 23 in decimal notation. If that comparison isn’t successfull, strncmp isn’t called here. We also notice the 0x17 as a parameter to strncmp in

 0x000010e1      ba17000000     mov edx, 0x17    

Remember that according to the linux 64-bit calling convention, function parameters are passed - at least parameter 1 to 6 - in registers. Especially, the first 3 parameters are passed in RDI, RSI and RDX in this order (see here [PDF], p. 20). The function header of strncmp is as follows:

int strncmp ( const char * str1, const char * str2, size_t num );

So our strncmp function will compare 0x17 = 23 characters. We can infer that our secret string should probably have a length of 23 characters.

Let’s finally try to hook the strncmp function and simply print out its arguments. We can expect that this gives us the decrypted input string. We have to

  1. Find the memory address of the strncmp function in libfoo.so
  2. Hook the strncmp function in libfoo.so with Interceptor.attach and dump the parameters

If you do this, you will find that strncmp is called on a lot of ocassions and we will therefore limit the output even further. Here is a Frida snippet:

var strncmp = undefined;
imports = Module.enumerateImportsSync("libfoo.so");

for(i = 0; i < imports.length; i++) {
if(imports[i].name == "strncmp") {
        strncmp = imports[i].address;
        break;
    }

}

Interceptor.attach(strncmp, {
            onEnter: function (args) {
               if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
                    console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
               }
            }
});

Some remarks to this script:

  1. The script calls Module.enumerateImportsSync to retreive an array of objects with information about imports from libfoo.so (- check the documentation). We iterate through this array until we find strncmp and retreive its address. Then we attach the Interceptor to it.
  2. Strings in Java are not null terminated. When we access the memory location of the strncmp string pointers with Frida’s Memory.readUtf8String method and don’t provide a length, Frida will expect a \0 terminator or else spit out some memory garbage. It doesn’t know where the string ends. If we specify the amount of characters to read as a second argument, we overcome this caveat.
  3. If we don’t put constraints on the conditions under which we dump strncmp arguments we get a lot of output. So we only output the arguments when the 3rd argument size_t to strncmp is 23 and when the first arguments points to our input in the input box where we are going to enter 01234567890123456789012 (which has - you get it - 23 characters.)

How did I know that args[0] points to our input and args[1] to the secret string? I didn’t, I just tested it and dumped a whole lot of output to the screen to find my input in it. If you don’t want to skip this, you can remove the if statement in the script above and use Frida’s hexdump output:

buf = Memory.readByteArray(args[0],32);
console.log(hexdump(buf, {
     offset: 0,
     length: 32,
     header: true,
     ansi: true
}));

buf = Memory.readByteArray(args[1],32);
console.log(hexdump(buf, {
    offset: 0,
    length: 32,
    header: true,
   ansi: true
}));

This outputs a lot of hexdumps every time strncmp is called, so be warned.

Here is the complete version of the script that outputs the parameters more nicely:

setImmediate(function() {
    Java.perform(function() {
        console.log("[*] Hooking calls to System.exit");
        exitClass = Java.use("java.lang.System");
        exitClass.exit.implementation = function() {
            console.log("[*] System.exit called");
        }

        var strncmp = undefined;
        imports = Module.enumerateImportsSync("libfoo.so");

        for(i = 0; i < imports.length; i++) {
        if(imports[i].name == "strncmp") {
                strncmp = imports[i].address;
                break;
            }

        }

        Interceptor.attach(strncmp, {
            onEnter: function (args) {
               if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
                    console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
                }
             },
        });
        console.log("[*] Intercepting strncmp");
    });
});

Now, start Frida and load the script:

frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js 

Enter the string and press verify:

Uncrackable input

In the console, you get:

michael@sixtyseven:~/Development/frida$ frida -U -f sg.vantagepoint.uncrackable2 --no-pause -l uncrackable2.js 
     ____
    / _  |   Frida 9.1.27 - 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/
Spawned `sg.vantagepoint.uncrackable2`. Resuming main thread!           
[USB::Android Emulator 5554::['sg.vantagepoint.uncrackable2']]-> [*] Hooking calls to System.exit
[*] Intercepting strncmp
[*] System.exit called
[*] Secret string at 0x7fffa628f010: Thanks for all the fish

There it is, nice and plain: our complete secret string. Feed it to the input and enjoy the success message:

Uncrackable input

Fixing the patching solution

Finally, some remark about the patching and why we wouldn’t get the secret when using the patched apk. The init function from libfoo contains some initialisation logic that prevents the app from checking or decoding the secret string against our input.

If we have a look at the disassembly of the init function again we see an interesting line:

 0x00001056      c605af2f0000.  mov byte [0x0000400c], 1 

That same variable is checked later in the bar function of libfoo and if it is not set, the code jumps over strncmp:

0x0000107d      803d882f0000.  cmp byte [0x0000400c], 1    ; [0x1:1]=69                                                                                                                 
0x00001084      7570           jne 0x10f6                  ;[1]       

So behind it is probably some boolean variable that gets set if the init function runs. If we want the patched version of our apk to call strncmp we would have to set this variable or at least prevent it from jumping over the actual strncmp call.

We could now resort to patching again, decompile the apk, overwrite the jmp instruction, and recompile everything again. Cumbersome. Since this is a Frida tutorial, we will use Frida to change the memory dynamically.

Therefore, we need to:

  1. Get the base address of the loaded foo library
  2. Locate the variable relative to the libraries base address (we know that its offset from the base address is 0x400C bytes from our disassembly)
  3. Set the variable to 1

So, in terms of Frida:

//Get base address of library
var libfoo = Module.findBaseAddress("libfoo.so");

//Calculate address of variable
var initialized = libfoo.add(ptr("0x400C"));

//Write 1 to the variable
Memory.writeInt(initialized,1);

Here is the complete script for the patched version of the app:

setImmediate(function() {
    Java.perform(function() {
        console.log("[*] Hooking calls to System.exit");
        exitClass = Java.use("java.lang.System");
        exitClass.exit.implementation = function() {
            console.log("[*] System.exit called");
        }

        var strncmp = undefined;
        imports = Module.enumerateImportsSync("libfoo.so");

        for(i = 0; i < imports.length; i++) {
            if(imports[i].name == "strncmp") {
                strncmp = imports[i].address;
                break;
            }

        }

        //Get base address of library
        var libfoo = Module.findBaseAddress("libfoo.so");

        //Calculate address of variable
        var initialized = libfoo.add(ptr("0x400C"));

        //Write 1 to the variable
        Memory.writeInt(initialized,1);

        Interceptor.attach(strncmp, {
            onEnter: function (args) {
               if(args[2].toInt32() == 23 && Memory.readUtf8String(args[0],23) == "01234567890123456789012") {
                    console.log("[*] Secret string at " + args[1] + ": " + Memory.readUtf8String(args[1],23));
                }
             },
        });
        console.log("[*] Intercepting strncmp");
    });
});

Now run the app, load the script via frida and enter our 01234567890123456789012 again. Press verify. The app calls strncmp and the secret string gets logged:

root@sixtyseven:/home/michael/Development/frida# frida -U sg.vantagepoint.uncrackable2 -l uncrackable2-final.js 
     ____
    / _  |   Frida 9.1.27 - 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/
                                                                                
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable2]-> [*] Hooking calls to System.exit
[*] Intercepting strncmp
[*] System.exit called
[*] Secret string at 0x7fffd52c6570: Thanks for all the fish

Hope you had some fun with Frida.

Comments, critique, suggestions etc as always on Twitter. Thanks for your attention.

EDIT: Thanks to @oleavr for pointing out a bug to me and the correct way for dealing with pointers in Frida.