导语:在第一篇博文中,我对Frida做了一些简单的介绍,现在,我们就使用 Frida 来进行一个安卓APP的破解实战吧。

在第一篇博文中,我对Frida做了一些简单的介绍,现在,我们就使用 Frida 来进行一个安卓APP的破解实战吧。通过之前我们对Frida用法的了解和掌握,破解一个简单的crackme 就变得非常简单了。如果你想跟着我一起操作,你需要下载下面几个文件:

1. OWASP Crackme Level 1 (APK)

2. Bytecode Viewer

3. dex2jar

当然,我假设你已经成功的安装了Frida 9.1.16或更高的版本,并且在你的已经ROOT过的安卓设备中安装了相应的server 二进制文件。我在本次教程中使用的是安卓7.1.1 的ARM版本的模拟器。

可以使用如下命令在你的安卓设备上安装 OWASP  Crackme Level 1 app:

adb install sg.vantagepoint.uncrackable1.apk

等待APP安装完毕,然后在模拟器的菜单中启动这个APP。如下图(右下角的橘黄色图标):

1.png

当你启动了这个APP后,你会发现它会提示你“不能运行在已ROOT的安卓设备中”。如下图所示:

11.png

点击“OK”后,APP会立马退出。嗯……看起来,我们无法继续破解这个APP了。真的么?让我们来看看到底发生了什么事情以及这个APP的内部到底有些什么。

使用dex2jar把这个apk文件转换成一个jar包。

[email protected]:/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代码看起来要比反编译的代码更加直观,准确一些。

1492569490192796.png

下面是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 detected”,我们找到了下面的代码:

if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())

如果你看过sg.vantagepoint.a.c 这个class文件内容,你就会看到很多个针对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,这在我们点击“OK”按钮时会被触发。

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不会退出。让我们试试吧 打开APP(让它显示“Root detected”对话框)。

1111.png

并且在这个时候注入脚本:

frida -U -l uncrackable1.js sg.vantagepoint.uncrackable1

Frida注入代码会等待几秒钟,直到你看到“onClickHandler”的消息(这个时候你可能会得到一个shell,因为我们把代码放在了一个setImmediate包装器中,所以Frida会在后台执行它)。

1492569630972757.png

然后点击APP中的“OK”按钮。如果一切顺利,APP应该不再退出。

22.png

很好:对话框消失了,现在我们可以输入密码。 我们输入一些内容,按“验证”按钮看看会发生什么:

222.png

正如预期的那样,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.a.a.a的返回值。string.equals会将我们的输入内容与arrby2进行比较。因此,我们需要关注的就是sg.vantagepoint.a.a.a的返回值。

我们现在可以开始对字符串操作和解密功能进行逆向分析,并对原始的加密字符串进行处理,这些字符串也包含在上面的代码中。或者我们让APP执行我们并不在乎的所有加密处理过程,我们只需要HOOK sg.vantagepoint.a.a.a这个函数来捕获它的返回值就行。 返回值是解密后字符串(以字节数组的形式)并与我们的输入进行比较。 下面是要注入的脚本的作用:

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.a.a.a函数,捕获其返回值并将其转换为可读字符串。 这是我们正在寻找的解密后的字符串,所以我们将其打印到控制台,并希望得到我们的破解解决方案。

把这些代码片段放在一起,这里是完整的脚本:

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.a.a.a被修改的消息后,在提示Root detected的对话框中单击“OK”,然后在密码字段中输入一些内容,然后点击“验证”按钮。然而, 我们在模拟器中仍然没有好的运气。

不过请注意Frida的输出:

[email protected]:~/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

很好,实际上我们已经得到了解密的字符串:“I want to believe”。 没错,就是它。我们来看看它是否有效:

2222.png

到目前为止,我希望你至少对Frida可以做些什么有了一些深刻的印象了——它是一个动态的二进制插桩工具。

源链接

Hacking more

...