在第一篇博文中,我对 Frida 做了一个简单的介绍。现在,让我们用Frida来破解一个Android APP。通过我们之前对Frida用法的理解和掌握,破解一个简单的crackme变得非常容易。如果你想跟着我,你需要下载以下文件:
1. OWASP Crackme 1 级 (APK)
2.字节码查看器
3.dex2jar
当然,我假设你已经成功安装了 Frida 9.1.16 或更高版本,并在你的根 Android 设备上安装了相应的服务器二进制文件。我在本教程中使用的是 Android 7.1.1 ARM 版本的模拟器。
可以使用以下命令在您的 Android 设备上安装 OWASP Crackme Level 1 应用程序:
adb install sg.vantagepoint.uncrackable1.apk
等待安装应用程序,然后在模拟器的菜单中启动应用程序。如下图(右下角橙色图标):
启动APP,会发现提示“Cannot run on rooted Android device”。如下所示:
点击“确定”后,APP会立即退出。嗯......看起来我们不能再继续破解应用程序了。真的吗?让我们来看看发生了什么以及应用程序内部发生了什么。
使用 dex2jar 将这个 apk 文件转换成 jar 包。
michael@sixtyseven:/opt/dex2jar/dex2jar-2.0$ ./d2j-dex2jar.sh -o /home/michael/UnCrackable-Level1.jar /home/michael/UnCrackable-Level1.apk
dex2jar /home/michael/UnCrackable-Level1.apk -> /home/michael/UnCrackable-Level1.jar
然后把这个jar加载到BytecodeViewer中,也可以选择使用其他支持Java的反汇编工具。也可以选择直接将APK文件加载到BytecodeViewer,但是我在做的时候遇到了一个问题,所以我用dex2jar把APK转成jar,然后导入到BytecodeViewer。
在 BytecodeViewer 中,选择 View->Pane1->CFR->Java 菜单使用 CFR 反编译器。如果您想将反编译结果与 Smali 代码进行比较,可以将面板 2 设置为 Smali 代码。Smali 代码看起来比反编译代码更直观、更准确。
下面是CFR反编译器对这个APP的MainActivity进行反编译的输出:
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.uncrackable1.a;
import sg.vantagepoint.uncrackable1.b;
import sg.vantagepoint.uncrackable1.c;
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 in unacceptable. The app is now going to exit.");
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
alertDialog.show();
}
protected void onCreate(Bundle bundle) {
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c()) {
this.a("Root detected!"); //This is the message we are looking for
}
if (sg.vantagepoint.a.b.a((Context)this.getApplicationContext())) {
this.a("App is debuggable!");
}
super.onCreate(bundle);
this.setContentView(2130903040);
}
public void verify(View object) {
object = ((EditText)this.findViewById(2131230720)).getText().toString();
AlertDialog alertDialog = new AlertDialog.Builder((Context)this).create();
if (a.a((String)object)) {
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 c(this));
alertDialog.show();
}
}
看了其他反编译的class文件,发现这是一个小APP,所以完全可以逆向这个crackme的解密算法,修改通用字符串。不过我们有Frida这个破解工具,操作起来会更加方便简单。让我们看看这个APP在哪里检查设备是否被root。根据上面提示的消息内容“检测到Root”,我们发现如下代码:
if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())
如果你查看 sg.vantagepoint.ac 类文件的内容,你会看到很多 root 的检查代码:
public static boolean a()
{
String[] a = System.getenv("PATH").split(":");
int i = a.length;
int i0 = 0;
while(true)
{
boolean b = false;
if (i0 >= i)
{
b = false;
}
else
{
if (!new java.io.File(a[i0], "su").exists())
{
i0 = i0 + 1;
continue;
}
b = true;
}
return b;
}
}
public static boolean b()
{
String s = android.os.Build.TAGS;
if (s != null && s.contains((CharSequence)(Object)"test-keys"))
{
return true;
}
return false;
}
public static boolean c()
{
String[] a = new String[7];
a[0] = "/system/app/Superuser.apk";
a[1] = "/system/xbin/daemonsu";
a[2] = "/system/etc/init.d/99SuperSUDaemon";
a[3] = "/system/bin/.ext/.su";
a[4] = "/system/etc/.has_su_daemon";
a[5] = "/system/etc/.installed_su_daemon";
a[6] = "/dev/com.koushikdutta.superuser.daemon/";
int i = a.length;
int i0 = 0;
while(i0 < i)
{
if (new java.io.File(a[i0]).exists())
{
return true;
}
i0 = i0 + 1;
}
return false;
}
使用 Frida,我们可以通过覆盖本教程系列第一部分中介绍的方法来覆盖这些方法以返回 false。但是当一个方法返回 true 时究竟会发生什么呢?是不是因为发现设备被root了?我们在 MainActivity 的函数 a 中看到它打开了一个表单。并设置一个 onClickListener,当我们单击“确定”按钮时会触发它。
alertDialog.setButton(-3, (CharSequence)"OK", (DialogInterface.OnClickListener)new b(this));
监听器onClickListener的实现代码不多。
package sg.vantagepoint.uncrackable1;
class b implements android.content.DialogInterface$OnClickListener {
final sg.vantagepoint.uncrackable1.MainActivity a;
b(sg.vantagepoint.uncrackable1.MainActivity a0)
{
this.a = a0;
super();
}
public void onClick(android.content.DialogInterface a0, int i)
{
System.exit(0);
}
}
上面的代码只是使用 System.exit(0) 来退出 APP。所以我们要做的就是阻止APP退出。让我们使用 Frida 重写 onClick 方法。
创建一个名为 uncrackable1.js 的文件并将以下代码写入此文件。
setImmediate(function() { //prevent timeout
console.log("[*] Starting script");
Java.perform(function() {
bClass = Java.use("sg.vantagepoint.uncrackable1.b");
bClass.onClick.implementation = function(v) {
console.log("[*] onClick called");
}
console.log("[*] onClick handler modified")
})
})
如果你看过本系列教程的第一部分,你应该明白代码了:我们将代码包装在 setImmediate 函数中,防止超时(你可能不需要注意这一点),然后调用 Java.perform通过 Java 使用 Frida 内置的一些方法。实际工作的代码是:我们检索这个类的包装器,实现 OnClickListener 接口并覆盖它的 onClick 方法。在我们的版本中,这个函数只是向控制台打印一些东西。而不是像以前那样退出APP。由于原来的 onClickHandler 被我们的 Frida 注入函数替换了,并且永远不会被调用,所以当我们点击对话框的 OK 按钮时,APP 并没有退出。让我们尝试打开应用程序(让它显示“检测到根”对话框)。
并在此时注入脚本:
frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
Frida 注入的代码将等待几秒钟,直到您看到“onClickHandler”消息(此时您可能会得到一个 shell,因为我们将代码放在 setImmediate 包装器中,所以 Frida 将在后台执行它)。
然后点击APP中的“确定”按钮。如果一切顺利,APP应该不再退出。
太好了:对话框消失了,现在我们可以输入密码了。让我们输入一些内容,按下“验证”按钮,看看会发生什么:
不出所料,APP提示错误代码。但是我们有一个我们正在寻找的想法:某种加密/解密输入并将结果与??您输入的内容进行比较的算法。
再次查看 MainActivity软件破解教程,我们在函数中看到如下代码:
public void verify(View object) {
此函数调用 sg.vantagepoint.uncrackable1.a 中的方法 a:
if (a.a((String)object)) {
这是 sg.vantagepoint.uncrackable1.a 类的反编译代码:
package sg.vantagepoint.uncrackable1;
import android.util.Base64;
import android.util.Log;
/*
* Exception performing whole class analysis ignored.
*/
public class a {
public static boolean a(String string) {
byte[] arrby = Base64.decode((String)"5UJiFctbmgbDoLXmpL12mkno8HT4Lv8dlat8FxR2GOc=", (int)0);
byte[] arrby2 = new byte[]{};
try {
arrby2 = arrby = sg.vantagepoint.a.a.a((byte[])a.b((String)"8d127684cbc37c17616d806cf50473cc"), (byte[])arrby);
}
catch (Exception var2_2) {
Log.d((String)"CodeCheck", (String)("AES error:" + var2_2.getMessage()));
}
if (!string.equals(new String(arrby2))) return false;
return true;
}
public static byte[] b(String string) {
int n = string.length();
byte[] arrby = new byte[n / 2];
int n2 = 0;
while (n2 < n) {
arrby[n2 / 2] = (byte)((Character.digit(string.charAt(n2), 16) << 4) + Character.digit(string.charAt(n2 + 1), 16));
n2 += 2;
}
return arrby;
}
}
注意上面代码中a方法末尾的string.equals比较代码以及try代码块中arrby2的创建。arrby2 是 sg.vantagepoint.aaa 的返回值。string.equals 会将我们的输入与 arrby2 进行比较。因此,我们需要注意的是 sg.vantagepoint.aaa 的返回值。
我们现在可以开始对字符串操作和解密函数进行逆向工程,并处理原始加密字符串,这些字符串也包含在上面的代码中。或者我们让APP执行所有我们不关心的加密处理,我们只需要HOOK sg.vantagepoint.aaa这个函数来捕获它的返回值。返回值是解密后的字符串(以字节数组的形式)并与我们的输入进行比较。以下是要注入的脚本的作用:
aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
retval = this.a(arg1, arg2);
password = ''
for(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");
我们重写了 sg.vantagepoint.aaa 函数以捕获其返回值并将其转换为可读字符串。这是我们正在寻找的解密字符串,因此我们将其打印到控制台并希望得到我们的破解解决方案。
将这些代码片段放在一起,这是完整的脚本:
setImmediate(function() {
console.log("[*] Starting script");
Java.perform(function() {
bClass = Java.use("sg.vantagepoint.uncrackable1.b");
bClass.onClick.implementation = function(v) {
console.log("[*] onClick called.");
}
console.log("[*] onClick handler modified")
aaClass = Java.use("sg.vantagepoint.a.a");
aaClass.a.implementation = function(arg1, arg2) {
retval = this.a(arg1, arg2);
password = ''
for(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");
});
});
让我们运行这个脚本。和以前一样,将其保存为 uncrackable1.js 并执行(如果 Frida 没有自动重新运行)。
frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
等到您看到 sg.vantagepoint.aaa 已被修改的消息,在显示检测到 Root 的对话框中单击“确定”,然后在密码字段中输入内容软件破解教程,然后单击“验证”按钮。但是,我们在模拟器中仍然没有好运气。
但是请注意 Frida 的输出:
michael@sixtyseven:~/Development/frida$ frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1
____
/ _ | Frida 9.1.16 - 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/
[*] Starting script
[USB::Android Emulator 5554::sg.vantagepoint.uncrackable1]-> [*] onClick handler modified
[*] sg.vantagepoint.a.a.a modified
[*] onClick called.
[*] Decrypted: I want to believe
太好了,实际上我们得到了解密的字符串:“我想相信”。是的,就是这样。让我们看看它是否有效:
到目前为止,我希望您至少对 Frida 的功能印象深刻——它是一个动态二进制检测工具。
本文翻译自:codemetrix.net-IT与IT-Security笔记,如转载请注明出处:咆哮:安卓APP破解工具Frida破解实战更多内容请关注《咆哮专业版》——专业版