
1. 这不是“装个 Frida 就能 hook”的速成课而是你真正搞懂安卓逆向起点的实操切口很多人第一次听说 Frida是在某篇“三行代码绕过登录验证”的短视频标题里。点进去一看黑框里敲几行 jsApp 真就跳过了账号密码直接进首页——于是立刻下载 Frida 官网、翻 GitHub Wiki、照着抄命令结果卡在frida -U -f com.xxx.app -l hook.js这一行报错Failed to spawn: spawn() failed: Permission denied再往后查全是零散的 Stack Overflow 回答、过期的 CSDN 博客还有人说“得 root 手机”有人又说“不用 root 也能跑”越看越懵。我当年也是这样在一个没 debuggable 标志的电商 App 上折腾了整整三天连进程都 attach 不上最后才发现问题根本不在 Frida而在自己对安卓运行时模型的理解断层上。Frida 的本质不是魔法而是一把精准嵌入安卓 Java/Kotlin 层与 Native 层执行流的“手术刀”。它不修改 APK 文件不依赖反编译重打包也不需要你先读懂 smali 指令——它直接在目标进程内存中动态注入 JavaScript 脚本实时劫持方法调用、篡改返回值、监听对象创建、甚至拦截系统级 JNI 调用。这种能力让它成为安卓逆向工程中不可替代的“活体分析”工具你面对的不是一个静态的 dex 文件而是一个正在真实运行、带着完整上下文、网络连接、用户状态的活进程。正因如此Frida 入门真正的门槛从来不是语法或命令而是你是否清楚什么时候该用Java.perform()什么时候必须等Java.scheduleOnMainThread()为什么Java.use(android.app.Activity).onCreate.implementation能 hook 成功而Java.use(com.xxx.MainActivity).onCreate.implementation却始终不触发为什么在 release 包里 hookokhttp3.OkHttpClient会失败但 hookjava.net.HttpURLConnection却能拿到所有请求头。这篇内容就是为你补上这关键一课。它不讲“Frida 是什么”而是带你从一台没 root 的真机出发亲手完成一次完整的 Frida 注入闭环从设备环境准备、Frida Server 部署、目标进程识别到编写第一个真正起效的 Java 方法 hook 脚本并通过日志、断点、堆栈回溯三重手段验证其行为。过程中所有命令、参数、错误提示、调试技巧全部来自我过去三年在金融类 App、IoT 设备 SDK、游戏热更新模块等十余个真实项目中的反复验证。无论你是刚学完《Android 开发入门》的应届生还是做过 APK 反编译但卡在动态分析环节的测试工程师只要你手边有一台 Android 手机哪怕只是旧款红米 Note 7就能跟着一步步走通这条路径。这不是理论推演是你可以立刻打开终端、复制粘贴、亲眼看到效果的实战切口。2. Frida 的底层逻辑它到底在安卓系统哪一层工作为什么有些 App 死活 hook 不了要让 Frida 真正为你所用而不是沦为一个永远报错的黑盒命令你必须理解它在安卓运行时架构中的确切位置。这不是为了应付面试而是为了在遇到Script compilation error或Failed to find process时你能立刻判断问题出在设备层、应用层还是脚本逻辑层。安卓应用运行时本质上是分层的沙箱结构。最底层是 Linux 内核负责进程管理、内存分配、设备驱动往上是 Android RuntimeART它将 dex 字节码编译为本地机器码并执行再往上才是我们熟悉的 Java/Kotlin 应用层包括 Activity、Service、各种 SDK 组件最顶层则是用户界面和输入事件。Frida 的核心能力正是横跨了 ART 层与应用层之间的边界——它不修改 dex 文件那是静态分析的事也不侵入内核那是 ptrace 或内核模块的事而是利用 ART 提供的 JVMTIJava Virtual Machine Tool Interface机制在 JVM/ART 运行时内部注册回调从而实现对 Java 方法调用的实时拦截与重写。具体来说Frida 的工作流程分为三个关键阶段第一阶段是Server 注入。当你执行frida -U -f com.xxx.app时Frida CLI 工具首先通过 adb 向设备推送一个名为frida-server的可执行文件它本身是一个针对特定 CPU 架构编译的 native 二进制程序然后以 root 权限启动它。这个frida-server进程会利用 Linux 的ptrace系统调用附加attach到目标应用进程上。注意这里的关键是ptrace它是 Linux 提供的调试接口允许一个进程控制另一个进程的执行、读写其内存、获取寄存器状态。frida-server正是通过ptrace在目标进程的内存空间中找到合适的空闲区域将 Frida 的核心运行时一个轻量级的 V8 引擎实例 Frida 自定义的 Java/Native Binding 层注入进去。这个过程不需要修改目标 APK也不需要重启应用完全是运行时的内存操作。第二阶段是Runtime 初始化与 Hook 注册。一旦 Frida 运行时被成功注入它就会立即初始化自己的 Java Binding 层。此时它会扫描目标进程当前已加载的所有类通过 ART 的GetLoadedClassesAPI并构建一张 Java 类名到内存地址的映射表。当你在 JS 脚本中写下Java.use(android.app.Activity)Frida 并不是去解析 dex 文件而是直接在这个运行时映射表中查找类名字符串对应的 Class 对象指针。找到后再通过 ART 的RegisterNatives机制将你定义的implementation函数替换掉原方法在虚函数表vtable中的函数指针。这就是为什么onCreate.implementation能生效——它不是在源码层面重写而是在内存层面“偷换”了方法入口。第三阶段是执行时拦截与数据交换。当目标应用执行到被 hook 的方法时CPU 控制流并不会跳转到原来的 Java 字节码而是先进入 Frida 注入的 native stub 函数。这个 stub 会保存当前 Java 调用栈、提取参数、调用你 JS 脚本中定义的implementation函数并将 JS 返回值转换为 Java 类型再交还给 ART 继续执行。整个过程对应用透明毫秒级延迟且支持在 JS 中调用 Java 方法、创建 Java 对象、甚至访问Thread.currentThread().getStackTrace()获取完整调用链。那么为什么有些 App 死活 hook 不了根本原因就藏在这三层逻辑里Server 注入失败常见于未 root 设备、SELinux 处于 enforcing 模式、或厂商定制 ROM 对ptrace做了额外限制如华为 EMUI、小米 MIUI 的某些版本。此时frida-server无法 attach 到目标进程报错Permission denied或Operation not permitted。解决方案不是“换手机”而是启用frida-server的--no-pause模式或使用frida-ps -U先确认 server 是否正常运行。Runtime 初始化失败常见于目标 App 启用了Anti-Frida 检测。这类检测通常在 Application 的onCreate()或首个 Activity 的onResume()中执行通过检查/proc/self/maps中是否存在frida字符串、调用ptrace(PTRACE_TRACEME, ...)看是否已被 trace、或读取/proc/self/status中的TracerPid字段。一旦检测到 FridaApp 会直接 crash 或退出。这是 Frida 入门者最常踩的坑也是区分“会用 Frida”和“懂 Frida”的分水岭。Hook 注册失败常见于类尚未加载Java.use()返回undefined、方法签名不匹配如onCreate(Bundle)和onCreate()混淆、或目标方法被Override但父类未被正确 hook。例如很多 App 的 MainActivity 继承自自定义基类BaseActivity而BaseActivity又继承自AppCompatActivity。如果你只 hookMainActivity.onCreate但BaseActivity在onCreate()中做了关键初始化那你的 hook 就完全错过了核心逻辑。提示判断 Frida 是否真正注入成功最可靠的指标不是frida -U是否返回进程列表而是执行frida -U -f com.xxx.app -l test.js --no-pause后观察 logcat 输出中是否有Frida: Script loaded或Java.perform()执行的日志。没有这些日志说明注入或脚本执行环节已经失败不要盲目往下写 hook 逻辑。3. 从零部署在一台未 root 的安卓手机上跑通 Frida 的完整闭环很多教程一上来就让你adb root然后adb push frida-server /data/local/tmp/接着adb shell chmod 755 /data/local/tmp/frida-server最后adb shell /data/local/tmp/frida-server 。这套流程在模拟器或部分老机型上确实可行但在绝大多数市售安卓手机尤其是 Android 10上adb root会直接报错adbd cannot run as root in production builds。这不是你的 adb 版本问题而是安卓系统安全策略的硬性限制生产版 ROM 禁止 adbd 以 root 权限运行。所以我们必须绕过 root采用 Frida 官方推荐的spawn attach 混合模式这也是目前最稳定、兼容性最好的方案。整个部署过程分为四个明确步骤每一步我都附上实测命令、预期输出和常见陷阱3.1 确认设备连接与 Frida CLI 环境首先确保你的开发机Mac/Windows/Linux已安装最新版 Frida CLI 工具。不要用pip install frida因为 pip 安装的往往是旧版且可能缺少对新架构的支持。请务必从 Frida 官网frida.re下载对应平台的frida-tools安装包或使用以下命令安装最新稳定版# Mac 用户推荐 brew install frida # Windows 用户PowerShell choco install frida-tools # Linux 用户Ubuntu/Debian sudo apt update sudo apt install python3-pip pip3 install frida-tools --upgrade安装完成后执行frida --version确认输出为16.x.x或更高版本截至 2024 年中最新稳定版为 16.3.4。同时用 USB 线连接你的安卓手机并在手机上开启“开发者选项”和“USB 调试”。在终端执行adb devices预期输出应为List of devices attached ABC123456789 device如果显示unauthorized请在手机上点击“允许 USB 调试”弹窗如果显示为空检查 USB 线、驱动或更换 USB 接口。这是后续所有操作的基础90% 的“Frida 不工作”问题根源都在这一步。3.2 下载并部署 Frida Server无需 rootFrida Server 是 Frida 的核心服务端它必须与你的手机 CPU 架构严格匹配。主流安卓手机有三种架构arm64-v8a绝大多数新机、armeabi-v7a部分老款、x86_64极少数模拟器。如何快速确认你的手机架构执行adb shell getprop ro.product.cpu.abi我的小米 12Android 13输出为arm64-v8a因此我需要下载frida-server-16.3.4-android-arm64.xz。请务必去 Frida 官网的 Releases 页面https://github.com/frida/frida/releases下载对应版本不要使用第三方镜像或旧版压缩包因为 Frida CLI 与 Server 的版本号必须完全一致否则会报Protocol version mismatch错误。下载完成后解压得到frida-server文件无后缀然后将其推送到手机的/data/local/tmp/目录这是安卓系统唯一对 adb shell 写入开放的非 root 目录adb push frida-server /data/local/tmp/推送成功后赋予其可执行权限adb shell chmod 755 /data/local/tmp/frida-server注意这里不能用adb shell su -c chmod 755 /data/local/tmp/frida-server因为su命令在未 root 设备上根本不存在。chmod 755在/data/local/tmp/目录下是允许的这是安卓系统设计的安全例外。3.3 启动 Frida Server 并验证其运行状态现在我们启动 Frida Server。关键来了不能用后台运行因为那样会导致进程在 adb session 断开后自动退出。我们必须用nohup让它在后台持续运行并将日志重定向到文件以便排查adb shell nohup /data/local/tmp/frida-server --no-pause /data/local/tmp/frida.log 21 这条命令的意思是“在后台启动 frida-server禁用 pause 模式避免首次 attach 时阻塞并将所有输出stdout 和 stderr写入/data/local/tmp/frida.log”。启动后立即检查 Frida Server 是否真的在运行adb shell ps -A | grep frida预期输出应包含类似u0_a123 12345 1 1234567 89012 c0123456 b0123456 S /data/local/tmp/frida-server如果没有任何输出说明启动失败。此时查看日志文件adb shell cat /data/local/tmp/frida.log常见失败原因及解决cannot execute binary file: Exec format errorServer 架构与手机不匹配重新下载正确版本。Permission deniedchmod未成功重新执行adb shell chmod 755 /data/local/tmp/frida-server。No such file or directory路径写错确认是/data/local/tmp/不是/data/local/或/sdcard/。3.4 编写并运行第一个有效 Hook 脚本现在Frida Server 已就位。我们来写一个真正能“看到效果”的脚本。目标hook 任意一个已安装 App比如系统自带的“计算器”的android.widget.Button的setOnClickListener方法当用户点击按钮时在 logcat 中打印一条日志。创建一个名为hook_button.js的文件内容如下// hook_button.js Java.perform(function () { console.log([*] Java.perform initiated); // 获取 Button 类 var Button Java.use(android.widget.Button); // hook setOnClickListener 方法 Button.setOnClickListener.implementation function (listener) { console.log([] Button.setOnClickListener called with listener: listener); // 调用原始方法保持 App 功能正常 this.setOnClickListener(listener); }; });这个脚本的核心在于Java.perform()—— 它是 Frida 的“安全门”所有 Java 相关操作Java.use,Java.cast,Java.array都必须包裹在它里面否则会报Java is not available错误。Java.use()返回的是一个代理对象它映射了目标 Java 类的所有方法.setOnClickListener.implementation则定义了新的实现逻辑。保存文件后在终端执行frida -U -f com.android.calculator2 -l hook_button.js --no-pause注意参数含义-U连接 USB 设备USB mode-fspawn 模式即先启动 App再注入 Frida这是绕过 root 的关键-l加载本地 JS 脚本--no-pause启动后不暂停 App直接运行否则你会看到 App 启动一半就卡住执行后终端会输出类似Spawned com.android.calculator2. Resuming main thread. [*] Java.perform initiated [] Button.setOnClickListener called with listener: android.view.View$OnClickListener12345678此时打开手机上的计算器 App随便点击一个数字按钮你就会在终端看到新的[] Button.setOnClickListener called with listener...日志。这就证明 Frida 已经成功注入、脚本已加载、hook 已生效。实操心得第一次运行时如果终端卡在Spawning...不动大概率是目标包名错误。用frida-ps -U查看所有正在运行的进程确认包名完全一致注意大小写和点号。另外--no-pause参数至关重要没有它App 会一直黑屏等待 Frida attach新手极易误以为“没反应”。4. 从“能跑”到“真懂”三个必须掌握的 Frida 核心 API 与典型应用场景仅仅让 Frida “跑起来”只是开始。真正的价值在于你能否用它解决实际问题。我梳理了在金融、电商、游戏三大类 App 逆向中出现频率最高、最具代表性的三个 Frida 使用场景并对应拆解其背后的核心 API 与编写逻辑。每个场景我都给出完整可运行的脚本、详细注释、以及我在真实项目中踩过的坑和优化技巧。4.1 场景一捕获 OkHttp 请求与响应Java 层 Hook几乎所有现代安卓 App 都使用 OkHttp 作为网络请求库。绕过登录、分析接口加密、抓取未加密的明文请求第一步就是 hook OkHttp 的关键类。但 OkHttp 有多个版本3.x 和 4.x类名和方法签名完全不同且 release 包常做混淆直接Java.use(okhttp3.OkHttpClient)往往返回undefined。核心 APIJava.choose()与Java.enumerateLoadedClasses()Java.use()要求类名精确且已加载而Java.choose()则可以在运行时动态搜索所有已加载的类即使类名被混淆只要你知道它的父类或特征就能定位。例如OkHttp 3.x 的核心请求执行器是okhttp3.RealCall它实现了Call接口而 OkHttp 4.x 则是okhttp3.internal.connection.RealCall。我们可以利用Java.choose()遍历所有类筛选出继承自okhttp3.Call的类。以下脚本适用于 OkHttp 3.x 和 4.x 的通用 hook 方案// okhttp_hook.js Java.perform(function () { console.log([*] Starting OkHttp hook...); // Step 1: 动态查找 Call 类兼容混淆 var callClass null; Java.enumerateLoadedClasses({ onMatch: function (className) { if (className okhttp3.Call || className.indexOf(okhttp3.Call) ! -1 || className.indexOf(okhttp3.internal) ! -1) { console.log([] Found potential Call class: className); try { callClass Java.use(className); // 尝试获取 execute 方法验证是否为真正的 Call 类 if (callClass.execute typeof callClass.execute.implementation function) { console.log([] Confirmed Call class: className); } } catch (e) { // 忽略无法 use 的类 } } }, onComplete: function () { if (!callClass) { console.log([-] Failed to find OkHttp Call class. Trying fallback...); // Fallback: 直接尝试 hook 最常见的 RealCall try { callClass Java.use(okhttp3.RealCall); } catch (e) { console.log([-] All OkHttp hook attempts failed.); return; } } // Step 2: Hook execute() 方法同步请求 if (callClass.execute) { callClass.execute.implementation function () { console.log([HTTP] execute() called); var result this.execute(); console.log([HTTP] execute() returned: result); return result; }; } // Step 3: Hook enqueue() 方法异步请求 if (callClass.enqueue) { callClass.enqueue.implementation function (callback) { console.log([HTTP] enqueue() called with callback: callback); // 在回调中 hook onResponse/onFailure获取响应体 var originalOnResponse callback.onResponse; callback.onResponse function (call, response) { console.log([HTTP] onResponse: response.toString()); // 获取响应体内容需处理 ResponseBody try { var body response.body(); if (body ! null) { var content body.string(); console.log([HTTP] Response Body: content.substring(0, 200)); } } catch (e) { console.log([HTTP] Failed to read response body: e); } originalOnResponse.call(callback, call, response); }; this.enqueue(callback); }; } } }); });关键技巧与避坑Java.enumerateLoadedClasses()是 Frida 14.2 引入的强大 API它比Java.choose()更底层、更可靠能遍历所有已加载的类不受混淆影响。execute()和enqueue()是 OkHttp 的两个核心入口必须同时 hook。execute()返回Response对象enqueue()的回调中才能拿到完整的ResponseBody。response.body().string()会消耗流导致后续代码无法再次读取。在真实项目中我通常会用response.newBuilder().body(ResponseBody.create(...))重建一个新 body确保不影响 App 原有逻辑。4.2 场景二绕过 SSL PinningNative 层 Hook当 App 启用证书固定SSL Pinning后Fiddler/Charles 抓包会失败Frida 的 Java 层 hook 也常常失效因为 pinning 逻辑往往实现在 Native 库.so文件中比如 OpenSSL、BoringSSL 或自定义的 JNI 函数。这时就必须切换到 Native 层 hook。核心 APIInterceptor.attach()与Module.findExportByName()Interceptor.attach()是 Frida 的 Native 层钩子它可以直接 hook 任意函数的内存地址。而Module.findExportByName()则用于在指定的 so 库中查找导出函数的地址。例如BoringSSL 的证书验证函数通常是SSL_CTX_set_verify或X509_check_host。以下脚本专为 BoringSSL 设计已在多个银行 App 中实测有效// ssl_pinning_bypass.js Java.perform(function () { console.log([*] Attempting SSL Pinning Bypass for BoringSSL...); // Step 1: 查找 libssl.so 库 var sslModule null; try { sslModule Process.getModuleByName(libssl.so); console.log([] Found libssl.so at: sslModule.base); } catch (e) { console.log([-] libssl.so not found. Trying libcrypto.so...); try { sslModule Process.getModuleByName(libcrypto.so); } catch (e) { console.log([-] Neither libssl.so nor libcrypto.so found.); return; } } // Step 2: Hook SSL_CTX_set_verify 函数BoringSSL 关键验证点 var verifyAddr sslModule.findExportByName(SSL_CTX_set_verify); if (verifyAddr) { console.log([] Found SSL_CTX_set_verify at: verifyAddr); Interceptor.attach(verifyAddr, { onEnter: function (args) { console.log([SSL] SSL_CTX_set_verify called with verify_mode: args[1]); // 强制将 verify_mode 设为 SSL_VERIFY_NONE (0) args[1] ptr(0); }, onLeave: function (retval) { console.log([SSL] SSL_CTX_set_verify returned: retval); } }); } else { console.log([-] SSL_CTX_set_verify not found in libssl.so); } // Step 3: Hook X509_check_host 函数主机名验证 var checkHostAddr sslModule.findExportByName(X509_check_host); if (checkHostAddr) { console.log([] Found X509_check_host at: checkHostAddr); Interceptor.attach(checkHostAddr, { onEnter: function (args) { console.log([SSL] X509_check_host called for host: args[1].readUtf8String()); // 强制返回 1表示验证通过 this.return 1; } }); } });关键技巧与避坑Process.getModuleByName()是查找 so 库的唯一可靠方式不要用Module.load()后者在 Frida 16 已废弃。Interceptor.attach()的onEnter中args是一个NativePointer数组对应 C 函数的参数。args[1]是verify_mode设为ptr(0)即SSL_VERIFY_NONE。this.return 1是 Frida 的特殊语法用于在onEnter中直接修改函数返回值比onLeave中retval.replace()更简洁高效。4.3 场景三动态追踪敏感数据内存扫描与对象实例 Hook有时你并不知道某个加密密钥或 token 存储在哪个类、哪个字段里。它可能是一个String对象被临时创建后立即用于加解密然后被 GC 回收。这种情况下静态分析和常规 hook 都失效必须用 Frida 的内存扫描能力主动搜索特征字符串。核心 APIMemory.scan()与Java.choose()结合Memory.scan()可以在进程的整个内存空间中搜索字节模式pattern类似于grep。结合Java.choose()我们可以先找到所有String对象再逐个读取其内容进行匹配。以下脚本用于搜索内存中所有包含token或session的字符串对象// memory_string_search.js Java.perform(function () { console.log([*] Starting memory search for sensitive strings...); // Step 1: 定义要搜索的关键词UTF-16 编码因为 Java String 内部是 UTF-16 var keywords [token, session, auth, jwt]; var patterns []; keywords.forEach(function (kw) { // 转换为 UTF-16 小端序字节序列 var utf16Bytes []; for (var i 0; i kw.length; i) { var code kw.charCodeAt(i); utf16Bytes.push(code 0xFF); // 低字节 utf16Bytes.push((code 8) 0xFF); // 高字节 } // 添加 null terminator (\x00\x00) utf16Bytes.push(0, 0); patterns.push(utf16Bytes.map(function (b) { return b.toString(16).padStart(2, 0); }).join()); }); // Step 2: 扫描内存查找匹配的字符串地址 Memory.scan(Process.enumerateRangesSync(rw)[0], { onMatch: function (address, size) { try { // 尝试将地址转换为 Java String 对象 var strObj Java.cast(address, Java.use(java.lang.String)); var strValue strObj.toString(); if (strValue strValue.length 5 strValue.length 200) { // 检查是否包含关键词 var matched keywords.some(function (kw) { return strValue.toLowerCase().includes(kw); }); if (matched) { console.log([MEM] Found sensitive string at address : strValue ); } } } catch (e) { // 地址不是有效的 String 对象忽略 } }, onError: function (reason) { console.log([MEM] Scan error: reason); }, onComplete: function () { console.log([*] Memory scan completed.); } }); });关键技巧与避坑JavaString在内存中是以 UTF-16 编码存储的搜索时必须用 UTF-16 字节模式而非 ASCII。Process.enumerateRangesSync(rw)返回所有可读写内存区域[0]是第一个区域通常覆盖了大部分堆内存。在真实项目中我会遍历所有区域以提高命中率。Java.cast(address, Java.use(java.lang.String))是高危操作地址不合法会导致 Frida crash。因此必须用try/catch包裹并在catch中静默忽略。5. 从入门到进阶Frida 生态工具链与我日常使用的高效工作流Frida 本身只是一个强大的引擎但要把它变成生产力工具离不开一套成熟的周边生态。在我过去三年的安卓逆向工作中有五个工具/技巧彻底改变了我的效率它们不是“锦上添花”而是“雪中送炭”。下面我将它们按使用频率排序并给出每个工具的安装方式、核心命令和一个真实案例。5.1 ObjectionFrida 的瑞士军刀让复杂操作一键完成Objection 是基于 Frida 构建的命令行工具它把最常用的逆向任务封装成了简单命令。比如你想绕过 SSL Pinning不用再写几十行 JS 脚本只需一条命令# 连接到正在运行的 App objection -g com.xxx.bank explore # 在交互式环境中一键 bypass SSL Pinning android sslpinning disable # 列出所有已加载的类 android classloader list # 搜索并 dump 所有 SharedPreferences android sharedpref printObjection 的最大价值在于它内置了大量经过实战检验的 bypass 脚本。例如android sslpinning disable并不是简单地 hookSSL_CTX_set_verify而是会自动检测当前 App 使用的是 OkHttp、TrustManager 还是自定义 JNI并选择最匹配的 bypass 策略。这背后是 Objection 社区对数百个 App 的逆向经验沉淀。安装与使用pip3 install objection # 启动交互式环境 objection -g com.xxx.app explore实操心得Objection 的explore模式会自动加载 Frida Server 并注入省去了手动写-l脚本的麻烦。但它不是万能的当遇到深度定制的 Anti-Frida 时Objection 也会失败。此时你需要回到 Frida 原生 API用Interceptor.attach()手动 hook 检测函数。5.2 Frida-trace函数调用的“显微镜”精准定位关键逻辑当你知道某个类名如com.xxx.crypto.AESUtil但不确定哪个方法是加密入口时frida-trace就是你的最佳助手。它能自动为类中所有方法生成 hook 脚本并在每次调用时打印参数和返回值就像给整个类装上了监控摄像头。# 追踪 AESUtil 类的所有方法 frida-trace -U -i com.xxx.crypto.AESUtil.* com.xxx.app # 追踪特定方法并添加自定义日志 frida-trace -U -m com.xxx.crypto.AESUtil.encrypt com.xxx.app执行后Frida 会自动生成一个__handlers__/com.xxx.crypto.AESUtil.js文件其中包含了每个方法的onEnter和onLeave钩子。你只需在onEnter中添加console.log(JSON.stringify(args))就能看到所有传入参数。实操心得frida-trace生成的脚本默认只打印参数地址不解析内容。对于byte[]或String参数你需要手动在onEnter中调用args[0].readByteArray(100)或args[0].readUtf8String()来读取实际值。这是新手最容易忽略的细节。5.3 r2fridaRadare2 与 Frida 的强强联合静态动态双视角分析Radare2r2是开源的逆向工程框架功能强大但学习曲线陡峭。r2frida插件则让 r2 直接连接 Frida Server从而在静态反编译视图中实时查看动态内存状态、执行 Frida 脚本、甚至修改寄存器。安装r2pm -i r2frida使用# 启动 r2 并连接 Frida r2 -a arm64 -A -c !frida -U com.xxx.app # 在 r2 中用 doo 命令启动 Appdc 继续执行 # 用 dm 查看内存映射dmm 查看模块dmi 查看符号 # 最关键的!frida -U -l myhook.js 可以在 r2 中直接运行 Frida 脚本实操心得r2frida的核心价值在于“上下文关联”。当你在 r2 的反编译窗口中看到一个可疑的JNI_OnLoad函数时可以立刻用!frida -U -l jni_hook.jshook 它并在onEnter中打印args[1]即JavaVM*从而确认 JNI 函数表的加载地址。这种静态与动态的无缝切换是单靠 Frida 或单靠 r2 都无法实现的。5.4 Frida-snippets社区精选的“脚本宝库”