安卓破解逆向入门教程 | 轻松学习如何逆向工程并破解安卓应用的方法与技巧插图

一、课程目标

在这个教程中,我们将学习如何使用逆向工程技巧和方法,以及借助 Frida-Native-Hook 和 IDA 脚本来破解安卓应用程序。

二、工具

在本教程中,我们将使用以下工具:

  1. 教程 Demo(更新)
  2. jadx-gui
  3. VS Code
安卓破解逆向入门教程 | 轻松学习如何逆向工程并破解安卓应用的方法与技巧插图1

三、课程内容

1. 基础概念

1.1 Process、Module、Memory 基础

  • Process
    • Process 对象代表当前被 Hook 的进程,能获取进程的信息,枚举模块,枚举范围等。
    API含义Process.id返回附加目标进程的 PIDProcess.isDebuggerAttached()检测当前是否对目标程序已经附加Process.enumerateModules()枚举当前加载的模块,返回模块对象的数组Process.enumerateThreads()枚举当前所有的线程,返回包含 id、state、context 等属性的对象数组
  • Module
    • Module 对象代表一个加载到进程的模块(例如,在 Windows 上的 DLL,或在 Linux/Android 上的 .so 文件),能查询模块的信息,如模块的基址、名称、导入 / 导出的函数等。
    API含义Module.load()加载指定 so 文件,返回一个 Module 对象Module.enumerateImports()枚举所有 Import 库函数,返回 Module 数组对象Module.enumerateExports()枚举所有 Export 库函数,返回 Module 数组对象Module.enumerateSymbols()枚举所有 Symbol 库函数,返回 Module 数组对象Module.findExportByName(name)寻找指定 so 中 export 库中的函数地址Module.getExportByName(name)获取指定 so 中 export 库中的函数地址Module.findBaseAddress(name)返回 so 的基地址
  • Memory
    • Memory 是一个工具对象,提供直接读取和修改进程内存的功能,能够读取特定地址的值、写入数据、分配内存等。
    方法功能Memory.copy()复制内存Memory.scan()搜索内存中特定模式的数据Memory.scanSync()同上,但返回多个匹配的数据Memory.alloc()在目标进程的堆上申请指定大小的内存,返回一个 NativePointerMemory.writeByteArray()将字节数组写入一个指定内存Memory.readByteArray读取内存

2. 枚举导入导出表

  • 导出表(Export Table):列出了库中可以被其他程序或库访问的所有公开函数和符号的名称。
  • 导入表(Import Table):列出了库需要从其他库中调用的函数和符号的名称。
function hookTest1() {
    Java.perform(function() {
        // 打印导入表
        var imports = Module.enumerateImports("lib52pojie.so");
        for (var i = 0; i < imports.length; i++) {
            if (imports[i].name == "vip") {
                console.log(JSON.stringify(imports[i])); // 通过 JSON.stringify 打印 object 数据
                console.log(imports[i].address);
            }
        }
        // 打印导出表
        var exports = Module.enumerateExports("lib52pojie.so");
        for (var i = 0; i < exports.length; i++) {
            console.log(JSON.stringify(exports[i]));
        }
    });
}

3. Native 函数的基础 Hook 打印

在本节中,我们将学习如何使用 Frida 对 Native 函数进行基础的 Hook,包括整数型、布尔值类型、char 类型和字符串类型。

3.1 整数型、布尔值类型、char 类型

function hookTest2() {
    Java.perform(function() {
        // 根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
        console.log(helloAddr);
        if (helloAddr != null) {
            Interceptor.attach(helloAddr, {
                // onEnter 里可以打印和修改参数
                onEnter: function(args) { // args 传入参数
                    console.log(args[0]); // 打印第一个参数的值
                    console.log(this.context.x1); // 打印寄存器内容
                    console.log(args[1].toInt32()); // toInt32()转十进制
                    console.log(args[2].readCString()); // 读取字符串 char 类型
                    console.log(hexdump(args[2])); // 内存 dump
                },
                // onLeave 里可以打印和修改返回值
                onLeave: function(retval) { // retval 返回值
                    console.log(retval);
                    console.log("retval", retval.toInt32());
                }
            })
        }
    })
}

3.2 字符串类型

function hookTest2() {
    Java.perform(function() {
        // 根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
        if (helloAddr != null) {
            Interceptor.attach(helloAddr, {
                // onEnter 里可以打印和修改参数
                onEnter: function(args) { // args 传入参数
                    // 方法一
                    var jString = Java.cast(args[2], Java.use('java.lang.String'));
                    console.log("参数:", jString.toString());
                    // 方法二
                    var JNIEnv = Java.vm.getEnv();
                    var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
                    console.log("参数:", originalStrPtr);
                },
                // onLeave 里可以打印和修改返回值
                onLeave: function(retval) { // retval 返回值
                    var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
                    console.log("返回值:", returnedJString.toString());
                }
            })
        }
    })
}

4. Native 函数的基础 Hook 修改

在本节中,我们将学习如何修改 Native 函数的参数和返回值,包括整数型和字符串类型的修改。

4.1 整数型修改

function hookTest3() {
    Java.perform(function() {
        // 根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
        console.log(helloAddr);
        if (helloAddr != null) {
            Interceptor.attach(helloAddr, {
                onEnter: function(args) { // args 参数
                    args[0] = ptr(1000); // 第一个参数修改为整数 1000,先转为指针再赋值
                    console.log(args[0]);
                },
                onLeave: function(retval) { // retval 返回值
                    retval.replace(20000); // 返回值修改
                    console.log("retval", retval.toInt32());
                }
            })
        }
    })
}

4.2 字符串类型修改

function hookTest2() {
    Java.perform(function() {
        // 根据导出函数名打印地址
        var helloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
        if (helloAddr != null) {
            Interceptor.attach(helloAddr, {
                // onEnter 里可以打印和修改参数
                onEnter: function(args) { // args 传入参数
                    var JNIEnv = Java.vm.getEnv();
                    var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
                    console.log("参数:", originalStrPtr);
                    var modifiedContent = "至尊";
                    var newJString = JNIEnv.newStringUtf(modifiedContent);
                    args[2] = newJString;
                },
                // onLeave 里可以打印和修改返回值
                onLeave: function(retval) { // retval 返回值
                    var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
                    console.log("返回值:", returnedJString.toString());
                    var JNIEnv = Java.vm.getEnv();
                    var modifiedContent = "无敌";
                    var newJString = JNIEnv.newStringUtf(modifiedContent);
                    retval.replace(newJString);
                }
            })
        }
    })
}

5. SO 基址的获取方式

获取 SO 基址的几种方式:

var moduleAddr1 = Process.findModuleByName("lib52pojie.so").base; var moduleAddr2 = Process.getModuleByName("lib52pojie.so").base; var moduleAddr3 = Module.findBaseAddress("lib52pojie.so");

6. Hook 未导出函数函数地址计算

在本节中,我们将学习如何 Hook 未导出函数以及如何计算函数地址。

function hookTest6() {
    Java.perform(function() {
        // 根据导出函数名打印基址
        var soAddr = Module.findBaseAddress("lib52pojie.so");
        console.log(soAddr);
        var funcaddr = soAddr.add(0x1071C);  
        console.log(funcaddr);
        if (funcaddr != null) {
            Interceptor.attach(funcaddr, {
                onEnter: function(args) {
                    // args 参数
                },
                onLeave: function(retval) {
                    console.log(retval.toInt32());
                }
            })
        }
    })
}

函数地址计算

安卓中,通常 32 位的 SO 中使用 thumb 指令,64 位的 SO 中使用 arm 指令。通过 IDA 中的 opcode bytes 来判断,arm 指令为 4 个字节 (Options -> General -> Number of opcode bytes (non-graph) 输入 4)。对于 thumb 指令,函数地址计算方式为:SO 基址 + 函数在 SO 中的偏移 + 1,而 arm 指令的计算方式为:SO 基址 + 函数在 SO 中的偏移。

7. Hook_dlopen

function hook_dlopen() {
    var dlopen = Module.findExportByName(null, "dlopen");
    Interceptor.attach(dlopen, {
        onEnter: function(args) {
            var so_name = args[0].readCString();
            if (so_name.indexOf("lib52pojie.so") >= 0) this.call_hook = true;
        },
        onLeave: function(retval) {
            if (this.call_hook) hookTest2();
        }
    });

    // 高版本 Android 系统使用 android_dlopen_ext
    var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
    Interceptor.attach(android_dlopen_ext, {
        onEnter: function(args) {
            var so_name = args[0].readCString();
            if (so_name.indexOf("lib52pojie.so") >= 0) this.call_hook = true;
        },
        onLeave: function(retval) {
            if (this.call_hook) hookTest2();
        }
    });
}

8. 借助 IDA 脚本实现一键式 hook

安卓破解逆向入门教程 | 轻松学习如何逆向工程并破解安卓应用的方法与技巧插图2

这就是本次安卓破解逆向入门教程的全部内容。希望你能够通过这些技巧和工具,更深入地了解如何逆向工程和破解安卓应用程序。如果你有任何问题或疑问,欢迎留言讨论!