MASTG-TECH-0043: Method Hooking
Xposed¶
Let's assume you're testing an app that's stubbornly quitting on your rooted device. You decompile the app and find the following highly suspect method:
package com.example.a.b
public static boolean c() {
int v3 = 0;
boolean v0 = false;
String[] v1 = new String[]{"/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/",
"/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/"};
int v2 = v1.length;
for(int v3 = 0; v3 < v2; v3++) {
if(new File(String.valueOf(v1[v3]) + "su").exists()) {
v0 = true;
return v0;
}
}
return v0;
}
This method iterates through a list of directories and returns true
(device rooted) if it finds the su
binary in any of them. Checks like this are easy to deactivate all you have to do is replace the code with something that returns "false". Method hooking with an Xposed module is one way to do this (see "Android Basic Security Testing" for more details on Xposed installation and basics).
The method XposedHelpers.findAndHookMethod
allows you to override existing class methods. By inspecting the decompiled source code, you can find out that the method performing the check is c
. This method is located in the class com.example.a.b
. The following is an Xposed module that overrides the function so that it always returns false:
package com.awesome.pentestcompany;
import static de.robv.android.xposed.XposedHelpers.findAndHookMethod;
import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam;
public class DisableRootCheck implements IXposedHookLoadPackage {
public void handleLoadPackage(final LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.example.targetapp"))
return;
findAndHookMethod("com.example.a.b", lpparam.classLoader, "c", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
XposedBridge.log("Caught root check!");
param.setResult(false);
}
});
}
}
Just like regular Android apps, modules for Xposed are developed and deployed with Android Studio. For more details on writing, compiling, and installing Xposed modules, refer to the tutorial provided by its author, rovo89.
Frida¶
We'll use Frida to solve the Android UnCrackable L1 and demonstrate how we can easily bypass root detection and extract secret data from the app.
When you start the crackme app on an emulator or a rooted device, you'll find that the it presents a dialog box and exits as soon as you press "OK" because it detected root:
Let's see how we can prevent this.
The main method (decompiled with CFR) looks like this:
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.a.b;
import sg.vantagepoint.a.c;
import sg.vantagepoint.uncrackable1.a;
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 is unacceptable. The app is now going to exit.");
alertDialog.setButton(-3, (CharSequence)"OK", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialogInterface, int n) {
System.exit((int)0);
}
});
alertDialog.setCancelable(false);
alertDialog.show();
}
protected void onCreate(Bundle bundle) {
if (c.a() || c.b() || c.c()) {
this.a("Root detected!");
}
if (b.a(this.getApplicationContext())) {
this.a("App is debuggable!");
}
super.onCreate(bundle);
this.setContentView(2130903040);
}
/*
* Enabled aggressive block sorting
*/
public void verify(View object) {
object = ((EditText)this.findViewById(2130837505)).getText().toString();
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
if (a.a((String)object)) {
alertDialog.setTitle((CharSequence)"Success!");
object = "This is the correct secret.";
} else {
alertDialog.setTitle((CharSequence)"Nope...");
object = "That's not it. Try again.";
}
alertDialog.setMessage((CharSequence)object);
alertDialog.setButton(-3, (CharSequence)"OK", new DialogInterface.OnClickListener(){
public void onClick(DialogInterface dialogInterface, int n) {
dialogInterface.dismiss();
}
});
alertDialog.show();
}
}
Notice the "Root detected" message in the onCreate
method and the various methods called in the preceding if
-statement (which perform the actual root checks). Also note the "This is unacceptable..." message from the first method of the class, private void a
. Obviously, this method displays the dialog box. There is an alertDialog.onClickListener
callback set in the setButton
method call, which closes the application via System.exit
after successful root detection. With Frida, you can prevent the app from exiting by hooking the MainActivity.a
method or the callback inside it. The example below shows how you can hook MainActivity.a
and prevent it from ending the application.
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
var mainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainActivity.a.implementation = function(v) {
console.log("[*] MainActivity.a called");
};
console.log("[*] MainActivity.a modified");
});
});
Wrap your code in the function setImmediate
to prevent timeouts (you may or may not need to do this), then call Java.perform
to use Frida's methods for dealing with Java. Afterwards retrieve a wrapper for MainActivity
class and overwrite its a
method. Unlike the original, the new version of a
just writes console output and doesn't exit the app. An alternative solution is to hook onClick
method of the OnClickListener
interface. You can overwrite the onClick
method and prevent it from ending the application with the System.exit
call. If you want to inject your own Frida script, it should either disable the AlertDialog
entirely or change the behavior of the onClick
method so the app does not exit when you click "OK".
Save the above script as uncrackable1.js
and load it:
frida -U -f owasp.mstg.uncrackable1 -l uncrackable1.js --no-pause
After you see the "MainActivity.a modified" message and the app will not exit anymore.
You can now try to input a "secret string". But where do you get it?
If you look at the class sg.vantagepoint.uncrackable1.a
, you can see the encrypted string with which your input gets compared:
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
public class a {
public static boolean a(String string) {
byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
try {
arrby = sg.vantagepoint.a.a.a(a.b("8d127684cbc37c17616d806cf50473cc"), arrby);
}
catch (Exception exception) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("AES error:");
stringBuilder.append(exception.getMessage());
Log.d((String)"CodeCheck", (String)stringBuilder.toString());
arrby = new byte[]{};
}
return string.equals((Object)new String(arrby));
}
public static byte[] b(String string) {
int n = string.length();
byte[] arrby = new byte[n / 2];
for (int i = 0; i < n; i += 2) {
arrby[i / 2] = (byte)((Character.digit((char)string.charAt(i), (int)16) << 4) + Character.digit((char)string.charAt(i + 1), (int)16));
}
return arrby;
}
}
Look at the string.equals
comparison at the end of the a
method and the creation of the string arrby
in the try
block above. arrby
is the return value of the function sg.vantagepoint.a.a.a
. string.equals
comparison compares your input with arrby
. So we want the return value of sg.vantagepoint.a.a.a.
Instead of reversing the decryption routines to reconstruct the secret key, you can simply ignore all the decryption logic in the app and hook the sg.vantagepoint.a.a.a
function to catch its return value.
Here is the complete script that prevents exiting on root and intercepts the decryption of the secret string:
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
var mainActivity = Java.use("sg.vantagepoint.uncrackable1.MainActivity");
mainActivity.a.implementation = function(v) {
console.log("[*] MainActivity.a called");
};
console.log("[*] MainActivity.a modified");
var aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
var retval = this.a(arg1, arg2);
var password = '';
for(var 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");
});
});
After running the script in Frida and seeing the "[*] sg.vantagepoint.a.a.a modified" message in the console, enter a random value for "secret string" and press verify. You should get an output similar to the following:
$ frida -U -f owasp.mstg.uncrackable1 -l uncrackable1.js --no-pause
[*] Starting script
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] MainActivity.a modified
[*] sg.vantagepoint.a.a.a modified
[*] MainActivity.a called.
[*] Decrypted: I want to believe
The hooked function outputted the decrypted string. You extracted the secret string without having to dive too deep into the application code and its decryption routines.
You've now covered the basics of static/dynamic analysis on Android. Of course, the only way to really learn it is hands-on experience: build your own projects in Android Studio, observe how your code gets translated into bytecode and native code, and try to crack our challenges.
In the remaining sections, we'll introduce a few advanced subjects, including process exploration, kernel modules and dynamic execution.