本文系统讲解 Frida 框架在 Android 逆向中的应用,涵盖环境搭建、Java 层 Hook、Native 层 Hook 以及内存 Patch,配合 Frida-Labs 靶场实战案例,手把手带你从零上手。
一、什么是 Hook?
Hook 是一种在程序运行时动态修改或拦截函数调用、参数或返回值的技术。在 Android 安全研究、逆向分析以及自动化测试中,Hook 技术扮演着至关重要的角色。
目前 Android 生态中主流的 Hook 框架主要有两种,而本文将重点介绍逆向中最常用的 Frida 框架。
二、Frida 环境搭建
Frida 采用 C/S 架构(客户端/服务端),安装分为 PC 端(客户端)和手机端(服务端)两部分。
2.1 安装 PC 端(客户端)
确保已安装 Python 环境,使用 pip 安装:
bash
pip install frida frida-tools
推荐指定版本安装(PC 端和手机端版本保持一致,以保证稳定性):
bash
pip install frida==16.7.14 frida-tools
2.2 安装手机端(服务端)
第一步:确认设备 CPU 架构
bash
adb shell getprop ro.product.cpu.abi
第二步:下载对应版本的 frida-server
前往 Frida Releases 下载。
📌 下载原则:版本号与 PC 端一致,架构与手机匹配。
例如:手机为
arm64-v8a,PC 端 Frida 版本为16.7.14,则下载frida-server-16.7.14-android-arm64.xz。
第三步:部署到手机
bash
# 1. 推送到手机临时目录
adb push frida-server-16.7.14-android-arm64 /data/local/tmp/
# 2. 进入手机 Shell 并提权
adb shell
su
# 3. 赋予执行权限并后台运行
chmod +x /data/local/tmp/frida-server-16.7.14-android-arm64
/data/local/tmp/frida-server-16.7.14-android-arm64 &
💡 建议重命名 frida-server 文件(如
fs),既方便输入,也能规避部分 App 对 "frida-server" 文件名的字符串检测。
2.3 建立端口转发
方式一:默认端口(推荐)
Frida 默认通信端口为 27042:
bash
adb forward tcp:27042 tcp:27042
方式二:自定义端口(规避检测)
手机端启动时指定端口:
bash
/data/local/tmp/fs -l 0.0.0.0:8888 &
电脑端转发:
bash
adb forward tcp:8888 tcp:8888
客户端连接时通过 -H 参数指定地址:
bash
frida -H 127.0.0.1:8888 -f com.example.app
三、Frida 基本命令与注入模式
3.1 两种注入模式
| 模式 | 关键词 | 原理 | 适用场景 |
|---|---|---|---|
| Spawn | -f |
重启 App,启动前注入脚本 | Root 检测、启动阶段逻辑(如 onCreate) |
| Attach | -n / -p |
附加到已运行的进程 | 运行中分析、规避启动注入检测 |
3.2 查找目标应用
bash
# 列出所有已安装的应用(获取包名,用于 Spawn)
frida-ps -Uai
# 列出正在运行的进程并过滤(用于 Attach)
frida-ps -U | grep "关键词" # Linux / macOS
frida-ps -U | findstr "关键词" # Windows
四、Java 层 Hook 核心 API
4.1 入门三件套
javascript
Java.perform(function () {
// 1. Java.use:获取目标类的引用
var MainActivity = Java.use("com.example.app.MainActivity");
// 2. implementation:替换目标方法的实现
MainActivity.check.implementation = function (i, i2) {
console.log("原始参数:i=" + i + ", i2=" + i2);
// 3. 调用原方法(推荐方括号写法,兼容混淆名)
return this["check"](0, 4);
};
});
Java.perform(fn):入口函数,确保当前线程已附加到 Java VM。Java.use(className):动态获取类引用(类似反射)。implementation:替换方法的实际实现。
4.2 延迟执行技巧
某些场景下类尚未加载,直接运行会报 ClassNotFoundException,可使用延迟执行:
javascript
// 方式一:setImmediate(推荐,注意不要放在第一行)
setImmediate(function () {
Java.perform(function () { /* Hook 逻辑 */ });
});
// 方式二:setTimeout(适用于启动初期类未加载的情况)
setTimeout(function () {
Java.perform(function () { /* Hook 逻辑 */ });
}, 1000);
五、Java 层实战案例
5.1 Hook 普通方法 --- 篡改参数(Frida-Labs 0x1)
目标 :MainActivity.check(int i, int i2),满足 (i * 2) + 4 == i2 即可通过。
策略:拦截方法,强制修改参数为满足条件的值。
javascript
Java.perform(function () {
var MainActivity = Java.use("com.ad2001.frida0x1.MainActivity");
MainActivity.check.implementation = function (i, i2) {
console.log("[*] 原始参数: i=" + i + ", i2=" + i2);
// 强制修改为 i=0, i2=4,满足 (0*2)+4==4
return this["check"](0, 4);
};
});
5.2 主动调用静态方法(Frida-Labs 0x2)
静态方法属于类本身,无需实例即可调用:
javascript
Java.perform(function () {
var TargetClass = Java.use("com.ad2001.frida0x2.TargetClass");
// 直接通过类引用调用静态方法
var result = TargetClass.staticMethod(参数);
console.log("[*] 返回值: " + result);
});
5.3 修改静态字段值(Frida-Labs 0x3)
目标 :Checker.code 默认值为 0,需要修改为 512。
javascript
Java.perform(function () {
var Checker = Java.use("com.ad2001.frida0x3.Checker");
// 注意:必须通过 .value 读写字段值
console.log("[*] 修改前: " + Checker.code.value);
Checker.code.value = 512;
console.log("[*] 修改后: " + Checker.code.value);
});
⚠️ 直接打印
Checker.code得到的是 Frida 的字段包装对象,而非具体的值,必须使用.value。
5.4 手动创建实例并调用方法(Frida-Labs 0x4)
当目标方法是非静态的,且 App 当前未创建该类实例时:
javascript
Java.perform(function () {
var Check = Java.use("com.ad2001.frida0x4.Check");
// 使用 $new() 创建新实例
var instance = Check.$new();
var flag = instance.get_flag(1337);
console.log("[*] Flag: " + flag);
});
5.5 内存堆搜索已有实例(Frida-Labs 0x5)
对于系统管理的类(如 Activity),不能自行 $new(),需要用 Java.choose 从堆中搜索:
javascript
setTimeout(function () {
Java.perform(function () {
Java.choose("com.ad2001.frida0x5.MainActivity", {
onMatch: function (instance) {
console.log("[*] 找到实例: " + instance);
// 调用实例方法
instance.targetMethod();
},
onComplete: function () {
console.log("[*] 搜索完成");
}
});
});
}, 1000); // 延迟确保 Activity 已创建
5.6 构造复杂对象作为参数(Frida-Labs 0x6)
综合运用内存实例查找 + 对象实例化:
javascript
setTimeout(function () {
Java.perform(function () {
// 1. 搜索 MainActivity 实例
Java.choose("com.ad2001.frida0x6.MainActivity", {
onMatch: function (instance) {
// 2. 创建并配置参数对象
var Checker = Java.use("com.ad2001.frida0x6.Checker");
var checker = Checker.$new();
// 配置属性...
// 3. 主动调用
instance.targetMethod(checker);
},
onComplete: function () {}
});
});
}, 1000);
5.7 Hook 构造方法(Frida-Labs 0x7)
Frida 中构造方法映射为 $init:
javascript
Java.perform(function () {
var Checker = Java.use("com.ad2001.frida0x7.Checker");
// Hook 构造方法
Checker.$init.implementation = function (a, b) {
console.log("[*] 原始构造参数: a=" + a + ", b=" + b);
// 强制修改为满足条件的值
this.$init(999, 999);
};
});
📌 如果存在多个重载构造方法,需要使用
.overload()指定签名:
javascriptChecker.$init.overload('int', 'int').implementation = function (a, b) { ... };
5.8 处理方法重载(Frida-Labs Demo)
Java 中同名方法参数不同即为重载,Hook 时必须用 .overload() 明确签名:
javascript
Java.perform(function () {
var cls = Java.use("com.example.Challenge4Activity");
// Hook 接收两个 int 参数的版本
cls.check.overload('int', 'int').implementation = function (a, b) {
console.log("[*] check(int, int) called");
return this.check(a, b);
};
// Hook 接收一个 String 参数的版本
cls.check.overload('java.lang.String').implementation = function (s) {
console.log("[*] check(String) called");
return this.check(s);
};
});
签名书写规则 :基本类型用小写(int、boolean),引用类型用全限定名(java.lang.String)。
💡 实用技巧 :不确定签名?故意写错
.overload(),Frida 报错时会列出所有可用签名,直接复制即可!
六、Native 层 Hook
Native 层 Hook 针对 C/C++ 代码(.so 动态链接库),操作的是内存地址和寄存器。
6.1 核心 API ------ Interceptor.attach
javascript
Interceptor.attach(targetAddress, {
onEnter: function (args) {
// 函数执行前:读取/修改参数
console.log("arg0: " + args[0]);
console.log("arg1 字符串: " + args[1].readUtf8String());
},
onLeave: function (retval) {
// 函数执行后:读取/修改返回值
console.log("返回值: " + retval);
retval.replace(1337); // 修改返回值
}
});
6.2 辅助侦察脚本
javascript
// 查看目标 SO 是否已加载及其基址
var module = Process.findModuleByName("libtarget.so");
if (module) {
console.log("基址: " + module.base);
console.log("大小: " + module.size);
}
// 枚举导出函数
Module.enumerateExports("libtarget.so", {
onMatch: function (exp) {
console.log(exp.type + " | " + exp.name + " | " + exp.address);
},
onComplete: function () {}
});
6.3 Hook 有符号 Native 函数(Frida-Labs 0x8)
场景 :Hook libc.so 中的 strcmp 来截获 Flag。
javascript
// 监听目标 SO 加载时机
Java.perform(function () {
var Runtime = Java.use("java.lang.Runtime");
Runtime.loadLibrary0.implementation = function (classLoader, libName) {
var result = this.loadLibrary0(classLoader, libName);
if (libName === "frida0x8") {
console.log("[*] 目标 SO 已加载,开始 Hook strcmp");
hookStrcmp();
}
return result;
};
});
function hookStrcmp() {
var strcmpAddr = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmpAddr, {
onEnter: function (args) {
var arg0 = args[0].readUtf8String();
var arg1 = args[1].readUtf8String();
if (arg0 && arg1) {
console.log("[strcmp] " + arg0 + " vs " + arg1);
}
}
});
}
6.4 修改 Native 函数返回值(Frida-Labs 0x9)
场景:Native 函数固定返回 1,但 Java 层要求返回 1337。
javascript
var checkFlagAddr = Module.findExportByName("liba0x9.so",
"Java_com_ad2001_a0x9_MainActivity_check_1flag");
Interceptor.attach(checkFlagAddr, {
onLeave: function (retval) {
console.log("[*] 原始返回值: " + retval);
retval.replace(1337);
console.log("[*] 修改后返回值: 1337");
}
});
6.5 主动调用 Native 函数 ------ NativeFunction(Frida-Labs 0xA)
场景 :SO 中存在隐藏函数 get_flag,App 从未调用。
javascript
// 方式一:通过符号名(有符号)
var getFlagAddr = Module.findExportByName("libfrida0xa.so", "_Z8get_flagii");
var getFlag = new NativeFunction(getFlagAddr, 'void', ['int', 'int']);
getFlag(参数1, 参数2);
// 方式二:通过基址 + 偏移(无符号/符号被 Strip)
var base = Module.findBaseAddress("libfrida0xa.so");
var offset = 0x1DD60;
var targetAddr = base.add(offset);
// 32位 ARM Thumb 模式需要 +1
// var targetAddr = base.add(offset + 1);
var getFlag = new NativeFunction(targetAddr, 'void', ['int', 'int']);
getFlag(参数1, 参数2);
NativeFunction 支持的类型 :void、int、uint、long、float、double、pointer、bool 等。
⚠️ Thumb 模式注意 :在 32 位 ARM 下,如果目标函数是 Thumb 指令集,地址最低位必须为 1,即需要
base.add(offset + 1)。
七、内存 Patch --- 修改机器码(Frida-Labs 0xB)
当 Hook 无法满足需求时(如需要绕过复杂的跳转逻辑),可以直接修改内存中的汇编指令。
场景 :Native 函数中存在 B.NE(条件跳转)指令,跳过了 Flag 生成逻辑。
策略 :将 B.NE 替换为 NOP(空操作指令),让代码"直落"执行。
javascript
var base = Module.findBaseAddress("libtarget.so");
var patchAddr = base.add(0x15248); // B.NE 指令的偏移
// 1. 修改内存权限为 RWX(代码段默认只读)
Memory.protect(patchAddr, 4, 'rwx');
// 2. 使用 Arm64Writer 写入 NOP 指令
var writer = new Arm64Writer(patchAddr);
writer.putNop();
writer.flush();
console.log("[*] Patch 完成:B.NE -> NOP");
📌 代码段(
.text)默认是只读(RX)的,必须先用Memory.protect改为可读可写可执行(RWX)才能写入。
八、速查总结表
| 操作 | 核心 API | 关键点 |
|---|---|---|
| Hook Java 方法 | Java.use + .implementation |
使用方括号写法兼容混淆名 |
| 修改静态字段 | Java.use + .value |
必须用 .value 读写 |
| 创建新实例 | $new() |
不适用于 Activity 等系统类 |
| 搜索堆中实例 | Java.choose |
注意执行时机,建议延迟 |
| Hook 构造方法 | $init |
重载时用 .overload() |
| 处理重载 | .overload('type1', 'type2') |
写错签名可获取所有签名提示 |
| Hook Native 函数 | Interceptor.attach |
args[n] 是指针,需 Memory 读写 |
| 修改返回值 | retval.replace(value) |
在 onLeave 中操作 |
| 主动调用 Native | NativeFunction |
注意 Thumb +1 问题 |
| 内存 Patch | Arm64Writer / X86Writer |
需先 Memory.protect 改权限 |
写在最后
Frida 作为最灵活的动态 Hook 框架,几乎能覆盖 Android 逆向分析中的所有场景。本文从环境搭建到 Java 层 Hook、Native 层 Hook、再到内存 Patch,系统地梳理了 Frida 的核心用法。