After the introduction to Frida, 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 12.2.18 or later) on your computer and started the corresponding server binary on the (rooted) device. I'm going to use an Android 7.0 ARM64 in smartphone Samsung Galaxy S6 for this tutorial.
Install the Uncrackable Crackme Level 1 app on your device:
wget -c -k https://github.com/OWASP/owasp-mstg/raw/master/Crackmes/Android/Level_01/UnCrackable-Level1.apk &&
adb install UnCrackable-Level1.apk
Wait until it is installed, then start it. Once you start the app you will notice that it doesn't want to run on a rooted device.
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.
Open APK using JADX-GUI on Host OS.
Here is the output of MainActivity.java
package sg.vantagepoint.uncrackable1;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import owasp.mstg.uncrackable1.R;
import sg.vantagepoint.a.b;
import sg.vantagepoint.a.c;
public class MainActivity extends Activity {
private void a(String str) {
AlertDialog create = new Builder(this).create();
create.setTitle(str);
create.setMessage("This is unacceptable. The app is now going to exit.");
create.setButton(-3, "OK", new OnClickListener(this) {
final /* synthetic */ MainActivity a;
{
this.a = r1;
}
public void onClick(DialogInterface dialogInterface, int i) {
System.exit(0);
}
});
create.setCancelable(false);
create.show();
}
protected void onCreate(Bundle bundle) {
if (c.a() || c.b() || c.c()) {
a("Root detected!");
}
if (b.a(getApplicationContext())) {
a("App is debuggable!");
}
super.onCreate(bundle);
setContentView(R.layout.activity_main);
}
public void verify(View view) {
CharSequence charSequence;
String obj = ((EditText) findViewById(R.id.edit_text)).getText().toString();
AlertDialog create = new Builder(this).create();
if (a.a(obj)) {
create.setTitle("Success!");
charSequence = "This is the correct secret.";
} else {
create.setTitle("Nope...");
charSequence = "That's not it. Try again.";
}
create.setMessage(charSequence);
create.setButton(-3, "OK", new OnClickListener(this) {
final /* synthetic */ MainActivity a;
{
this.a = r1;
}
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
create.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 (c.a() || c.b() || c.c()) {
a("Root detected!");
}
If you have a look at the sg.vantagepoint.a.c class you see the various checks for root:
package sg.vantagepoint.a;
import android.os.Build;
import java.io.File;
public class c {
public static boolean a() {
for (String file : System.getenv("PATH").split(":")) {
if (new File(file, "su").exists()) {
return true;
}
}
return false;
}
public static boolean b() {
String str = Build.TAGS;
return str != null && str.contains("test-keys");
}
public static boolean c() {
for (String file : new String[]{"/system/app/Superuser.apk", "/system/xbin/daemonsu", "/system/etc/init.d/99SuperSUDaemon", "/system/bin/.ext/.su", "/system/etc/.has_su_daemon", "/system/etc/.installed_su_daemon", "/dev/com.koushikdutta.superuser.daemon/"}) {
if (new File(file).exists()) {
return true;
}
}
return false;
}
}
Before overwriting function on spesific class, we need to enumerate related classses.
vi scripts/enumClassName.js
Write codes.
Java.perform(function(){
Java.enumerateLoadedClasses({
"onMatch":function(className){
var str = JSON.stringify(className)
if (str.startsWith('"sg.vantagepoint')){
console.log(str)
}
},
"onComplete":function(){}
})
})
$ frida -U -l scripts/enumClassName.js owasp.mstg.uncrackable1
____
/ _ | Frida 12.2.18 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Attaching...
"sg.vantagepoint.uncrackable1.MainActivity"
"sg.vantagepoint.a.b"
"sg.vantagepoint.a.c"
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:
private void a(String str) {
AlertDialog create = new Builder(this).create();
create.setTitle(str);
create.setMessage("This is unacceptable. The app is now going to exit.");
create.setButton(-3, "OK", new OnClickListener(this) {
final /* synthetic */ MainActivity a;
{
this.a = r1;
}
public void onClick(DialogInterface dialogInterface, int i) {
System.exit(0);
}
});
create.setCancelable(false);
create.show();
}
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 System.exit()
method with Frida. Create a file systemExit.js
and put your code inside:
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
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");
}
});
})
If you have read Introduction to Frida, 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. Unlike the original, it does not exit the app. Since the original System.exit
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)
Inject the script:
frida -U -l scripts/systemExit.js owasp.mstg.uncrackable1
____
/ _ | Frida 12.2.18 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at http://www.frida.re/docs/home/
Attaching...
[*] Hooking calls to System.exit
[Samsung SM-G920F::owasp.mstg.uncrackable1]-> [*] System.exit called
[Samsung SM-G920F::owasp.mstg.uncrackable1]->
Root detection bypassed.