利用FRIDA攻击Android应用程序(二)

简介:

本系列文章的第一篇中,我们已经对Frida的原理进行了详细的介绍,现在,我们将演示如何通过Frida搞定crackme问题。有了第一篇的内容作为基础,理论上讲这应该不是什么难事。如果你想亲自动手完成本文介绍的实验的话,请下载 

OWASP Uncrackable Crackme Level 1 (APK)

BytecodeViewer

dex2jar


当然,这里假定您已在计算机上成功地安装了Frida(版本9.1.16或更高版本),并在(已经获得root权限的)设备上启动了相应服务器的二进制代码。我们这里将在模拟器中使用Android 7.1.1 ARM映像。

然后,请在您的设备上安装Uncrackable Crackme Level 1应用程序: 

adb install sg.vantagepoint.uncrackable1.apk


安装完成后,从模拟器的菜单(右下角的橙色图标)启动它: 


一旦启动应用程序,您就会注意到它不太乐意在已经获取root权限的设备上运行: 


如果单击“OK”,应用程序会立即退出。嗯,不太友好啊。看起来我们无法通过这种方法来搞定crackme。真是这样吗?让我们看看到底怎么回事,同时考察一下这个应用程序的内部运行机制。


现在,使用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


然后,将其加载到BytecodeViewer(或其他支持Java的反汇编器)中。你也可以尝试直接加载到BytecodeViewer中,或直接提取classes.dex,但是试了一下好像此路不通,所以我才提前使用dex2jar完成相应的转换。


为了使用CFR解码器,需要在BytecodeViewer中依次选择View-> Pane1-> CFR-> Java。如果你想将反编译器的结果与Smali反汇编(通常比反编译稍微准确一些)进行比较的话,可以将Pane2设置为Smali代码。



下面是CFR解码器针对应用程序的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();
    }
}


if (sg.vantagepoint.a.c.a() || sg.vantagepoint.a.c.b() || sg.vantagepoint.a.c.c())通过查看其他反编译的类文件,我们发现它是一个小应用程序,并且貌似可以通过逆向解密例程和字符串修改例程来解决这个crackme问题。然而,既然有神器Frida在手,自然会有更方便的手段可供我们选择。首先,让我们看看这个应用程序是在哪里检查设备是否已获取root权限的。在“Root detected”消息上面,我们可以看到: 

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


如果你查看sg.vantagepoint.a.c类的话,你就会发现与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,这一点我们已经在第一篇中介绍过了。但是,当一个函数由于检测到设备已经取得了root权限而返回true时,结果会怎样呢? 正如我们在MainActivity函数中看到的那样,它会打开一个对话框。此外,它还会设置一个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)退出应用程序而已。所以我们要做的事情就是防止应用程序退出。为此,我们可以用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来使用Frida用于处理Java的方法。接下来,我们将得到一个类的包装器,可用于实现OnClickListener接口并覆盖其onClick方法。在我们的版本中,这个函数只是向控制台写一些输出。与之前不同的是,它不会退出应用程序。由于原来的onClickHandler被替换为Frida注入的函数,因此它绝对不会被调用了,所以当我们点击对话框的OK按钮时,应用程序就不退出了。好了,让我们实验一下:打开应用程序(使其显示“Root detected”对话框) 



并注入脚本: 

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


Frida注入代码需要几秒钟的时间,当你看到“onClick handler modified”消息时说明注入完成了(当然,注入完成时你也可以得到一个shell之前,因为可以把我们的代码放入一个setImmediate包装器中,从而让Frida在后台执行它)。



然后,点击应用程序中的OK按钮。如果一切顺利的话,应用程序就不会退出了。



我们看到对话框消失了,这样我们就可以输入密码了。下面让我们输入一些内容,点击Verify,看看会发生什么情况: 



不出所料,这是一个错误的密码。但是这并不要紧,因为我们真正要找的是:加密/解密例程以及结果和输入的比对。

再次检查MainActivity时,我们注意到了下面的函数 

public void verify(View object) {


它调用了类sg.vantagepoint.uncrackable1.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的返回值。


现在,我们可以着手对这些字符串操作函数和解密函数进行逆向工程,并处理原始加密字符串了,实际上它们也包含在上面的代码中。或者,我们还可以让应用程序替我们完成字符串的处理和加密工作,而我们只要钩住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发生变化,然后在Root detected对话框中单击OK,在secret code中输入一些字符,然后按Verify按钮。哎,运气好像不太好啊。

但是,请注意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


太好了。我们实际上已经得到了解密的字符串:I want to believe。那么,我们赶紧输入这个字符串,看看是否正确: 



本文到此结束,但愿读者阅读本文后,能够对学习Frida的动态二进制插桩功能有所帮助。


相关文章
|
2天前
|
移动开发 API Android开发
构建高效Android应用:Kotlin协程的实践指南
【5月更文挑战第11天】 在移动开发领域,性能优化和资源管理是至关重要的。特别地,对于Android开发者来说,合理利用Kotlin协程可以极大地改善应用的响应性和稳定性。本文将深入探讨Kotlin协程在Android中的实际应用,包括它们如何简化异步编程模型、提高UI线程的响应性,以及减少内存消耗。我们将通过具体案例分析,了解如何在实际项目中有效地使用协程,从而帮助开发者构建更加高效的Android应用程序。
|
4天前
|
Android开发
Android应用实例(一)之---有道辞典VZ.0
Android应用实例(一)之---有道辞典VZ.0
10 2
|
13天前
|
移动开发 Java Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第30天】在移动开发领域,随着用户需求的不断增长和设备性能的持续提升,实现流畅且高效的用户体验已成为开发者的首要任务。针对Android平台,Kotlin协程作为一种新兴的异步编程解决方案,以其轻量级线程管理和简洁的代码逻辑受到广泛关注。本文将深入探讨Kotlin协程的概念、优势以及在实际Android应用中的运用,通过实例演示如何利用协程提升应用性能和响应能力,为开发者提供一条构建更高效Android应用的实践路径。
|
13小时前
|
移动开发 Android开发 开发者
构建高效Android应用:探究Kotlin协程的优化实践
【5月更文挑战第13天】 在移动开发领域,Android平台的流畅体验至关重要。随着Kotlin语言的普及,协程作为其核心特性之一,为异步编程提供了简洁且高效的解决方案。本文将深入探讨Kotlin协程在Android应用中的优化使用,从基本概念到实际案例分析,旨在帮助开发者构建更加响应迅速、性能卓越的应用。我们将通过对比传统线程与协程的差异,展示如何利用协程简化代码结构,并通过优化实践减少资源消耗,提升用户体验。
|
1天前
|
移动开发 监控 Android开发
构建高效Android应用:Kotlin协程的实践与优化
【5月更文挑战第12天】 在移动开发领域,性能与响应性是衡量一个应用程序优劣的关键指标。特别是在Android平台上,由于设备的多样性和系统资源的限制,开发者需要精心编写代码以确保应用流畅运行。近年来,Kotlin语言因其简洁性和功能性而广受欢迎,尤其是其协程特性,为异步编程提供了强大而轻量级的解决方案。本文将深入探讨如何在Android应用中使用Kotlin协程来提升性能,以及如何针对实际问题进行优化,确保应用的高效稳定执行。
|
4天前
|
开发工具 Android开发 Windows
Android应用] 问题2:ERROR: unknown virtual device name:
Android应用] 问题2:ERROR: unknown virtual device name:
|
4天前
|
XML JSON API
转Android上基于JSON的数据交互应用
转Android上基于JSON的数据交互应用
|
5天前
|
安全 Java Android开发
构建高效Android应用:采用Kotlin进行内存优化的策略
【5月更文挑战第8天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,合理管理内存资源是确保应用流畅运行的关键因素之一。近年来,Kotlin作为官方推荐的开发语言,以其简洁、安全和互操作性的特点受到开发者青睐。本文将深入探讨利用Kotlin语言特性,通过具体策略对Android应用的内存使用进行优化,旨在帮助开发者提高应用性能,减少内存消耗,避免常见的内存泄漏问题。
8 0
|
6天前
|
移动开发 数据库 Android开发
构建高效Android应用:Kotlin协程的全面应用
【5月更文挑战第7天】 在移动开发领域,性能优化与流畅的用户体验是至关重要的。随着Kotlin语言的流行,其并发神器——协程,已成为提升Android应用性能的重要工具。本文将深入探讨如何在Android项目中利用Kotlin协程进行异步编程、网络请求和数据库操作,以及如何通过协程简化代码结构,增强应用的响应性和稳定性。我们的目标是为开发者提供一套实用的协程使用模式和最佳实践,以便构建更加高效的Android应用。
23 3
|
6天前
|
移动开发 数据库 Android开发
构建高效Android应用:Kotlin与协程的完美结合
【5月更文挑战第7天】 在移动开发领域,性能优化和资源管理始终是核心议题。随着Kotlin语言的普及,其提供的协程特性为Android开发者带来了异步编程的新范式。本文将深入探讨如何通过Kotlin协程来优化Android应用的性能,实现流畅的用户体验,并减少资源消耗。我们将分析协程的核心概念,并通过实际案例演示其在Android开发中的应用场景和优势。