MASTG-TECH-0043: 方法 Hook
Xposed¶
假设你正在测试一个在已root设备上顽固退出的应用。你反编译该应用,并发现以下高度可疑的方法
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;
}
此方法遍历目录列表,如果在其中任何一个目录中找到 su
二进制文件,则返回 true
(设备已root)。像这样的检查很容易被禁用,你所要做的就是用返回“false”的代码替换它。使用 Xposed 模块进行方法挂钩是执行此操作的一种方式(有关 Xposed 安装和基础知识的更多详细信息,请参阅“Android 基本安全测试”)。
XposedHelpers.findAndHookMethod
方法允许你覆盖现有的类方法。通过检查反编译的源代码,你可以发现执行检查的方法是 c
。此方法位于 com.example.a.b
类中。以下是一个 Xposed 模块,它覆盖该函数,使其始终返回 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);
}
});
}
}
与常规 Android 应用一样,Xposed 的模块也是使用 Android Studio 开发和部署的。有关编写、编译和安装 Xposed 模块的更多详细信息,请参阅其作者 rovo89 提供的教程。
Frida¶
我们将使用 Frida 来解决 * Android UnCrackable L1,并演示我们如何轻松绕过 root 检测并从应用中提取秘密数据。
当你在模拟器或已 root 设备上启动 crackme 应用时,你会发现它会显示一个对话框并在你按下“确定”后立即退出,因为它检测到 root
让我们看看如何阻止这种情况。
主方法(使用 CFR 反编译)如下所示
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();
}
}
请注意 onCreate
方法中的“Root detected”消息以及前面的 if
语句中调用的各种方法(这些方法执行实际的 root 检查)。还要注意该类的第一个方法 private void a
中的“This is unacceptable...”消息。显然,此方法显示对话框。在 setButton
方法调用中设置了一个 alertDialog.onClickListener
回调,该回调在成功检测到 root 后通过 System.exit
关闭应用程序。使用 Frida,你可以通过挂钩 MainActivity.a
方法或其中的回调来阻止应用退出。以下示例演示了如何挂钩 MainActivity.a
并阻止它结束应用程序。
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");
});
});
将你的代码包装在函数 setImmediate
中以防止超时(你可能需要或不需要这样做),然后调用 Java.perform
以使用 Frida 的方法来处理 Java。之后,检索 MainActivity
类的包装器并覆盖其 a
方法。与原始版本不同,新版本的 a
只是写入控制台输出,不会退出应用。另一种解决方案是挂钩 OnClickListener
接口的 onClick
方法。你可以覆盖 onClick
方法并阻止它使用 System.exit
调用结束应用程序。如果要注入自己的 Frida 脚本,则应完全禁用 AlertDialog
或更改 onClick
方法的行为,以便在单击“确定”时应用不会退出。
将以上脚本另存为 uncrackable1.js
并加载它
frida -U -f owasp.mstg.uncrackable1 -l uncrackable1.js --no-pause
在你看到“MainActivity.a modified”消息后,应用将不再退出。
你现在可以尝试输入“secret string”。但是你从哪里获得它呢?
如果你查看 sg.vantagepoint.uncrackable1.a
类,你可以看到你的输入与之进行比较的加密字符串
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;
}
}
查看 a
方法末尾的 string.equals
比较以及上面 try
块中字符串 arrby
的创建。arrby
是函数 sg.vantagepoint.a.a.a
的返回值。string.equals
比较你的输入与 arrby
。所以我们想要 sg.vantagepoint.a.a.a
的返回值。
你可以简单地忽略应用程序中的所有解密逻辑并挂钩 sg.vantagepoint.a.a.a
函数以捕获其返回值,而不是反转解密例程以重建密钥。 这是完整的脚本,可防止在 root 上退出并拦截密钥的解密
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");
});
});
在 Frida 中运行脚本并在控制台中看到“[*] sg.vantagepoint.a.a.a modified”消息后,为“secret string”输入一个随机值并按验证。你应该获得类似于以下的输出
$ 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
挂钩的函数输出了解密的字符串。 你提取了秘密字符串,而无需深入研究应用程序代码及其解密例程。
你现在已经了解了 Android 上静态/动态分析的基础知识。当然,真正 学习它的唯一方法是实践经验:在 Android Studio 中构建自己的项目,观察你的代码如何转换为字节码和本机代码,并尝试破解我们的挑战。
在剩余的部分中,我们将介绍一些高级主题,包括进程探索、内核模块和动态执行。