1. 项目概述:为什么我们需要Frida Hook?
在移动安全研究和应用逆向分析领域,我们常常会遇到一个核心需求:在不修改目标应用原始代码的情况下,动态地观察、修改其运行时行为。无论是为了分析某个应用的加密算法、绕过某些功能限制,还是为了自动化测试,传统的静态分析(如反编译看代码)往往力有不逮。这时,动态插桩技术就成了我们的“瑞士军刀”,而Frida正是这把军刀中最锋利、最趁手的一把。
简单来说,Frida是一个动态代码插桩工具包。它允许你将JavaScript(或Python)代码片段注入到目标进程(比如一个Android App)中,从而能够Hook(挂钩)该进程中的函数,实时地读取函数参数、修改返回值,甚至替换整个函数的实现。这就像给运行中的应用装上了一套“透视镜”和“手术刀”,你能看到它内部的数据流转,并能精准地进行干预。
这次我们聚焦于Android平台,通过一个完整的实战案例——分析AutoJsPro这款自动化工具——来带你从零开始,掌握Frida Hook的核心技能。选择AutoJsPro作为案例,是因为它本身功能强大,涉及Java层和Native(C/C++)层的交互,非常适合展示Frida在不同层面的Hook能力。从环境搭建、基础Hook,到复杂的内存操作和RPC远程调用,我们将一步步拆解,让你不仅能看懂,更能亲手复现每一个环节。
2. 环境准备与Frida基础搭建
工欲善其事,必先利其器。一个稳定、可复现的Frida工作环境是成功的第一步。这里我们会详细讲解在Windows/macOS/Linux主机上配置Frida,并连接Android设备(真机或模拟器)的完整流程,同时避开那些新手常踩的坑。
2.1 主机端Frida环境安装
Frida分为两部分:运行在你电脑上的客户端(frida-tools)和运行在目标设备上的服务端(frida-server)。首先安装客户端。
打开你的命令行终端(Windows用CMD或PowerShell,macOS/Linux用Terminal),使用Python的包管理工具pip进行安装。强烈建议使用Python 3.7及以上版本,并创建一个虚拟环境以避免包冲突。
# 安装frida-tools,它会自动安装frida客户端库 pip install frida-tools安装完成后,可以通过命令验证:
frida --version如果成功输出版本号(如16.1.4),说明客户端安装成功。
注意:如果遇到网络超时或下载缓慢,可以尝试使用国内的PyPI镜像源,例如
pip install frida-tools -i https://pypi.tuna.tsinghua.edu.cn/simple。
2.2 目标设备端Frida-Server部署
这是关键且容易出错的一步。Frida-server是一个需要根据目标设备CPU架构和Android系统版本精心选择的二进制文件。
确定设备架构:连接你的Android设备(确保已开启USB调试),在命令行输入:
adb shell getprop ro.product.cpu.abi常见的输出有:
arm64-v8a: 64位ARM架构(现代手机主流)。armeabi-v7a: 32位ARM架构(较旧设备)。x86/x86_64: Intel架构(常见于模拟器,如雷电模拟器通常为x86_64)。
下载匹配的frida-server:前往Frida的GitHub Releases页面(https://github.com/frida/frida/releases ),找到与你的
frida --version输出相同版本号的发布包。例如,你主机安装的是16.1.4,就去找frida-server-16.1.4-android-*.xz文件。根据你的设备架构选择对应的文件(如frida-server-16.1.4-android-arm64.xz)。推送并启动frida-server:
# 解压下载的.xz文件,得到frida-server文件 # 使用adb push推送到设备的可执行目录,如/data/local/tmp adb push frida-server-16.1.4-android-arm64 /data/local/tmp/ # 进入adb shell,赋予可执行权限 adb shell cd /data/local/tmp chmod 755 frida-server-16.1.4-android-arm64 # 以后台方式运行frida-server,&符号表示后台运行 ./frida-server-16.1.4-android-arm64 &验证连接:退出adb shell(按Ctrl+D或输入
exit),在主机命令行运行:frida-ps -U参数
-U表示通过USB连接设备。如果一切正常,你会看到设备上正在运行的进程列表。如果报错“Failed to enumerate processes: unable to connect to remote frida-server”,请检查设备是否授权了USB调试、frida-server进程是否真的在运行(可以用ps | grep frida查看),以及防火墙是否阻挡了连接。
实操心得:对于模拟器,尤其是雷电模拟器,其adb端口通常不是标准的5037。你需要先使用
adb connect 127.0.0.1:5555(雷电默认端口)连接上模拟器,再进行后续操作。另外,每次模拟器重启后,/data/local/tmp目录下的文件可能会被清除,需要重新push和启动frida-server。
2.3 基础Hook脚本结构与运行
一个最简单的Frida Hook脚本通常包含以下几个部分:
// hook_demo.js Java.perform(function () { // 确保在Java VM上下文中执行 // 1. 定位要Hook的类 var TargetClass = Java.use("com.example.target.ClassName"); // 2. Hook类中的特定方法 TargetClass.targetMethod.implementation = function (arg1, arg2) { // 3. 打印日志,查看传入的参数 console.log("[*] targetMethod called!"); console.log(" arg1: " + arg1); console.log(" arg2: " + arg2); // 4. 调用原方法(可选),并获取返回值 var retval = this.targetMethod(arg1, arg2); // 使用 this 调用原方法 // 5. 打印或修改返回值 console.log(" retval: " + retval); // retval = "Hooked!"; // 可以修改返回值 // 6. 返回结果 return retval; }; });运行这个脚本:
# -U: USB设备, -f: 启动应用, -l: 加载脚本 frida -U -f com.example.package -l hook_demo.js --no-pause # 或者附加到已运行进程 frida -U com.example.package -l hook_demo.js3. 核心Hook技术深度解析
掌握了基础环境,我们来深入Frida Hook的核心技术。理解这些概念,是你从“能用”到“精通”的关键。
3.1 Java层Hook:从方法到构造函数
Java层Hook是Frida最常用的功能。除了Hook普通方法,构造函数、静态方法、方法重载等都需要特别注意。
Hook构造函数:构造函数在对象创建时调用,是初始化数据的绝佳观察点。
Java.perform(function () { var SecretClass = Java.use("com.autojs.pro.core.SecretManager"); // Hook构造函数,注意方法名是`$init` SecretClass.$init.overload('android.content.Context', 'java.lang.String').implementation = function(ctx, key) { console.log(`[*] SecretManager created! Key: ${key}`); // 继续执行原构造函数 return this.$init(ctx, key); }; });处理方法重载:Java支持方法重载,同一个方法名可能有多个参数列表。Frida使用.overload()来指定。
var Utils = Java.use("com.autojs.pro.utils.EncryptUtils"); // Hook 第一个 encrypt(String) 方法 Utils.encrypt.overload('java.lang.String').implementation = function(data) { console.log(`Encrypting string: ${data}`); return this.encrypt(data); }; // Hook 第二个 encrypt(String, String) 方法 Utils.encrypt.overload('java.lang.String', 'java.lang.String').implementation = function(data, key) { console.log(`Encrypting with key. Data: ${data}, Key: ${key}`); return this.encrypt(data, key); };访问和修改类字段(成员变量):
Java.perform(function () { var Config = Java.use("com.autojs.pro.model.AppConfig"); // 获取静态字段 var oldFlag = Config.DEBUG.value; console.log(`Old DEBUG flag: ${oldFlag}`); // 修改静态字段 Config.DEBUG.value = true; // 对于实例字段,需要先有一个对象实例 // 假设我们Hook了一个方法,其中`this`就是实例 SomeClass.someMethod.implementation = function() { console.log(`Instance field value: ${this._privateField.value}`); this._privateField.value = 100; // 修改实例字段 return this.someMethod(); }; });3.2 Native层(SO库)Hook:深入C/C++世界
许多核心逻辑和加密算法会放在Native层的SO(共享对象)库中。Hook SO库需要更精确的地址和函数签名。
通过导出函数名Hook:这是最简单的情况,函数名在SO中是公开的。
Interceptor.attach(Module.findExportByName("libcrypto.so", "AES_encrypt"), { onEnter: function(args) { // args[0], args[1]... 是函数的参数,根据函数签名确定 console.log(`[*] AES_encrypt called.`); // 打印第一个参数(假设是输入数据指针) this.inputPtr = args[0]; console.log(hexdump(this.inputPtr, { length: 16 })); }, onLeave: function(retval) { // retval 是函数的返回值 console.log(`[*] AES_encrypt returned.`); console.log(hexdump(retval, { length: 16 })); } });通过地址偏移Hook:当函数是静态链接或未导出时,我们需要计算函数在内存中的绝对地址。公式为:函数地址 = Module.baseAddress + 函数偏移量。偏移量可以通过IDA Pro等逆向工具分析SO文件获得。
var libnative = Module.findBaseAddress("libnative-lib.so"); if (libnative) { var funcOffset = 0x1234; // 从IDA中获取的偏移量,例如 0x1234 var funcAddress = libnative.add(funcOffset); Interceptor.attach(funcAddress, { onEnter: function(args) { console.log(`[*] Function at offset 0x${funcOffset.toString(16)} entered.`); } }); }Hook Android JNI函数:Java Native Interface是Java和Native交互的桥梁。Hook JNI函数可以捕获两者之间的数据交换。
// 例如,Hook JNIEnv->FindClass 函数 var jniEnv = Java.vm.getEnv(); var findClassAddr = Module.findExportByName(null, "_ZN7_JNIEnv9FindClassEPKc"); if (findClassAddr) { Interceptor.attach(findClassAddr, { onEnter: function(args) { var className = Memory.readCString(args[1]); // 第二个参数是类名字符串 console.log(`JNIEnv->FindClass: ${className}`); } }); }3.3 主动调用与RPC:将Hook能力转化为工具
Hook是“被动监听”,而主动调用和RPC(远程过程调用)则是“主动出击”,让你能像调用本地函数一样调用目标进程中的方法,这对于批量测试、算法还原和自动化工具开发至关重要。
在Hook脚本内主动调用:
Java.perform(function () { // 1. 定位类 var Calculator = Java.use("com.autojs.pro.utils.Calculator"); // 2. 调用静态方法 var staticResult = Calculator.addStatic(5, 3); console.log(`Static call result: ${staticResult}`); // 3. 构造对象实例并调用实例方法 // 首先需要获取类的引用(Class对象),然后获取构造函数 var CalculatorClass = Java.use("com.autojs.pro.utils.Calculator").class; // 假设有一个无参构造函数 var calculatorInstance = Calculator.$new(); var instanceResult = calculatorInstance.add(10, 20); console.log(`Instance call result: ${instanceResult}`); });建立Frida RPC接口:这是更强大的功能,允许你的Python脚本或其他外部程序与注入的JS脚本通信,远程调用函数。
// rpc_script.js Java.perform(function () { // 定义一个RPC函数,它将被暴露给外部调用 rpc.exports = { // 定义一个名为`decryptData`的远程函数 decryptData: function (encryptedBase64) { var result = ""; Java.perform(function () { try { var Crypto = Java.use("com.autojs.pro.core.Crypto"); // 将JS字符串转为Java字符串 var jString = Java.use("java.lang.String").$new(encryptedBase64); // 主动调用解密方法 var jResult = Crypto.decrypt(jString); result = jResult.toString(); // 将Java字符串转回JS字符串 } catch (e) { result = "Error: " + e.message; } }); return result; // 返回给调用者 }, getVersion: function () { var version = ""; Java.perform(function () { var AppInfo = Java.use("com.autojs.pro.App"); version = AppInfo.getVersionName(); }); return version; } }; });对应的Python控制端脚本:
# rpc_client.py import frida import sys def on_message(message, data): if message['type'] == 'send': print(f"[*] Message from script: {message['payload']}") else: print(message) # 连接到设备 device = frida.get_usb_device() # 附加到目标进程 session = device.attach("com.autojs.pro") # 加载包含RPC的脚本 with open("rpc_script.js", "r", encoding="utf-8") as f: script_code = f.read() script = session.create_script(script_code) script.on('message', on_message) script.load() # 获取RPC接口 api = script.exports # 远程调用! version = api.get_version() print(f"App Version: {version}") encrypted_input = "aGVsbG8gd29ybGQ=" # "hello world"的base64 decrypted_output = api.decrypt_data(encrypted_input) print(f"Decrypted: {decrypted_output}") # 保持脚本运行 sys.stdin.read()4. AutoJsPro实战案例拆解
现在,我们将所有技术应用于一个具体目标:AutoJsPro。请注意,本案例仅用于学习Frida技术和移动应用安全研究方法,请勿用于任何违反软件许可协议或法律的行为。我们的目标是理解其部分实现机制。
假设我们通过基础分析,发现AutoJsPro的核心功能(如脚本执行、控件操作)依赖于一个名为com.autojs.pro.core.Engine的类,其中有一个关键方法executeScript(String script)。
4.1 目标分析与方法定位
首先,我们需要确认目标类和方法的存在。可以使用Frida的枚举功能:
// enumerate.js - 用于快速侦察 Java.perform(function () { // 枚举所有已加载的类,查找包含特定关键词的类 Java.enumerateLoadedClasses({ onMatch: function(className) { if (className.includes("autojs") && className.includes("Engine")) { console.log(`[*] Found class: ${className}`); // 进一步枚举类的方法 var TargetClass = Java.use(className); var methods = TargetClass.class.getDeclaredMethods(); methods.forEach(function(method) { console.log(` -> ${method.getName()}`); }); } }, onComplete: function() { console.log("[*] Class enumeration complete."); } }); });运行此脚本,我们可能发现目标类com.stardust.autojs.core.Engine和方法executeScript。
4.2 关键方法Hook与参数捕获
接下来,编写详细的Hook脚本,捕获脚本执行时的细节。
// hook_autojs.js Java.perform(function () { console.log("[*] Hooking AutoJsPro Engine..."); var Engine = Java.use("com.stardust.autojs.core.Engine"); // Hook executeScript 方法 Engine.executeScript.overload('java.lang.String').implementation = function(scriptContent) { console.log("\n========== Script Execution Hooked =========="); console.log(`[*] Timestamp: ${new Date().toLocaleString()}`); console.log(`[*] Call Stack:`); // 打印Java调用栈,有助于理解执行路径 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); // 获取传入的脚本内容 console.log(`[*] Script Content (${scriptContent.length()} chars):`); // 如果脚本太长,可以截断显示 if (scriptContent.length() > 500) { console.log(scriptContent.substring(0, 500) + "...[TRUNCATED]"); } else { console.log(scriptContent.toString()); } // 获取调用此方法的对象信息(如果有) console.log(`[*] Engine Instance: ${this.toString()}`); // 调用原方法执行脚本 var startTime = Date.now(); try { var result = this.executeScript(scriptContent); // 执行原逻辑 var endTime = Date.now(); console.log(`[*] Execution Time: ${endTime - startTime}ms`); console.log(`[*] Returned: ${result}`); return result; } catch (e) { console.log(`[*] Execution threw an exception: ${e}`); throw e; // 重新抛出异常 } finally { console.log("========== Hook End ==========\n"); } }; // 可以同时Hook其他相关方法,比如初始化或资源加载 Engine.prepare.overload().implementation = function() { console.log(`[*] Engine.prepare() called.`); return this.prepare(); }; console.log("[*] Hooks installed successfully."); });4.3 拦截与修改脚本执行逻辑
在某些学习场景下,我们可能想动态修改即将执行的脚本。例如,在所有脚本开头插入一行日志。
Engine.executeScript.overload('java.lang.String').implementation = function(scriptContent) { var modifiedScript = "console.log('【Frida Injected】Script started at ' + new Date());\n" + scriptContent; console.log(`[*] Original script modified.`); // 使用修改后的脚本调用原方法 return this.executeScript(modifiedScript); };4.4 定位加密与通信模块
AutoJsPro可能涉及与服务器通信或本地加密。我们可以通过Hook常见的加密类和网络库来观察。
Hook JSON解析(常用于配置):
var JSONObject = Java.use("org.json.JSONObject"); JSONObject.$init.overload('java.lang.String').implementation = function(jsonStr) { console.log(`[*] JSONObject created with string: ${jsonStr.substring(0, 200)}...`); return this.$init(jsonStr); };Hook 网络请求库(如OkHttp3):
Java.perform(function() { // 尝试Hook OkHttp的Call.execute方法 var Call = null; try { Call = Java.use("okhttp3.Call"); } catch (e) { console.log("OkHttp3 not found or different version."); } if (Call) { Call.execute.implementation = function() { var request = this.request(); console.log(`[*] HTTP Request: ${request.method()} ${request.url()}`); var headers = request.headers(); for (var i = 0; i < headers.size(); i++) { console.log(` ${headers.name(i)}: ${headers.value(i)}`); } // 获取请求体(可能需要异步处理) var requestBody = request.body(); if (requestBody) { // 注意:读取请求体可能比较耗时,在生产环境Hook中需谨慎 console.log(` [Request Body Hooked]`); } var response = this.execute(); // 执行请求 console.log(`[*] HTTP Response Code: ${response.code()}`); return response; }; } });5. 高级技巧与问题排查实录
掌握了基础Hook和实战后,你会遇到更复杂的情况。这一章分享一些高级技巧和常见问题的解决方法。
5.1 处理加固与反调试
一些应用会使用加固技术或反调试手段来干扰Frida。
检测Frida:常见手段包括检测frida-server进程、端口(默认27042)或特征库文件。应对方法:
- 重命名frida-server:将推送至设备的
frida-server文件改名为其他名字,如/data/local/tmp/fs,运行时也使用新名字。 - 修改默认端口:启动frida-server时指定非默认端口。
连接时指定端口:./fs -l 0.0.0.0:8080 # 在设备上运行,监听8080端口frida -H 192.168.1.5:8080 -f com.example.app -l script.js # 假设设备IP是192.168.1.5 - 使用隐蔽模式:Frida提供了一些尝试隐藏自身的选项,但效果有限。更彻底的方法是定制编译Frida,修改其二进制特征。
应对应用崩溃:Hook某些关键函数可能导致应用崩溃,尤其是onEnter或onLeave中代码有异常时。
- 使用try-catch:将Hook回调函数内的代码用try-catch包裹。
Implementation = function() { try { // 你的Hook代码 } catch (e) { console.log(`Hook error: ${e}`); } return this.method(); } - 避免阻塞操作:在Hook回调中执行耗时操作(如网络请求)会阻塞主线程,导致ANR。应将耗时操作放到其他线程或通过RPC交给外部处理。
5.2 性能优化与稳定Hook
- 精确Hook,避免宽泛匹配:使用
.overload()精确指定参数类型,避免Hook到不期望的方法导致意外行为或性能下降。 - 适时禁用和启用Hook:对于频繁调用的函数(如日志输出),持续打印会导致性能灾难。可以设置条件或开关。
var enableLogging = false; TargetClass.busyMethod.implementation = function() { if (enableLogging) { console.log("called"); } return this.busyMethod(); }; // 通过RPC控制开关 rpc.exports = { setLogging: function(en) { enableLogging = en; } }; - 使用
setImmediate:在Java.perform中,如果初始化Hook的代码较多,使用setImmediate可以将部分代码推迟到下一个事件循环执行,避免阻塞VM初始化。Java.perform(function() { // 立即执行核心Hook hookCriticalMethod(); setImmediate(function() { // 延迟执行次要或耗时的Hook hookSecondaryMethods(); enumerateClasses(); }); });
5.3 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
frida-ps -U无输出或报错 | 1. frida-server未运行 2. USB调试未授权 3. 设备离线 4. 端口冲突 | 1.adb shell进入设备,ps | grep frida查看进程。2. 检查设备屏幕是否弹出“允许USB调试”提示。 3. adb devices确认设备列表。4. 尝试重启adb: adb kill-server && adb start-server。 |
| 脚本注入后应用闪退 | 1. Hook了错误的方法或类 2. Hook代码中有未处理异常 3. 触发了反调试 | 1. 检查类名和方法签名是否完全正确,注意混淆后的名称可能变化。 2. 在Hook回调函数内部添加 try-catch。3. 尝试先Hook一些无害的方法(如 java.lang.System.currentTimeMillis())测试环境。 |
| Hook后无任何输出 | 1. 脚本未正确加载 2. 目标方法从未被调用 3. console.log输出被重定向或过滤 | 1. 在脚本开头加console.log("[*] Script loaded!");确认加载。2. 确认应用逻辑确实走到了你Hook的代码路径。 3. 使用 frida -U -f package -l script.js --no-pause确保看到Frida输出。也可以尝试用send()代替console.log()并通过onMessage回调接收。 |
| 无法找到类或方法 | 1. 类尚未加载 2. 类名错误(混淆) 3. 方法签名不匹配 | 1. 将Hook代码包裹在setImmediate或等待应用启动后再注入。2. 使用 Java.enumerateLoadedClasses动态查找类名。3. 使用 Java.choose()在堆上查找已存在的实例,或通过实例获取类。 |
RPC调用返回undefined或错误 | 1. RPC函数内部有异常未捕获 2. 线程上下文问题 3. 数据类型转换错误 | 1. 在RPC函数内部添加详细的try-catch,并将错误信息返回。2. 确保在 Java.perform()内执行Java操作。3. 注意JS字符串和Java字符串的转换,使用 Java.use("java.lang.String").$new()。 |
5.4 内存操作与指针读写
在Native层Hook时,经常需要直接读写内存。
// 读取内存中的数据 var baseAddr = Module.findBaseAddress("libtarget.so"); // 读取一个32位整数(4字节) var intValue = Memory.readS32(baseAddr.add(0x1000)); // 读取一个以null结尾的C字符串 var cString = Memory.readCString(baseAddr.add(0x2000)); // 读取一段内存并十六进制转储 console.log(hexdump(baseAddr.add(0x3000), { length: 64, ansi: true })); // 写入内存 Memory.writeS32(baseAddr.add(0x1000), 0x12345678); // 写入一个32位整数 Memory.writeUtf8String(baseAddr.add(0x2000), "Frida Injected"); // 写入字符串 // 分配内存 var allocatedMemory = Memory.alloc(1024); // 分配1KB内存 Memory.writeByteArray(allocatedMemory, [0x41, 0x42, 0x43]); // 写入字节数组踩过的坑告诉我,在进行内存操作前,务必确认地址是可读写的,否则会导致进程崩溃。可以使用Memory.protect()查询或修改内存页权限,但需谨慎操作。
从环境搭建到实战案例,再到高级技巧,Frida Hook的学习是一个螺旋上升的过程。最关键的一步永远是动手实践。从一个简单的Toast显示方法Hook起,逐步挑战更复杂的逻辑。遇到问题多查阅官方文档(https://frida.re/docs/ )和社区案例,善用console.log进行调试。记住,逆向分析的本质是理解和学习,请始终在法律和道德允许的范围内使用这些技术。