Frida安卓Hook实战:5分钟稳定hook函数的完整链路
1. 这不是“秒 hook”,而是把5分钟拆解成你真正能掌控的每一步
很多人看到标题里的“5分钟”就点进来,结果照着网上零散教程跑了一小时,frida-server 启动失败、目标进程找不到、Java.choose 返回空数组、hook 函数后没反应——最后关掉终端,默默怀疑自己是不是不适合搞逆向。我刚入行那会儿也这样,花三天才搞懂为什么Java.use('android.util.Log').d.overload('java.lang.String', 'java.lang.String').implementation能 hook 成功,而换一个 overload 就直接报overload is not found。后来才明白:所谓“5分钟搞定”,根本不是指从零开始到弹出 log 的总耗时,而是指当你已经踩过所有坑、环境稳定、目标明确、脚本结构清晰之后,真正执行 hook 动作本身,确实只需要敲几行命令、改两处函数名、回车运行,5分钟内就能看到结果。
这篇内容的核心关键词是:Frida、安卓 APP 函数 Hook、常见错误排查。它不面向纯小白讲“什么是逆向”,也不堆砌 Frida 的源码原理,而是聚焦在真实设备上、真实 APK 包里、真实开发/测试/安全分析场景下,如何让 Frida 稳稳地 hook 到你想看的那个函数,并且当它不工作时,你能像老司机一样快速定位到底是 Frida-server 没连上、类没加载、函数签名写错、还是目标 APP 做了反调试干扰。适合三类人:Android 开发想动态调试自家 APP 的关键逻辑;渗透测试人员需要绕过登录校验或抓取加密参数;以及刚接触 Frida、被各种ScriptDestroyed、Failed to find class、TypeError: Cannot read property 'implementation' of undefined折磨得想卸载 adb 的实践者。下面我会把这“5分钟”掰开揉碎,还原成可复现、可验证、可 debug 的完整链路——从设备准备到脚本落地,再到每一个报错背后的真实原因和现场验证方法。
2. 环境不是“装完就完事”,而是每一层都必须亲手验证过
Frida 的环境链比表面看起来长得多:PC 上的 frida-tools → USB 线/网络连接 → 手机上的 frida-server → 目标 APP 进程 → APP 自身的类加载器与运行时状态。任何一层断开,都会表现为“hook 不生效”,但表现形式千差万别。我见过太多人卡在第一步:frida -U -f com.example.app --no-pause执行后卡住不动,就以为是 Frida 问题,其实只是手机没开 USB 调试,或者开了但选的是“仅充电”模式。所以“环境准备”不是 checklist 式的安装步骤罗列,而是逐层握手、逐层确认通信是否真实建立。
2.1 PC 端:frida-tools 必须与手机端 frida-server 版本严格对齐
很多人用pip install frida-tools装最新版,然后去官网下载个旧版 frida-server(比如 v14.x),结果frida-ps -U能列出进程,但frida -U -f xxx就报Failed to spawn: unable to determine architecture。这不是兼容性问题,而是协议版本不匹配导致 handshake 失败。Frida 的通信协议在大版本间有 breaking change,v15.x 的 client 默认用新协议,而 v14.x server 只认旧协议。实测下来最稳的组合是:frida-tools v15.2.2 + frida-server v15.2.2(截至2024年中)。安装命令必须带版本号:
pip install frida-tools==15.2.2提示:不要用
pip install frida,它只装 Python binding,不包含 frida-tools 命令行工具;也不要pip install --upgrade frida-tools,升级后极易与旧版 server 不兼容。
验证方式不是看frida --version,而是执行:
frida --version && frida-ps -U如果frida-ps -U能正常输出进程列表(哪怕只有 system_server),说明 client 与 server 握手成功。如果报Failed to enumerate processes: unable to determine architecture,立刻检查 server 版本——别猜,直接adb shell ./data/local/tmp/frida-server --version。
2.2 手机端:frida-server 不是“扔进去就行”,必须适配架构、权限、SELinux 状态
安卓设备 CPU 架构分 arm64-v8a、armeabi-v7a、x86_64 等。frida-server 是原生二进制,必须严格匹配。常见错误是:在 arm64 手机上推了 arm32 的 server,结果./frida-server -D启动后立即退出,adb logcat | grep frida什么都没有。正确做法是:
- 先查手机架构:
adb shell getprop ro.product.cpu.abi - 去 https://github.com/frida/frida/releases 下载对应架构的 frida-server(注意文件名含
android-arm64或android-x86_64) - 推送到
/data/local/tmp/(这是唯一有执行权限的非系统目录):adb push frida-server-15.2.2-android-arm64 /data/local/tmp/frida-server adb shell "chmod 755 /data/local/tmp/frida-server"
注意:
/data/local/tmp/是临时目录,重启后内容丢失,但这是设计使然——避免残留旧版 server 干扰。每次重装 APP 或重启手机后,都要重新推送并启动 server。
启动 server 有两种模式:
- 后台守护模式(推荐):
adb shell "/data/local/tmp/frida-server -D",-D表示 daemonize,server 在后台持续运行,frida-ps -U才能稳定列出进程。 - 前台阻塞模式(调试用):
adb shell "/data/local/tmp/frida-server",此时终端被占住,适合观察 server 启动日志(如 SELinux 拒绝记录)。
SELinux 是另一个隐形杀手。某些定制 ROM(尤其国产厂商深度定制版)默认开启 enforcing 模式,即使你用 root 启动 frida-server,也会被 SELinux 策略拦截。现象是:server 进程存在,但frida-ps -U无响应,adb logcat | grep avc会刷屏avc: denied { execute } for path="/data/local/tmp/frida-server"。解决方法不是关 SELinux(很多设备无法setenforce 0),而是给 frida-server 打上正确的 SELinux context:
adb shell su -c "chcon u:object_r:shell_data_file:s0 /data/local/tmp/frida-server"这条命令把 frida-server 的安全上下文设为 shell 进程可执行的类型。实测在小米、华为、OPPO 多数机型上有效。如果chcon命令不存在,说明 su 权限未完全获取,需换用 Magisk Manager 的“高级设置”中开启“Enforce SELinux”。
2.3 连接验证:用最原始的frida-ps和frida-trace确认链路畅通
别急着写 hook 脚本。先做两件事:
- 确认设备识别:
frida-ps -U应该列出至少 10 个以上进程(system_server、zygote、surfaceflinger 等),如果只显示PID Name两行,说明连接失败; - 确认目标 APP 可见:启动目标 APP,再执行
frida-ps -U | grep com.example.app,必须能看到其 PID; - 确认基础 trace 可用:
frida-trace -U -i "open" com.example.app,如果看到Started tracing 1 function. Press Ctrl+C to stop.且后续有 open 系统调用日志输出,说明 Frida 已能注入并监控目标进程。
这三步走完,环境才算真正 ready。我坚持要求团队新人必须手敲这三行命令并截图给我看,因为 70% 的“hook 不生效”问题,根源都在这三步的某一个环节没过。
3. Hook 不是“写对函数名就行”,而是要穿透 Java 层、Native 层、混淆层三重迷雾
很多人以为 Frida Hook 就是Java.use('xxx').yyy.implementation = function() {...},结果发现Java.use('com.example.MainActivity')报Error: java.lang.ClassNotFoundException。其实,Frida 的 Java API 只能 hook已加载到当前 ClassLoader 中的类。而安卓 APP 的类加载是懒加载的:MainActivity只有在用户点击图标、AMS 启动 Activity 时才会被加载;OkHttpClient相关类可能在首次网络请求时才由 DexClassLoader 加载。所以Java.choose查不到类,往往不是类不存在,而是还没被加载。
3.1 Java 层 Hook:从Java.perform到Java.choose的时机陷阱
正确写法永远是:
Java.perform(function () { // 所有 Java.use / Java.choose 必须放在这里 Java.choose("com.example.network.ApiService", { onMatch: function (instance) { console.log("Found instance: " + instance); // 对每个已加载实例做操作 }, onComplete: function () { console.log("Search finished"); } }); });Java.perform是 Frida 的 Java 运行时入口,它确保代码在 Java VM 的上下文中执行。漏掉它,Java.use会直接报ReferenceError: Java is not defined。更隐蔽的坑是Java.choose的时机:如果目标类在脚本运行时尚未加载,onMatch一次都不会触发。解决方案有两个:
方案一:等待加载(推荐):用
Java.scheduleOnMainThread配合轮询:Java.perform(function () { var loaded = false; var interval = setInterval(function () { Java.choose("com.example.network.ApiService", { onMatch: function (instance) { console.log("Class loaded, hooking now..."); // 此处写真正的 hook 逻辑 clearInterval(interval); loaded = true; }, onComplete: function () { if (!loaded) console.log("Still waiting..."); } }); }, 500); // 每500ms查一次 });方案二:强制触发加载:如果知道某个方法调用会加载目标类,先调用它:
Java.perform(function () { try { var dummy = Java.use("com.example.util.DummyLoader"); dummy.triggerLoad(); // 假设这个方法会触发 ApiService 加载 } catch (e) { console.log("Dummy load failed, continue waiting..."); } });
3.2 Native 层 Hook:Interceptor.attach的符号解析真相
Hooklibnative.so里的Java_com_example_MainActivity_encrypt很简单,但 hooklibcrypto.so里的AES_encrypt就容易翻车。因为 Frida 的Interceptor.attach默认只解析导出符号(exported symbols)。AES_encrypt在 OpenSSL 中是静态链接的内部函数,不会出现在.dynsym表中,Module.findExportByName("libcrypto.so", "AES_encrypt")必然返回 null。
破解方法有三:
- 用
Module.findBaseAddress定位模块起始地址,再用偏移硬编码(不推荐,版本一升级就失效); - 用
Module.load()加载符号表(需提前编译带 debug info 的 so); - 最实用:用
Module.enumerateSymbols全局扫描:var crypto = Module.load("libcrypto.so"); crypto.enumerateSymbols({ onMatch: function(symbol) { if (symbol.name.indexOf("AES_encrypt") !== -1) { console.log("Found at: " + symbol.address); Interceptor.attach(symbol.address, { onEnter: function(args) { console.log("AES_encrypt called with key len: " + args[2]); } }); } }, onComplete: function() {} });
注意:
enumerateSymbols会遍历所有符号(包括调试符号),速度慢但可靠。实测在 10MB 级别的 so 上耗时约 2~3 秒,完全可以接受。
3.3 混淆对抗:从 ProGuard 映射表到Java.enumerateLoadedClasses
APP 经过 ProGuard 混淆后,com.example.network.ApiService可能变成a.b.c.d。靠猜名字 hook 是自杀行为。正确路径是:
- 获取映射表(mapping.txt):如果是自己公司的 APP,构建产物里一定有
app/build/outputs/mapping/release/mapping.txt,用addr2line或在线工具反查; - 运行时枚举所有已加载类:
Java.enumerateLoadedClasses({onMatch: function(name) {console.log(name);}, onComplete: function(){}}),输出上千行类名,用grep筛选关键词:frida -U -l list-classes.js com.example.app | grep -i "network\|api\|http" - 结合
Java.choose动态确认:找到疑似类名(如a.a.a.a)后,用Java.choose实例化并调用其方法,看返回值是否符合预期(如toString()输出包含 URL 字符串)。
我处理过一个金融类 APP,其核心加解密类被混淆成com.a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z.A.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z这种长度,靠人工根本没法看。最后是用enumerateLoadedClasses导出全部类名到文件,再用 Python 脚本统计类名中出现频率最高的字母组合,锁定z和A两个包名前缀,再结合Java.choose调用其encrypt方法传入固定字符串,比对返回值 hex 是否与 APP 实际网络请求中的加密字段一致,才最终确认。
4. 常见错误不是“报错就搜”,而是按错误类型归因到具体层级
Frida 报错信息往往很简短,但背后原因天差地别。我把高频报错按发生位置分类,给出可立即执行的验证步骤,而不是泛泛而谈“检查环境”。
4.1Failed to spawn: unable to determine architecture—— client/server 协议层断裂
这不是设备问题,是 Frida-tools 与 frida-server 版本不匹配的铁证。验证步骤:
- 在 PC 端执行:
frida --version,记下版本(如15.2.2); - 在手机端执行:
adb shell "/data/local/tmp/frida-server --version",如果返回14.2.18,立刻去 GitHub 下载 v15.2.2 的 server; - 不要覆盖旧文件:先
adb shell rm /data/local/tmp/frida-server,再adb push新版; - 重启 server:
adb shell killall frida-server && adb shell "/data/local/tmp/frida-server -D"; - 再试
frida-ps -U。
注意:
frida --version显示的是 frida-tools 版本,不是 frida-core。两者必须一致。曾有人pip install frida-tools==15.2.2但pip list | grep frida发现frida(core)是 14.x,导致同样报错。正确命令是pip install frida==15.2.2 frida-tools==15.2.2。
4.2ScriptDestroyed—— 脚本生命周期管理失控
现象:脚本运行几秒后自动退出,log 里只有一句ScriptDestroyed。根本原因是 Frida 默认在目标进程退出时销毁脚本,但很多 APP 启动后很快进入后台或被系统杀掉。解决方案不是“重写脚本”,而是控制脚本存活策略:
--no-pause参数必须加:frida -U -f com.example.app --no-pause -l hook.js,否则 APP 启动后 Frida 会暂停进程,等你 attach,但很多 APP 在暂停状态下会自检崩溃;- 用
Java.performNow替代Java.perform:Java.performNow立即执行,不等待 VM 初始化完成,适合极快退出的进程; - 加
setTimeout保活:在脚本末尾加:setTimeout(function() { console.log("Script kept alive for 30s"); }, 30000);
4.3TypeError: Cannot read property 'implementation' of undefined—— 函数签名拼写错误的精确诊断法
这不是 JS 语法错误,而是 Frida 找不到你要 hook 的那个 overload。例如:
var cls = Java.use("okhttp3.OkHttpClient"); cls.newCall.overload("okhttp3.Request").implementation = function(req) { ... };报错原因可能是:
newCall方法实际签名是newCall(Req),但Req类型全名是okhttp3.Request,大小写敏感;newCall有多个 overload,overload("okhttp3.Request")匹配不到,因为实际是overload("okhttp3.Request", "boolean");OkHttpClient类存在,但newCall方法是public,而 Frida 默认只找public方法,如果它是package-private,需用getDeclaredMethod。
诊断方法:不用猜,用 Frida 自带的反射 API 打印所有 overload:
Java.perform(function () { var cls = Java.use("okhttp3.OkHttpClient"); console.log("Methods in OkHttpClient:"); var methods = cls.class.getDeclaredMethods(); for (var i = 0; i < methods.length; i++) { if (methods[i].getName() === "newCall") { console.log("newCall overload: " + methods[i].toGenericString()); } } });输出类似:
public okhttp3.Call okhttp3.OkHttpClient.newCall(okhttp3.Request) public okhttp3.Call okhttp3.OkHttpClient.newCall(okhttp3.Request, boolean)然后你就能 100% 确定该用overload("okhttp3.Request")还是overload("okhttp3.Request", "boolean")。这个技巧我教过 20+ 个同事,没人再因为 overload 写错而浪费半小时。
4.4Failed to find class—— 类加载时机与 ClassLoader 隔离的实战解法
Java.use("com.example.network.ApiService")报错,但Java.enumerateLoadedClasses能列出它,说明类已加载,但Java.use找不到。这是因为 Frida 的Java.use默认使用系统 ClassLoader,而 APP 的类通常由PathClassLoader加载。解决方案:
Java.perform(function () { // 获取当前线程的上下文 ClassLoader var cl = Java.threadCxtClassLoader(); // 或者获取目标类的 ClassLoader var apiClass = Java.use("java.lang.Class").forName("com.example.network.ApiService"); var loader = apiClass.getClassLoader(); // 用指定 ClassLoader 加载 var apiService = Java.use("com.example.network.ApiService", { classLoader: loader }); });更彻底的方法是hook ClassLoader 的loadClass方法,全程监控类加载过程:
Java.perform(function () { var cl = Java.use("java.lang.ClassLoader"); cl.loadClass.overload("java.lang.String").implementation = function(name) { if (name.indexOf("ApiService") !== -1) { console.log("ClassLoader loading: " + name); } return this.loadClass.apply(this, arguments); }; });这样你就能看到ApiService是在哪个时刻、由哪个 ClassLoader 加载的,从而精准选择Java.use的上下文。
5. 真正的“5分钟”是:从确定目标到看到 log 的闭环验证
现在我们把前面所有环节串起来,走一遍标准的、可复现的“5分钟 Hook 流程”。以 hook 某电商 APP 的登录接口加密函数为例,目标是拿到明文密码被加密后的 hex 字符串。
5.1 第1分钟:确认环境与目标进程
- 手机打开 USB 调试,连接电脑;
adb devices确认设备在线;frida-ps -U | grep com.shop.app确认 APP 包名;adb shell am start -n com.shop.app/.activity.LoginActivity启动登录页;frida -U com.shop.app进入交互式 shell,输入%resume确保进程运行。
5.2 第2分钟:定位目标类与方法
- 在 Frida shell 中执行:
Java.enumerateLoadedClasses({ onMatch: function(name) { if (name.indexOf("login") !== -1 || name.indexOf("encrypt") !== -1) { console.log(name); } }, onComplete: function() {} }); - 找到
com.shop.security.EncryptUtil; - 执行
Java.choose("com.shop.security.EncryptUtil", {onMatch: function(i) {console.log(i);}, onComplete: function(){}}),确认有实例; - 调用其方法试探:
i.encryptPassword("123456"),看返回值是否为 32 位 hex(MD5)或 64 位(SHA256)。
5.3 第3分钟:编写并运行 hook 脚本
创建hook-login.js:
Java.perform(function () { console.log("[*] Script loaded"); Java.choose("com.shop.security.EncryptUtil", { onMatch: function (instance) { console.log("[+] Found instance: " + instance); var util = Java.use("com.shop.security.EncryptUtil"); util.encryptPassword.overload("java.lang.String").implementation = function (pwd) { console.log("[!] Encrypting password: " + pwd); var result = this.encryptPassword.apply(this, arguments); console.log("[!] Encrypted result: " + result); return result; }; }, onComplete: function () {} }); });运行:frida -U -l hook-login.js com.shop.app
5.4 第4-5分钟:触发与验证
- 在 APP 登录页输入密码
test123,点击登录; - Frida 终端立即输出:
[*] Script loaded [+] Found instance: com.shop.security.EncryptUtil@abcd1234 [!] Encrypting password: test123 [!] Encrypted result: a1b2c3d4e5f6... - 复制
a1b2c3d4e5f6...,用 Burp Suite 抓包对比,确认与网络请求中password字段值一致。
整个过程,从插上手机到看到加密结果,严格计时 4 分 38 秒。这“5分钟”的底气,来自对每一层失败可能的预判和验证手段——不是运气好,而是每一步都有备选方案和快速诊断路径。
最后分享一个小技巧:把上面这个流程固化成一个 shell 脚本frida-hook.sh,传入包名和目标类名作为参数,自动完成环境检查、类枚举、脚本生成、运行,下次 hook 新 APP,真的就是./frida-hook.sh com.bank.app com.bank.crypto.Cipher,回车,喝口咖啡,等结果。技术的价值,从来不是炫技,而是把重复劳动压缩成一次按键。
