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
- the OWASP Uncrackable Level2 APK
- Android SDK and Emulator (I am using an Android 7.1 x64 image)
- a working installation of Frida (plus release frida-server binary)
- Bytecode viewer
- radare2 (or some other disassembler of your choice)
- apktool
Note: Since the time of writing this post, the crackme has changed slightly, so your disassembly / decompilation might differ. The overall methodology should still be valid.
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.
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:
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:
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
- Find the memory address of the
strncmp
function inlibfoo.so
- Hook the
strncmp
function inlibfoo.so
withInterceptor.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:
- The script calls
Module.enumerateImportsSync
to retreive an array of objects with information about imports fromlibfoo.so
(- check the documentation). We iterate through this array until we findstrncmp
and retreive its address. Then we attach the Interceptor to it. - Strings in Java are not null terminated. When we access the memory location of the
strncmp
string pointers with Frida’sMemory.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. - 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 argumentsize_t
tostrncmp
is 23 and when the first arguments points to our input in the input box where we are going to enter01234567890123456789012
(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:
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:
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:
- Get the base address of the loaded
foo
library - Locate the variable relative to the libraries base address (we know that its offset from the base address is 0x400C bytes from our disassembly)
- 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.