发布于 

FRIDA 高级 API:Frida Hook Java(1&2)、Frida hook native

Java.use() 进阶用法

在 FRIDA 高级逆向中,Java.use() 是 Hook Java 层代码的核心 API。除了常见的 Hook 普通类和方法外,它还支持处理枚举类、内部类和匿名类等复杂 Java 结构。

Hook 枚举类

枚举类在 Android 开发中广泛使用,例如定义状态码、类型标识等。使用 Java.use() Hook 枚举类时,需要注意枚举的静态实例是通过类初始化块创建的。

Java.perform(function () {
    // 获取枚举类
    var EnumClass = Java.use("com.example.app.StatusEnum");

    // 遍历枚举值
    var values = EnumClass.$new().values();
    for (var i = 0; i < values.length; i++) {
        console.log("枚举值: " + values[i].name() + " = " + values[i].ordinal());
    }

    // Hook 枚举的静态方法
    EnumClass.valueOf.implementation = function (name) {
        console.log("valueOf 被调用: " + name);
        var result = this.valueOf(name);
        console.log("返回值: " + result.name());
        return result;
    };
});

Hook 内部类

Java 内部类在编译后会生成独立的 class 文件,命名规则为 外部类$内部类。在 FRIDA 中直接使用 $ 符号引用即可。

Java.perform(function () {
    // Hook 公有内部类
    var InnerClass = Java.use("com.example.app.OuterClass$InnerClass");

    InnerClass.secretMethod.implementation = function (arg) {
        console.log("内部类方法被调用,参数: " + arg);
        var result = this.secretMethod(arg);
        console.log("内部类方法返回: " + result);
        return result;
    };

    // Hook 私有内部类(同理,Frida 可以访问私有类)
    var PrivateInner = Java.use("com.example.app.OuterClass$PrivateInner");

    PrivateInner.getSecretData.implementation = function () {
        var result = this.getSecretData();
        console.log("私有内部类返回的密钥: " + result);
        return result;
    };
});

Hook 匿名类

匿名内部类没有显式名称,编译器会自动编号,命名格式为 外部类$数字。Hook 匿名类时需要先枚举类名来定位。

Java.perform(function () {
    // 通过枚举找到匿名类
    Java.enumerateLoadedClasses({
        onMatch: function (className) {
            if (className.indexOf("com.example.app.MainActivity$") !== -1) {
                console.log("找到内部/匿名类: " + className);
            }
        },
        onComplete: function () {
            console.log("枚举完成");
        }
    });

    // 直接 Hook 已知编号的匿名类
    var AnonymousClass = Java.use("com.example.app.MainActivity$1");

    AnonymousClass.onClick.implementation = function (v) {
        console.log("匿名类的 onClick 被触发");
        this.onClick(v);
    };
});

Java.perform 的线程安全与回调队列

线程安全问题

Java.perform() 内部的代码运行在 Frida 创建的特殊线程上,而许多回调(如 Interceptor 的 Native Hook 回调)运行在目标线程上。直接在这些回调中调用 Java API 会导致线程不安全错误。

// 错误示例:在 Native 回调中直接调用 Java API
Interceptor.attach(baseAddr.add(0x1234), {
    onEnter: function (args) {
        // 这里运行在 Native 线程,不能直接使用 Java.use()
        var clazz = Java.use("com.example.app.Util"); // 可能崩溃!
    }
});

// 正确示例:通过 Java.scheduleOnMainThread() 调度
Interceptor.attach(baseAddr.add(0x1234), {
    onEnter: function (args) {
        var arg0 = args[0].toInt32();
        // 将 Java 操作调度到主线程
        Java.scheduleOnMainThread(function () {
            Java.perform(function () {
                var Util = Java.use("com.example.app.Util");
                var result = Util.process(arg0);
                console.log("Java 处理结果: " + result);
            });
        });
    }
});

回调队列机制

Java.perform() 还有一个重要特性——回调队列。如果在 Java 虚拟机尚未完全初始化时调用 Java.perform(),它不会立即执行回调函数,而是将回调加入队列,等待 VM 就绪后依次执行。

// 即使 VM 还没准备好,Java.perform 也会自动排队
function hookJavaClass() {
    Java.perform(function () {
        console.log("Hook Java 类执行中...");
        var Target = Java.use("com.example.app.Target");
        Target.verify.implementation = function (token) {
            console.log("拦截到 token 验证: " + token);
            return true; // 绕过验证
        };
    });
}

// 在脚本加载时调用,无论 VM 是否就绪都能正常工作
hookJavaClass();

Interceptor.attach() Hook Native 函数

Interceptor 是 Frida 中最强大的 Native Hook 工具,它可以在函数的入口和出口处插入自定义代码,实现对 Native 函数参数的读取、修改以及对返回值的篡改。

基本用法

var baseAddr = Module.findBaseAddress("libnative.so");
if (baseAddr) {
    var funcAddr = baseAddr.add(0x1A2B); // 目标函数偏移

    Interceptor.attach(funcAddr, {
        onEnter: function (args) {
            // 函数入口,读取参数
            console.log("[+] 函数被调用");
            console.log("    arg0 (int): " + args[0].toInt32());
            console.log("    arg1 (ptr): " + args[1]);
            console.log("    arg2 (str): " + args[2].readUtf8String());
        },
        onLeave: function (retval) {
            // 函数出口,读取/修改返回值
            console.log("    返回值: " + retval);
            retval.replace(0); // 修改返回值为 0
        }
    });
}

onEnter 参数详解

onEnter 回调接收一个 args 数组,对应函数的参数列表。在 ARM64 下,前 8 个参数通过寄存器 x0-x7 传递,后续参数通过栈传递。在 ARM32 下,前 4 个参数通过 r0-r3 传递。

onLeave 参数详解

onLeave 回调接收一个 retval 对象,代表函数的返回值。对于整数返回值使用 retval.toInt32()retval.toUInt32() 读取,对于指针返回值使用 retval.readPointer() 读取。

读取和修改寄存器参数

除了通过 args 数组访问参数外,Frida 还提供了通过 this.context 直接读写 CPU 寄存器的能力,这在分析未识别参数类型的函数时非常有用。

ARM64 寄存器操作

Interceptor.attach(funcAddr, {
    onEnter: function (args) {
        var ctx = this.context;
        // 读取 ARM64 参数寄存器
        console.log("x0 = " + ctx.x0);        // 第1个参数
        console.log("x1 = " + ctx.x1);        // 第2个参数
        console.log("x2 = " + ctx.x2);        // 第3个参数
        console.log("x3 = " + ctx.x3);        // 第4个参数
        console.log("x4 = " + ctx.x4);        // 第5个参数
        console.log("x5 = " + ctx.x5);        // 第6个参数
        console.log("x6 = " + ctx.x6);        // 第7个参数
        console.log("x7 = " + ctx.x7);        // 第8个参数
        console.log("sp = " + ctx.sp);         // 栈指针
        console.log("pc = " + ctx.pc);         // 程序计数器

        // 修改寄存器值
        ctx.x0 = ptr(0xDEADBEEF);             // 修改第1个参数
    },
    onLeave: function (retval) {
        var ctx = this.context;
        // x0 在返回时存放返回值
        console.log("返回值 (x0) = " + ctx.x0);
    }
});

ARM32 寄存器操作

Interceptor.attach(funcAddr, {
    onEnter: function (args) {
        var ctx = this.context;
        // 读取 ARM32 参数寄存器
        console.log("r0 = " + ctx.r0);        // 第1个参数
        console.log("r1 = " + ctx.r1);        // 第2个参数
        console.log("r2 = " + ctx.r2);        // 第3个参数
        console.log("r3 = " + ctx.r3);        // 第4个参数
        console.log("sp = " + ctx.sp);         // 栈指针
        console.log("lr = " + ctx.lr);         // 返回地址
        console.log("pc = " + ctx.pc);         // 程序计数器

        // 修改参数
        ctx.r0 = ptr(0x12345678);
    },
    onLeave: function (retval) {
        // ARM32 的返回值也在 r0 中
        console.log("返回值 (r0) = " + this.context.r0);
    }
});

修改返回值

修改返回值是 Hook 中最常见的操作之一,用于绕过校验逻辑、改变程序行为等。

// 场景:绕过签名校验
Interceptor.attach(checkSignatureAddr, {
    onEnter: function (args) {
        console.log("[签名校验] 被调用");
    },
    onLeave: function (retval) {
        // 0 表示校验通过,1 表示失败
        console.log("[签名校验] 原始返回值: " + retval.toInt32());
        retval.replace(0); // 强制返回"通过"
        console.log("[签名校验] 已修改为: 0");
    }
});

// 场景:修改布尔返回值
Interceptor.attach(isVipAddr, {
    onLeave: function (retval) {
        console.log("[VIP 检查] 原始值: " + retval.toInt32());
        retval.replace(1); // 0=false, 1=true
        console.log("[VIP 检查] 已修改为: 1 (VIP)");
    }
});

替换整个 Native 函数实现

除了在函数前后插入代码外,Frida 还支持使用 Interceptor.replace() 完全替换一个函数的实现。这在需要对函数进行大范围修改时非常有用。

// 使用 NativeFunction 定义新的函数实现
var newImpl = new NativeFunction(function (param1, param2) {
    console.log("[替换函数] param1=" + param1 + ", param2=" + param2);

    // 完全自定义的逻辑
    var result = param1 * param2 + 0x1337;
    console.log("[替换函数] 计算结果: " + result);
    return result;
}, 'int', ['int', 'int']);

// 替换原函数
var targetAddr = baseAddr.add(0x2A3B);
Interceptor.replace(targetAddr, newImpl);

// 也可以替换为另一个已有函数的地址(函数指针转发)
// Interceptor.replace(targetAddr, anotherFuncAddr);

replace 与 attach 的区别

特性 Interceptor.attach Interceptor.replace
原函数 保留,可在前后插入代码 完全替换,原函数不再执行
调用原函数 直接调用 this.func() 无法调用原函数
适用场景 监控参数、修改返回值 重写整个函数逻辑
性能影响 较小 较小

Hook 导出函数和非导出函数

Hook 导出函数

导出函数可以通过函数名直接定位,使用 Module.getExportByName() 获取地址。

// 通过函数名获取导出函数地址
var openAddr = Module.getExportByName("libc.so", "open");
var strcmpAddr = Module.getExportByName("libnative.so", "check_password");

console.log("[*] open 地址: " + openAddr);
console.log("[*] check_password 地址: " + strcmpAddr);

// Hook 导出函数
Interceptor.attach(strcmpAddr, {
    onEnter: function (args) {
        console.log("[strcmp] s1=" + args[0].readUtf8String() +
                     " s2=" + args[1].readUtf8String());
    },
    onLeave: function (retval) {
        console.log("[strcmp] 返回值: " + retval.toInt32());
    }
});

Hook 非导出函数

非导出函数在 ELF 的动态符号表中不可见,需要通过 IDA Pro 分析确定偏移量后,通过基地址+偏移的方式定位。

// 第一步:获取模块基地址
var mod = Process.findModuleByName("libnative.so");
if (mod) {
    var base = mod.base;
    console.log("[*] libnative.so 基地址: " + base);

    // 第二步:使用 IDA 分析出的偏移量定位非导出函数
    // 假设在 IDA 中看到函数地址为 0x1A4C(相对 SO 文件偏移)
    var decryptFunc = base.add(0x1A4C);
    console.log("[*] 解密函数实际地址: " + decryptFunc);

    // 第三步:Hook 非导出函数
    Interceptor.attach(decryptFunc, {
        onEnter: function (args) {
            var ctx = this.context;
            console.log("[解密函数] 被调用");
            console.log("    输入指针: " + args[0]);
            console.log("    输入长度: " + ctx.x1);

            // 读取输入数据
            var len = ctx.x1.toInt32();
            if (len > 0 && len < 1024) {
                console.log("    输入数据(hex): " +
                    hexdump(args[0], { length: len }));
            }
        },
        onLeave: function (retval) {
            console.log("[解密函数] 返回: " + retval.toInt32());
        }
    });
}

处理 ASLR(地址随机化)

Android 系统启用了 ASLR,每次运行时模块基地址会变化。因此,非导出函数的地址必须动态计算,不能硬编码绝对地址。

// 安全的 Hook 模式:每次运行时动态计算
function hookNonExportedFunc(moduleName, offset) {
    var mod = Process.findModuleByName(moduleName);
    if (!mod) {
        console.log("[-] 模块未加载: " + moduleName);
        return;
    }

    var addr = mod.base.add(offset);
    console.log("[+] Hook 目标: " + moduleName +
                " + 0x" + offset.toString(16) +
                " = " + addr);

    Interceptor.attach(addr, {
        onEnter: function (args) {
            console.log("[+] 函数调用 @ " + addr);
        }
    });
}

// 使用示例
hookNonExportedFunc("libnative.so", 0x1A4C);
hookNonExportedFunc("libnative.so", 0x2B8D);
hookNonExportedFunc("libsecurity.so", 0x4567);

总结

本文介绍了 FRIDA 高级逆向中最核心的两组 API:Java Hook 和 Native Hook。Java.use() 配合 Java.perform() 可以深入 Hook Java 层的各种复杂类结构,而 Interceptor.attach() 则提供了对 Native 函数的细粒度控制——从读写寄存器参数到修改返回值、替换函数实现。掌握这些 API 是进行高级逆向分析的必备技能,后续文章将在此基础上介绍 Stalker 代码追踪和 OLLVM 混淆分析等进阶技术。