Frida 是一款跨平台的动态二进制插桩工具,凭借 JavaScript 脚本化、轻量灵活、全平台覆盖的特性,成为移动端逆向分析、安全测试领域的标配工具。无论是 Java 层的业务逻辑 Hook,还是 so 原生层的算法分析,Frida 都能提供统一的操作范式。本文将从基础环境搭建出发,系统总结 Java 层与 so 层的常用 Hook 姿势,结合实战案例梳理完整的分析链路,帮助读者快速掌握 Frida 的核心用法。
一、Frida 基础入门
1.1 什么是 Frida
Frida 本质是一个注入式调试框架,它向目标进程注入 frida-gum 引擎,允许开发者通过 JavaScript 脚本直接操作进程内存、Hook 函数、调用原生方法。它支持 Android、iOS、Linux、Windows、macOS 等主流平台,尤其在 Android 逆向场景中,Frida 可以无缝工作在 Java 虚拟机层与 Native 原生层,是分析 APP 逻辑、破解验证、逆向算法的核心工具。
1.2 环境搭建
- 电脑端:安装 Python 环境后,执行 pip install frida frida-tools 即可安装客户端工具与依赖库。
- 手机端:下载与手机架构(arm64/armv7)、电脑端版本完全一致的 frida-server,推送到手机 /data/local/tmp 目录,赋予执行权限并后台运行。
- 环境验证:手机通过 USB 连接电脑并开启调试,执行 frida-ps -U 可列出手机端进程,即为环境搭建成功。
1.3 核心概念与基础脚本结构
Frida 有两种核心运行模式,对应不同分析场景:
- Attach 模式:附加到已经运行的进程,适合调试后台运行的应用、页面交互逻辑,无需重启 APP。
- Spawn 模式:应用启动时直接注入,能 Hook 应用初始化、Application 生命周期、so 加载阶段的逻辑,适合分析启动流程。
基础脚本的通用结构如下:
// Java 层操作必须包裹在 Java.perform 中,确保在 Java 线程执行
Java.perform(function () {
// Java 层 Hook 逻辑写在这里
});
// Native 层操作可直接全局执行
Interceptor.attach(目标函数地址, {
onEnter: function(args) {
// 函数进入时触发,用于读取参数
},
onLeave: function(retval) {
// 函数返回时触发,用于读取/修改返回值
}
});
运行脚本的基础命令:
# Attach 模式:附加到运行中的进程
frida -U 应用包名 -l script.js
# Spawn 模式:冷启动应用并注入脚本
frida -U -f 应用包名 -l script.js
二、Java 层 Hook 常用姿势
Java 层 Hook 是 Frida 最常用的场景,核心围绕 Java.use() 获取类引用,通过重写方法实现拦截。
2.1 基础方法 Hook
最常规的场景是拦截指定类的普通方法,读取参数、修改返回值或直接替换逻辑。
示例:Hook 登录校验方法,打印输入密码与返回结果
Java.perform(function () {
// 通过完整类名获取目标类引用
var LoginManager = Java.use("com.example.app.LoginManager");
// 重写方法实现
LoginManager.verifyPassword.implementation = function (inputPwd) {
// 打印入参
console.log("[Java Hook] 输入密码:" + inputPwd);
// 调用原方法,保留原有逻辑
var result = this.verifyPassword(inputPwd);
console.log("[Java Hook] 校验结果:" + result);
// 也可直接返回 true 绕过校验
// return true;
return result;
};
});
2.2 构造方法与重载方法
- 构造方法 Hook:使用 $init 代表类的构造函数,用于监听对象创建过程。
- 重载方法 Hook:同一个方法名存在多个参数版本时,必须用 overload() 精确指定参数类型。
示例:Hook 重载方法与构造函数
var UserInfo = Java.use("com.example.app.UserInfo");
// Hook 带两个参数的构造方法
UserInfo.$init.overload("java.lang.String", "int").implementation = function (name, age) {
console.log("[构造] 创建用户:" + name + ",年龄:" + age);
this.$init(name, age); // 必须调用原构造,否则对象无法初始化
};
// Hook 重载的 getSign 方法(String + int 参数版本)
UserInfo.getSign.overload("java.lang.String", "int").implementation = function (data, type) {
console.log("[重载] data=" + data + ",type=" + type);
return this.getSign(data, type);
};
2.3 字段读写与内部类 Hook
- 修改成员字段:通过 .value 读写对象的成员变量,静态字段可直接通过类访问。
- 内部类 Hook:Java 内部类在虚拟机中以 外部类$内部类 的形式命名,按此规则引用即可。
示例:修改字段与 Hook 匿名内部类
var MainActivity = Java.use("com.example.app.MainActivity");
// Hook 按钮点击的匿名内部类
var ClickListener = Java.use("com.example.app.MainActivity$1");
ClickListener.onClick.implementation = function (v) {
console.log("[内部类] 登录按钮被点击");
this.onClick(v);
};
// 修改静态字段,强制开启调试模式
MainActivity.IS_DEBUG.value = true;
console.log("[字段] 调试模式状态:" + MainActivity.IS_DEBUG.value);
2.4 主动调用与批量 Hook
Frida 不仅能被动拦截,还可以主动调用类的静态 / 实例方法;批量 Hook 则适合快速定位目标函数。
示例:主动调用静态加密方法
Java.perform(function () {
var SignUtils = Java.use("com.example.app.utils.SignUtils");
// 主动调用 md5 加密方法
var sign = SignUtils.md5Encrypt("test123456");
console.log("[主动调用] 加密结果:" + sign);
});
批量 Hook 目标类的所有方法,用于快速定位关键逻辑:
var targetClass = Java.use("com.example.app.utils.SecurityUtils");
var methods = targetClass.class.getDeclaredMethods();
methods.forEach(function (method) {
var methodName = method.getName();
targetClass[methodName].implementation = function () {
console.log("[批量Hook] 调用方法:" + methodName);
return this[methodName].apply(this, arguments);
};
});
三、Native(so 层)Hook 核心姿势
so 层 Hook 针对 Native 代码,核心依赖 Interceptor 拦截模块与 Module 内存定位模块。
3.1 Native 层 Hook 的两种思路
so 层 Hook 主要覆盖两类场景:
3.2 导出函数 Hook
导出函数是 so 对外暴露的接口,可以直接通过 Module.findExportByName() 定位内存地址。
示例 1:Hook Java Native 方法(so 层视角)
// 确保目标 so 已加载完成
Module.ensureInitialized("libsecurity.so");
// 按 JNI 命名规则查找导出函数
var nativeSign = Module.findExportByName("libsecurity.so",
"Java_com_example_app_utils_SignUtils_nativeSign");
Interceptor.attach(nativeSign, {
onEnter: function (args) {
// JNI 函数前两个参数固定为 JNIEnv* 和 jobject
// args[2] 才对应 Java 层传入的第一个参数
var input = Memory.readCString(args[2]);
console.log("[so Hook] 签名输入:" + input);
},
onLeave: function (retval) {
var result = Memory.readCString(retval);
console.log("[so Hook] 签名结果:" + result);
}
});
示例 2:Hook 系统 libc 的 strcmp 函数
// 常用于绕过字符串比较校验
var strcmp = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp, {
onEnter: function (args) {
var str1 = Memory.readCString(args[0]);
var str2 = Memory.readCString(args[1]);
console.log("[strcmp] 比较:" + str1 + " vs " + str2);
},
onLeave: function (retval) {
// 修改返回值为 0,强制判定字符串相等
retval.replace(0);
}
});
3.3 未导出函数 Hook(基址 + 偏移)
核心算法函数通常不会导出,需要先通过 IDA Pro 等静态分析工具得到函数在 so 文件中的偏移,再加上内存加载基址,计算出运行时地址。
示例:通过基址 + 偏移 Hook 未导出算法函数
// 获取 so 在内存中的加载基址
var baseAddr = Module.findBaseAddress("libnative.so");
if (!baseAddr) {
console.log("[-] libnative.so 尚未加载");
return;
}
// IDA 中分析得到的函数文件偏移(示例值)
var funcOffset = 0x3F2C;
// 计算运行时的函数地址
var targetFunc = baseAddr.add(funcOffset);
Interceptor.attach(targetFunc, {
onEnter: function (args) {
console.log("[未导出函数] 进入目标函数,参数1:" + args[0]);
this.arg1 = args[0]; // 保存上下文,onLeave 中使用
},
onLeave: function (retval) {
console.log("[未导出函数] 返回值:" + retval.toInt32());
}
});
3.4 内存操作与参数解析
Native 层参数多为指针类型,需要通过 Memory 模块读写内存内容,常用 API 包括:
- Memory.readCString(addr):读取 C 风格字符串
- Memory.readInt(addr) / Memory.readLong(addr):读取整型数值
- Memory.writeInt(addr, value):向指定内存写入数值
- hexdump(addr, {length: len}):以十六进制格式打印内存
示例:解析结构体类型参数
Interceptor.attach(targetFunc, {
onEnter: function (args) {
// args[1] 为结构体指针,偏移 0x8 处是 int 类型的长度字段
var dataPtr = Memory.readPointer(args[1]);
var dataLen = Memory.readInt(args[1].add(8));
var data = Memory.readByteArray(dataPtr, dataLen);
console.log("[结构体] 数据长度:" + dataLen);
console.log(hexdump(data, { length: dataLen }));
}
});
四、实战案例:从 Java 到 so 的全链路 Hook
4.1 案例一:Java 层登录明文抓取
场景:某 APP 登录密码在 Java 层做了加密后传输,需要获取用户输入的原始明文密码。
分析思路:反编译 APK 定位登录按钮点击事件,追踪到密码处理类 LoginHelper 的 encryptPassword 方法,Hook 该方法的入参即可得到明文。
完整脚本:
Java.perform(function () {
var LoginHelper = Java.use("com.example.app.login.LoginHelper");
LoginHelper.encryptPassword.overload("java.lang.String").implementation = function (pwd) {
console.log("========== 登录明文抓取 ==========");
console.log("原始明文密码:" + pwd);
var encrypted = this.encryptPassword(pwd);
console.log("加密后密码:" + encrypted);
console.log("==================================");
return encrypted;
};
});
执行命令 frida -U -f com.example.app -l login_hook.js,启动 APP 输入密码后,控制台即可输出原始明文。
4.2 案例二:so 层加密函数 Hook 与参数还原
场景:某 APP 的核心签名算法放在 so 层,Java 层调用 nativeGetSign(String data) 返回签名字符串,需要深入分析算法的输入输出。
分析思路:
完整脚本:
// 1. Java 层:确认 native 方法调用时机
Java.perform(function () {
var SignUtils = Java.use("com.example.app.utils.SignUtils");
SignUtils.nativeGetSign.implementation = function (data) {
console.log("[Java] 调用 nativeGetSign,入参:" + data);
var result = this.nativeGetSign(data);
console.log("[Java] 返回结果:" + result);
return result;
};
});
// 2. so 层:监听 so 加载并 Hook 核心函数
setImmediate(function () {
var base = Module.findBaseAddress("libsign.so");
if (!base) {
// so 未加载时,Hook dlopen 监听加载事件
var dlopen = Module.findExportByName(null, "dlopen");
Interceptor.attach(dlopen, {
onEnter: function (args) {
this.soPath = Memory.readCString(args[0]);
},
onLeave: function () {
if (this.soPath && this.soPath.indexOf("libsign.so") !== -1) {
console.log("[so] libsign.so 加载完成,开始Hook核心函数");
hookCoreFunc();
}
}
});
} else {
hookCoreFunc();
}
});
function hookCoreFunc() {
var baseAddr = Module.findBaseAddress("libsign.so");
var coreFunc = baseAddr.add(0x256C); // IDA 分析得到的函数偏移
Interceptor.attach(coreFunc, {
onEnter: function (args) {
this.inputBuf = args[0];
this.inputLen = args[1].toInt32();
console.log("[so核心] 输入数据长度:" + this.inputLen);
console.log(hexdump(this.inputBuf, { length: this.inputLen }));
},
onLeave: function (retval) {
console.log("[so核心] 输出签名:" + Memory.readCString(retval));
}
});
}
五、进阶技巧与避坑指南
5.1 Spawn 与 Attach 模式选型
- Spawn 模式:适合分析应用启动逻辑、Application 初始化、so 加载阶段的校验,缺点是会重启应用。
- Attach 模式:适合分析页面交互、后台任务,无需重启 APP;缺点是无法拦截启动阶段已经执行过的代码。
5.2 常见时序问题
- 多 ClassLoader 问题:插件化、热修复 APP 存在多个类加载器,直接 Java.use 可能找不到类。需要遍历 ClassLoader 定位目标类所在的加载器。
- so 加载时机问题:so 是动态加载的,脚本执行时 so 可能尚未加载。标准解决方案是 Hook dlopen / android_dlopen_ext 监听加载事件,加载完成后再执行 Hook。
5.3 反检测与对抗技巧
很多 APP 会检测 Frida 特征,常用对抗手段:
5.4 高频坑点汇总
六、总结
Frida 的核心价值在于打通了 Java 层与 Native 层的分析壁垒:Java 层 Hook 适合快速定位业务逻辑、锁定关键函数入口;so 层 Hook 则深入底层,用于还原加密算法、绕过 native 校验。
实际逆向分析中,通常遵循 “先 Java 后 so,先导出后未导出” 的思路:从上层业务逻辑逐步追踪到底层算法,结合静态分析与动态调试,形成完整的分析链路。掌握本文的基础姿势与实战技巧,足以应对绝大多数 APP 的逆向分析场景。在此基础上,还可以进一步学习 Frida RPC、Stalker 代码追踪、内存漫游等进阶功能,向更深度的逆向分析方向进阶。
网硕互联帮助中心



评论前必须登录!
注册