Frida 的 Hook 能力分三个层次:Java 层方法拦截 → Native 函数拦截 → 内存级读写。从上往下难度递增、能力递增。本文按层次给出可运行的最小脚本与排错思路,帮你把"注入成功"到"拿到数据"的距离缩到最短。
环境怎么搭?
# PC 端(以 Python 环境为例)
pip install frida-tools
frida --version
# Android 设备端:下载对应架构的 frida-server 推送并运行
adb push frida-server-android-arm64 /data/local/tmp/fs
adb shell "chmod +x /data/local/tmp/fs && /data/local/tmp/fs &"
# 验证连接
frida-ps -U
测试目标建议使用 OWASP UnCrackable 或自写的 demo app——不要拿线上商业产品练手。
层次一:Java 层 Hook
目标在 jadx 里能看到的方法,用 Java.perform 拦截:
// hook_java.js — 拦截某类的静态方法并打印参数与返回值
Java.perform(() => {
const Cls = Java.use('com.demo.app.CryptoUtil');
Cls.sign.overload('java.lang.String').implementation = function (input) {
const ret = this.sign(input);
console.log('[sign] in :', input);
console.log('[sign] out:', ret);
return ret;
};
});
frida -U -l hook_java.js -f com.demo.app # spawn 模式,覆盖 attach 抓不到早期调用的场景
常见坑:方法重载时必须用 overload() 指定签名;类名被混淆就用脚本枚举当前内存里的类。
层次二:Native 层 Hook
目标在 so 库里,先用偏移量定位,再 Interceptor.attach:
// hook_native.js — 拦截 so 中的未导出函数(偏移 0x1A2B3C)
const base = Module.getBaseAddress('libnative.so');
Interceptor.attach(base.add(0x1A2B3C), {
onEnter(args) {
// 按函数约定读参数:指针按字节读,整型按位读
console.log('[n] arg0 ptr:', args[0].readCString());
},
onLeave(ret) {
console.log('[n] ret:', ret.toInt32());
}
});
定位偏移的方法:IDA 打开 so → 找到目标函数 → 记录函数起始地址减去模块基址。导出函数可以直接 Module.getExportByName(null, 'symbol')。
层次三:内存级操作
前两层都不奏效时(函数被内联、逻辑写在宏里),退到内存层:Memory.scan 搜特征字节、Memory.readByteArray 直接读数据区、Memory.protect 改页属性。这是"最后手段",脚本可移植性差,每次版本更新都要重新定位。
调试心法
三个技巧贯穿三层次:
- 差分观察:同一操作重复两次,对比 dump 结果,找"变的"和"不变的"字段;
- 从调用栈反推:
console.log(Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()))打印 Java 调用栈,定位是谁在调目标函数; - 最小化注入:一次只 Hook 一个函数,确认生效再加下一个——批量 Hook 出错时几乎无法定位。
Frida 在完整分析流程中的位置,见 逆向方法论五步;拿到关键数据后如何还原算法,将在会员篇 签名算法分析实战 中以自建靶场为例完整演示。