news 2026/10/8 22:41:48

Frida入门到实战:hook so层与Java层的姿势总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Frida入门到实战:hook so层与Java层的姿势总结

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 主要覆盖两类场景:

  1. Java Native 方法:Java 代码中声明为native的方法,既可以从 Java 层 Hook,也可以从 so 层 Hook。
  2. 纯 Native 函数:so 内部的普通函数(导出或未导出),比如系统 libc 的strcmp、自定义 so 的内部算法函数,只能通过 Native 层 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 层 Hook 确认 native 方法的调用时机与入参;
  2. 用 IDA 分析libsign.so,从导出的 JNI 函数向下追踪,找到内部核心加密函数(未导出,偏移 0x256C);
  3. Hook 核心函数,打印输入缓冲区与输出结果。

完整脚本:

// 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 可能尚未加载。标准解决方案是 Hookdlopen/android_dlopen_ext监听加载事件,加载完成后再执行 Hook。

5.3 反检测与对抗技巧

很多 APP 会检测 Frida 特征,常用对抗手段:

  1. 基础隐藏:重命名 frida-server 二进制文件,修改默认监听端口,使用非默认端口连接。
  2. 特征屏蔽:Hookstrcmp、strstr等函数,拦截对 "frida"、"gum-js" 等特征字符串的检测。
  3. 注入方式对抗:使用 frida-gadget 代替 frida-server,通过重打包 APK 注入 so,绕过 ptrace 检测。

5.4 高频坑点汇总

  1. JNI 参数偏移:Native 函数前两个参数是 JNIEnv* 和 jobject,Java 层第一个参数对应 Native 层的 args [2]。
  2. 位数差异:64 位系统指针长度为 8 字节,32 位为 4 字节,计算结构体偏移时必须区分。
  3. Java 对象打印:直接打印 Java 对象会显示[object Object],需要调用toString()或手动拼接字段。
  4. 线程安全:Java.perform必须在有 Java 环境的线程执行,Native 线程中调用需使用Java.scheduleOnMainThread。

六、总结

Frida 的核心价值在于打通了 Java 层与 Native 层的分析壁垒:Java 层 Hook 适合快速定位业务逻辑、锁定关键函数入口;so 层 Hook 则深入底层,用于还原加密算法、绕过 native 校验。

实际逆向分析中,通常遵循 “先 Java 后 so,先导出后未导出” 的思路:从上层业务逻辑逐步追踪到底层算法,结合静态分析与动态调试,形成完整的分析链路。掌握本文的基础姿势与实战技巧,足以应对绝大多数 APP 的逆向分析场景。在此基础上,还可以进一步学习 Frida RPC、Stalker 代码追踪、内存漫游等进阶功能,向更深度的逆向分析方向进阶。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 22:41:39

一张头像做剪映数字人口播:先核输入条件,再用试稿检查音画

教师只有一张授权头像,想制作课程开场,可以先确认当前数字人入口是否接受这类图片,再试一段包含姓名、课程名和完整长句的文稿。数字人能力存在,不代表所有端都接受一张照片,也不意味着任意头像都能直接得到自然的讲话…

作者头像 李华
网站建设 2026/10/8 22:40:32

猎头多平台寻访繁琐,聘小助7×24小时自动触达能力如何?

猎头与招聘团队在多平台寻访候选人时,常面临账号切换、重复搜索、逐个打招呼、消息跟进遗漏、非工作时段响应慢等问题。若工具能实现自动触达与AI初筛,确实可能压缩重复劳动。但“效率提升”不能只看宣传,需要从可验证信息、功能边界、数据合…

作者头像 李华
网站建设 2026/10/8 22:36:24

文献综述总写散?数智营销开题,我更建议按环节挑 AI 搭子 [特殊字符]

先说一个很典型的数智营销专业毕业任务: 研究主题:AI 个性化推荐对即时零售平台用户复购意愿的影响——隐私关注的调节作用 最终要完成:开题报告、国内外研究现状综述、研究模型与假设、问卷设计、抽样与数据分析方案,后续还要跑信…

作者头像 李华
网站建设 2026/10/8 22:35:11

智能体工程化落地的五大硬性门槛与实践路径

1. 这份周报不是“又一份GitHub榜单”,而是智能体演进的刻度尺你点开GitHub Trending页面,刷到的可能是一串新项目名:agent-dojo、hermes-agent、coze-plus、agno-framework……它们不再只是“AI玩具”或“Demo仓库”。过去三个月&#xff0c…

作者头像 李华
网站建设 2026/10/8 22:31:37

智能工厂数据底座:Linux与数据库如何扛住产线稳定运行

1. 从一条产线停机说起:智能工厂的底层到底在跑什么 去年冬天,我去一家做精密减速器的工厂做现场支持。下午两点,装配线突然停了,车间大屏上跳出一片红色报警。现场工程师第一反应是查PLC,查了半天没毛病;第…

作者头像 李华