Frida Android
动态插桩实战
从零搭建 Frida 环境,掌握 Java/Native Hook、内存读写、SSL Pinning 绕过等核心技术,覆盖攻防两端完整链路。
Frida 是什么?
Frida 是一款开源的动态二进制插桩(Dynamic Binary Instrumentation,DBI)框架,支持 Android、iOS、Windows、Linux、macOS。它允许你在运行时注入 JavaScript 代码到目标进程,实时 Hook 函数、修改参数和返回值、读写内存、追踪执行流,而无需修改 APK 或重新编译。
| 工具 | 方式 | 优点 | 缺点 |
|---|---|---|---|
| Frida | 动态插桩 / 运行时 | 无需修改 APK、实时交互、脚本灵活 | 需要 root 或重打包;容易被检测 |
| JADX / APKTool | 静态反编译 | 离线分析、无需设备 | 混淆难读,无法分析运行时状态 |
| Xposed | 框架级 Hook | 持久化、系统级 | 需要 Xposed 框架支持,重启生效 |
| objection | Frida 封装 | 一键操作,适合快速渗透测试 | 功能较固定,定制性不如纯 Frida |
架构与原理
Frida 采用 C/S 架构,由运行在 PC 上的客户端(frida-tools)和运行在 Android 设备上的 Server(frida-server)组成。
┌──────────────────────────────────┐ ADB / USB / TCP
│ PC (Host) │◄─────────────────────────────►│
│ │ │
│ frida (Python CLI) │ ┌─────┴──────────────────────────┐
│ frida-tools / objection │ │ Android Device │
│ 自定义 Python 脚本 │ │ │
│ │ │ frida-server (root) │
│ ┌──────────────────────┐ │ │ │ │
│ │ JavaScript Engine │ │ │ ▼ │
│ │ (QuickJS / V8) │◄── 双向RPC ──────────────────► │ ptrace → 注入 frida-agent │
│ └──────────────────────┘ │ │ │ │
└──────────────────────────────────┘ │ ▼ │
│ 目标进程 (App) │
│ ├─ Java Runtime (ART) │
│ ├─ .so Native Libs │
│ └─ Hook 生效 ✓ │
└────────────────────────────────┘
注入原理
Frida Server 通过 ptrace 系统调用附加到目标进程,在进程内存中注入 frida-agent 动态库。Agent 内嵌 QuickJS(或 V8)引擎,执行你传入的 JavaScript 脚本。对于 Java 层,它通过 ART 内部 API(JNI / jvmti)实现 Hook;对于 Native 层,它通过修改 PLT/GOT 表或 inline hook(覆盖函数头部指令)来重定向调用。
libfrida-gadget.so 打包进 APK 并在 AndroidManifest.xml 的 Application 中添加 extractNativeLibs=true,再重新签名安装。Gadget 会在 App 启动时自动初始化,监听本地端口等待连接。
环境搭建
1. 安装 frida-tools(PC 端)
# 推荐用 pipx 或 venv 隔离环境 pip3 install frida-tools # 包含 frida-ps、frida-trace 等命令行工具 pip3 install frida # Python 绑定(用于写脚本) # 验证安装 frida --version # → 16.x.x
2. 下载并推送 frida-server(Android 端)
# 查看设备 CPU 架构 adb shell getprop ro.product.cpu.abi # 常见值: arm64-v8a / armeabi-v7a / x86_64 # 前往 https://github.com/frida/frida/releases 下载对应版本 # 例: frida-server-16.x.x-android-arm64.xz xz -d frida-server-16.x.x-android-arm64.xz # 推送到设备 adb push frida-server-16.x.x-android-arm64 /data/local/tmp/frida-server adb shell chmod +x /data/local/tmp/frida-server # 启动(需要 root) adb shell su -c "/data/local/tmp/frida-server &" # PC 端验证:列出所有进程 frida-ps -U # -U = USB 设备 frida-ps -U -a # -a = 仅列出 App
3. ADB 端口转发(WiFi 调试)
adb forward tcp:27042 tcp:27042 # Frida 默认端口 frida-ps -H 127.0.0.1 # 通过 TCP 连接
adb shell setenforce 0(临时关闭 SELinux)。在模拟器(如 Android Studio AVD、Genymotion)上调试时更方便,推荐使用无 Google Play 的镜像,root 权限开箱即用。
4. 常用命令速查
| 命令 | 说明 |
|---|---|
frida-ps -Ua | 列出 USB 设备上运行的 App 进程 |
frida -U -f com.pkg.name -l script.js | spawn 模式:启动 App 并注入脚本 |
frida -U -n "App Name" -l script.js | attach 模式:附加到已运行的进程 |
frida -U -p 1234 -l script.js | 按 PID 附加 |
frida-trace -U -i "recv*" -f com.pkg | 自动 trace 匹配函数 |
objection -g com.pkg explore | 进入 objection 交互 Shell |
Hook Java 方法
所有 Java Hook 操作必须包裹在 Java.perform() 回调内,确保在 ART 完全初始化后执行。
基础 Hook 模板
Java.perform(function () { // 1. 获取目标类 var TargetClass = Java.use('com.example.app.TargetClass'); // 2. Hook 指定方法(注意重载) TargetClass.checkPassword.implementation = function (pwd) { // this = Java 对象实例 console.log('[*] checkPassword called, arg: ' + pwd); // 调用原始方法 var result = this.checkPassword(pwd); console.log('[*] original result: ' + result); // 强制返回 true(绕过验证) return true; }; });
处理方法重载(Overload)
Java.perform(function () { var Utils = Java.use('com.example.Utils'); // 通过 overload() 指定参数类型签名来选择具体重载 Utils.encrypt.overload('java.lang.String').implementation = function (data) { console.log('encrypt(String): ' + data); return this.encrypt(data); }; Utils.encrypt.overload('java.lang.String', '[B').implementation = function (data, key) { // '[B' = byte[] 类型签名 console.log('encrypt(String, byte[]): data=' + data + ' key=' + key); return this.encrypt(data, key); }; });
Hook 构造函数
Java.perform(function () { var MyClass = Java.use('com.example.MyClass'); // $init 代表构造函数 MyClass.$init.overload('java.lang.String', 'int').implementation = function (name, age) { console.log('[ctor] name=' + name + ' age=' + age); // 打印调用栈 console.log(Java.use('android.util.Log').getStackTraceString( Java.use('java.lang.Exception').$new() )); this.$init(name, age); // 调用原始构造 }; });
主动调用 Java 方法(无需等待 Hook 触发)
Java.perform(function () { var Crypto = Java.use('com.example.CryptoHelper'); // 调用静态方法 var result = Crypto.md5('hello_world'); console.log('md5 result: ' + result); // 实例化对象并调用实例方法 var obj = Crypto.$new('AES'); console.log('instance method: ' + obj.getAlgorithm()); // 修改私有字段(反射方式) var field = Crypto.class.getDeclaredField('secretKey'); field.setAccessible(true); field.set(obj, 'my_custom_key'); });
枚举所有实例(Instance Enumeration)
Java.perform(function () { // 枚举堆内存中所有 Activity 实例(可用于获取 Context) Java.choose('android.app.Activity', { onMatch: function (instance) { console.log('Found activity: ' + instance.getClass().getName()); }, onComplete: function () { console.log('Enumeration complete'); } }); });
function hookAllMethods(className) { Java.perform(function () { var clazz = Java.use(className); var methods = clazz.class.getDeclaredMethods(); methods.forEach(function (method) { var name = method.getName(); try { clazz[name].overloads.forEach(function (overload) { overload.implementation = function () { var args = Array.from(arguments).map(a => JSON.stringify(a)).join(', '); console.log(`[HOOK] ${className}.${name}(${args})`); return overload.apply(this, arguments); }; }); } catch (e) { /* 忽略无法 Hook 的方法 */ } }); console.log(`[*] Hooked all methods of ${className}`); }); } hookAllMethods('com.example.app.PaymentManager');
Hook Native(.so 函数)
许多 App 将加密算法、授权验证、反作弊逻辑写在 Native 层(C/C++ 编译的 .so 文件)。Frida 提供 Interceptor 和 NativeFunction API 处理 Native Hook。
按导出名称 Hook
// Hook libc 的 open 系统调用(可用于追踪文件访问) var openPtr = Module.getExportByName('libc.so', 'open'); Interceptor.attach(openPtr, { onEnter: function (args) { // args[0] = const char* pathname this.path = args[0].readUtf8String(); console.log('open() path: ' + this.path); }, onLeave: function (retval) { // retval = 文件描述符,-1 表示失败 console.log('open() fd = ' + retval.toInt32()); } });
按内存地址 Hook(配合 JADX / Ghidra 分析)
// 先获取 .so 在内存中的基地址(ASLR 每次运行会变化) var baseAddr = Module.findBaseAddress('libapp.so'); console.log('libapp.so base: ' + baseAddr); // Ghidra 中分析出偏移量 0x12A40(函数 validate_license 的 RVA) var funcAddr = baseAddr.add(0x12A40); Interceptor.attach(funcAddr, { onEnter: function (args) { console.log('validate_license called, arg0=' + args[0]); // ARM64: args[0..7] → 寄存器 x0..x7 }, onLeave: function (retval) { console.log('original retval: ' + retval.toInt32()); retval.replace(1); // 强制返回 1(成功) } });
主动调用 Native 函数
// 声明 Native 函数签名:返回类型 + 参数类型数组 var strlen = new NativeFunction( Module.getExportByName('libc.so', 'strlen'), 'size_t', // 返回类型 ['pointer'] // 参数类型 ); var str = Memory.allocUtf8String('hello frida'); console.log('strlen = ' + strlen(str)); // → 11
枚举所有已加载模块与导出
// 列出所有已加载 .so Process.enumerateModules().forEach(function (mod) { console.log(mod.name + ' @ ' + mod.base + ' size=' + mod.size); }); // 列出指定 .so 的所有导出函数 Module.enumerateExports('libssl.so').forEach(function (exp) { if (exp.name.includes('SSL_CTX')) { console.log(exp.name + ' @ ' + exp.address); } });
内存操作
读写内存
var ptr = ptr('0x7f1234abcd'); // 从地址字符串构造指针 // 读操作 ptr.readU8(); // 读 1 字节无符号整数 ptr.readU32(); // 读 4 字节 ptr.readU64(); // 读 8 字节 ptr.readFloat(); // 读浮点数 ptr.readPointer(); // 读指针(平台位宽) ptr.readUtf8String(); // 读 C 字符串 ptr.readByteArray(16); // 读 16 字节原始数据(返回 ArrayBuffer) // 写操作 Memory.protect(ptr, 0x1000, 'rwx'); // 先确保内存可写 ptr.writeU32(0xDEADBEEF); ptr.writeByteArray([0x90, 0x90, 0x90]); // NOP patch ptr.writeUtf8String('patched!'); // 分配内存(Frida 管理生命周期) var buf = Memory.alloc(256); // 分配 256 字节 var strBuf = Memory.allocUtf8String('test'); // 分配字符串
内存扫描(搜索特征字节)
// 在 libapp.so 内存范围内搜索特定字节序列(?? = 通配符) var mod = Process.getModuleByName('libapp.so'); Memory.scan(mod.base, mod.size, '48 65 6c 6c ?? ?? 57 6f 72 6c 64', { onMatch: function (address, size) { console.log('Pattern found @ ' + address); console.log(hexdump(address, { length: 32, ansi: true })); }, onComplete: function () { console.log('Scan complete'); } });
hexdump 工具
// hexdump 是 Frida 全局函数,可直接使用 console.log(hexdump(ptr, { offset: 0, length: 64, header: true, ansi: true })); // 输出类似: // 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF // 48 65 6c 6c 6f 00 57 6f 72 6c 64 00 00 00 00 00 Hello.World.....
Interceptor API 详解
Interceptor 是 Frida 用于 Native Hook 的核心 API,支持 inline hook(替换函数头部指令)和 call replacement(完全替换函数实现)。
Interceptor.replace(完全替换)
var isRootedAddr = Module.getExportByName('libsec.so', 'isRooted'); Interceptor.replace(isRootedAddr, new NativeCallback(function () { console.log('[*] isRooted() hooked → returning false'); return 0; // 0 = false,非 root 设备 }, 'int', [])); // 返回类型 int,无参数
追踪 JNI 调用
// 追踪 Native 代码对 Java 层的所有 JNI 调用 var jni_env = Java.vm.getEnv(); var findClass = jni_env.handle.readPointer().add(6 * Process.pointerSize).readPointer(); Interceptor.attach(findClass, { onEnter: function(args) { console.log('FindClass: ' + args[1].readUtf8String()); } });
绕过 SSL Pinning
SSL Pinning(证书固定)是 App 在 TLS 握手时校验服务器证书与内置证书是否匹配,防止中间人攻击。对于安全研究,我们需要绕过它,让 Burp Suite / Charles 等代理工具能解密 HTTPS 流量。
方法一:Hook OkHttp3 CertificatePinner
Java.perform(function () { // OkHttp3 证书固定核心类 var CertificatePinner = Java.use('okhttp3.CertificatePinner'); // check(String hostname, List peerCertificates) — 原始校验逻辑 CertificatePinner.check.overload('java.lang.String', 'java.util.List') .implementation = function (hostname, certs) { console.log('[SSL] CertificatePinner.check bypassed for: ' + hostname); // 直接返回,不抛出 SSLPeerUnverifiedException }; // check(String hostname, Certificate... peerCertificates)(旧版重载) CertificatePinner.check.overload('java.lang.String', '[Ljava.security.cert.Certificate;') .implementation = function (hostname, certs) { console.log('[SSL] CertificatePinner.check (varargs) bypassed for: ' + hostname); }; });
方法二:Hook TrustManager(通用方案)
Java.perform(function () { // 替换 TrustManager:让所有证书都被信任 var TrustManager = Java.registerClass({ name: 'com.frida.bypass.TrustManager', implements: [Java.use('javax.net.ssl.X509TrustManager')], methods: { checkClientTrusted: function (chain, authType) {}, checkServerTrusted: function (chain, authType) {}, // 不抛异常 = 信任所有 getAcceptedIssuers: function () { return []; } } }); var SSLContext = Java.use('javax.net.ssl.SSLContext'); var ctx = SSLContext.getInstance('TLS'); ctx.init(null, [TrustManager.$new()], null); SSLContext.setDefault(ctx); console.log('[*] TrustManager replaced successfully'); });
方法三:使用 objection 一键绕过
# 启动 App 并注入 objection -g com.example.app explore # 在 objection shell 内执行 android sslpinning disable # 同时可以: # android root disable ← 禁用 Root 检测 # android hooking list classes # android hooking watch class com.example.PaymentService
常见 SSL Pinning 实现 & 对应 Hook 点
| 框架 / 实现 | 关键类 / 方法 | Hook 策略 |
|---|---|---|
| OkHttp3 | okhttp3.CertificatePinner#check | 覆盖 implementation 为空函数 |
| Android 原生 | javax.net.ssl.X509TrustManager#checkServerTrusted | 替换 TrustManager 实例 |
| Retrofit | 内部使用 OkHttp,同上 | 同 OkHttp3 |
| Conscrypt / Cronet | org.conscrypt.TrustManagerImpl | Hook verifyChain / checkTrusted |
| Flutter | Native ssl_crypto_device_session_new | Hook libflutter.so 内 SSL_CTX_set_custom_verify |
| React Native | 底层 OkHttp / NSURLSession | 同 OkHttp3 |
绕过 Root 检测
App 常通过以下方式检测 Root:检查 su 路径是否存在、读取系统属性、调用 RootBeer 等第三方库、检测 Magisk 模块挂载点等。以下脚本覆盖主要场景。
Java.perform(function () { // ────────────────────────────────────────────────── // 1. 文件存在性检查:su、busybox、Magisk 路径 // ────────────────────────────────────────────────── var File = Java.use('java.io.File'); var suPaths = [ '/system/bin/su', '/system/xbin/su', '/sbin/su', '/system/app/Superuser.apk', '/data/data/com.noshufou.android.su', '/data/adb/magisk', '/sbin/.magisk' ]; File.exists.implementation = function () { var path = this.getAbsolutePath(); if (suPaths.some(p => path.includes(p))) { console.log('[Root] File.exists() bypassed for: ' + path); return false; } return this.exists(); }; // ────────────────────────────────────────────────── // 2. 系统属性检查(ro.debuggable、ro.secure 等) // ────────────────────────────────────────────────── var SystemProperties = Java.use('android.os.SystemProperties'); SystemProperties.get.overload('java.lang.String').implementation = function (key) { var val = this.get(key); if (key === 'ro.debuggable') { return '0'; } if (key === 'ro.secure') { return '1'; } return val; }; // ────────────────────────────────────────────────── // 3. Runtime.exec() 执行 "which su" // ────────────────────────────────────────────────── var Runtime = Java.use('java.lang.Runtime'); Runtime.exec.overload('java.lang.String').implementation = function (cmd) { if (cmd.includes('su') || cmd.includes('which')) { console.log('[Root] Runtime.exec() blocked: ' + cmd); throw Java.use('java.io.IOException').$new(); } return this.exec(cmd); }; // ────────────────────────────────────────────────── // 4. RootBeer 库 // ────────────────────────────────────────────────── try { var RootBeer = Java.use('com.scottyab.rootbeer.RootBeer'); RootBeer.isRooted.implementation = function () { return false; }; RootBeer.isRootedWithBusyBox.implementation = function () { return false; }; console.log('[Root] RootBeer bypassed'); } catch(e) { console.log('[Root] RootBeer not found, skipping'); } });
完整性校验绕过
签名校验绕过
Java.perform(function () { // 先获取真实签名(用于白名单填充) Java.choose('android.app.ActivityThread', { onMatch: function (thread) { var ctx = thread.getApplication().getApplicationContext(); var pm = ctx.getPackageManager(); var pkg = ctx.getPackageName(); var PackageManager = Java.use('android.content.pm.PackageManager'); var sigs = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES.value).signatures.value; console.log('[Sig] Real signature: ' + sigs[0].toCharsString()); }, onComplete: function () {} }); // Hook PackageManager.getPackageInfo,返回真实签名(即使 APK 被重新签名) var PackageInfo = Java.use('android.content.pm.PackageManager'); var realSig = null; // 在首次调用时捕获真实签名并缓存 // 实际上最简单的方案是让 App 重打包时保留原始签名的哈希值 // 并在这里 Hook checksum 比较函数直接返回 true });
Google Play Integrity API 绕过
Java.perform(function () { // Hook IntegrityManager — requestIntegrityToken 返回成功的 Task try { var StandardIntegrityManager = Java.use( 'com.google.android.play.core.integrity.StandardIntegrityManager' ); // 具体实现取决于 Play Core 版本,通常需要 Hook Task 的回调 console.log('[Integrity] StandardIntegrityManager hooked'); } catch(e) { console.log('[Integrity] Play Integrity API not found'); } // 更通用:直接 Hook App 内对 integrity verdict 的处理逻辑 // (需要先用 JADX 定位具体类名和方法名) });
Anti-Frida 检测与对抗
高安全级别的 App(银行、游戏反作弊)会主动检测 Frida 的存在。了解这些技术才能有效对抗(防守方也需了解以加固 App)。
| 检测方式 | 原理 | 对抗方案 |
|---|---|---|
| 端口扫描 | 扫描 27042 等 Frida 默认端口 | 启动时加 --listen-backlog,或修改 frida-server 端口 |
| 进程名检测 | 读取 /proc 目录查找 frida-server | 重命名 frida-server 二进制文件 |
/proc/maps 扫描 | 查找 frida-agent、gadget 内存映射 | 使用自定义名称编译 frida-gadget |
| D-Bus 检测 | frida-server 使用 D-Bus 通信,特征明显 | 使用 Gadget 嵌入模式,绑定到 unix socket |
| ptrace 检测 | 进程自 ptrace 防止外部附加 | Hook ptrace 系统调用返回 0 |
| 完整性内存检测 | 比较函数头部字节检测 inline hook | 使用 Frida 的 Stalker 而非 Interceptor |
| 线程枚举 | 检查是否有 Frida 注入线程 | 修改 frida-agent 线程名 |
Hook ptrace 绕过自 ptrace 防附加
// Anti-debug 技术:App 对自身调用 ptrace(PTRACE_TRACEME) // 使得其他调试器无法附加(一个进程只能有一个 tracer) // Frida 在 spawn 模式下已自动处理,但某些情况下仍需手动处理 var ptracePtr = Module.getExportByName('libc.so', 'ptrace'); Interceptor.attach(ptracePtr, { onEnter: function (args) { var request = args[0].toInt32(); var PTRACE_TRACEME = 0; if (request === PTRACE_TRACEME) { console.log('[Anti-Debug] ptrace(PTRACE_TRACEME) blocked'); args[0] = ptr('-1'); // 将请求号改为无效值 } } });
绕过 /proc/maps 内存检测
// Hook open/fopen,当打开 /proc/maps 时返回空内容 var fopenPtr = Module.getExportByName('libc.so', 'fopen'); Interceptor.attach(fopenPtr, { onEnter: function (args) { this.path = args[0].readUtf8String(); }, onLeave: function (retval) { if (this.path && this.path.includes('/proc/self/maps')) { console.log('[Anti-Frida] /proc/self/maps access intercepted'); // 可以返回空 FILE* 或自定义内容 } } });
Stalker:代码追踪引擎
Stalker 是 Frida 的代码追踪引擎,能追踪线程执行的每一条指令(或每次函数调用/基本块),功能强大但性能开销较高。
追踪指定线程的所有调用
var targetThreadId = Process.getCurrentThreadId(); Stalker.follow(targetThreadId, { events: { call: true, // 追踪 CALL 指令 ret: false, // 是否追踪 RET exec: false, // 追踪每条指令(极慢,慎用) block: false, // 追踪基本块 compile: false }, onReceive: function (events) { var parsed = Stalker.parse(events, { annotate: true, stringify: true }); parsed.forEach(function (event) { console.log(event); }); }, transform: function (iterator) { var instruction = iterator.next(); while (instruction !== null) { // 可在此插入自定义代码(类似 DBI callout) iterator.keep(); instruction = iterator.next(); } } }); // 停止追踪 // Stalker.unfollow(targetThreadId);
RPC 双向通信
Frida 支持 JavaScript 脚本与 Python Host 之间的双向 RPC 调用,可以构建复杂的自动化分析工具。
JavaScript 端(暴露 RPC 函数)
// 通过 rpc.exports 暴露可被 Python 调用的函数 rpc.exports = { callEncrypt: function (data) { var result = null; Java.perform(function () { var Crypto = Java.use('com.example.Crypto'); result = Crypto.encrypt(data); }); return result; }, getSharedPrefs: function (key) { var value = null; Java.perform(function () { Java.choose('android.app.Activity', { onMatch: function(act) { var prefs = act.getPreferences(0); value = prefs.getString(key, 'NOT_FOUND'); }, onComplete: function(){} }); }); return value; } }; // send() 主动向 Python 发消息 send({ type: 'init', message: 'Script loaded' });
Python 端(驱动脚本)
import frida import sys def on_message(message, data): if message['type'] == 'send': print(f"[JS→PY] {message['payload']}") elif message['type'] == 'error': print(f"[ERROR] {message['stack']}") # 连接设备并 spawn 目标 App device = frida.get_usb_device() pid = device.spawn(['com.example.app']) session = device.attach(pid) with open('script.js', 'r') as f: script = session.create_script(f.read()) script.on('message', on_message) script.load() device.resume(pid) # 调用 JS 端暴露的 RPC 函数 encrypted = script.exports.call_encrypt('secret_data') print(f"Encrypted: {encrypted}") pref_value = script.exports.get_shared_prefs('auth_token') print(f"auth_token: {pref_value}") sys.stdin.read() # 保持进程存活
最佳实践 & 调试技巧
调试技巧
-
先静态分析,再动态插桩
用 JADX 分析出类名、方法名和混淆映射后再写 Frida 脚本,效率远高于盲目 Hook。对于你正在做的 Tomato App 逆向,先在 JADX 中找到 Premium 检测类,再用 Frida 动态验证。
-
使用 spawn 而非 attach 模式
frida -U -f com.pkg -l script.js --no-pause,spawn 模式在 App 启动最早阶段注入,能 Hook 到初始化过程中的安全检测,attach 则可能错过早期调用。 -
捕获并打印调用栈
遇到未知触发点时,在 Hook 内打印 Java 调用栈定位上层调用链:
Java.use('android.util.Log').getStackTraceString(Java.use('java.lang.Exception').$new()) -
处理混淆类名
混淆后类名形如
a.b.c,可用Java.enumerateLoadedClasses()配合关键字过滤,或在 JADX 中通过字符串常量、特征方法逆向找到真实类名。 -
脚本热重载
配合
frida -U -f com.pkg -l script.js和 IDE 保存自动触发,或用 Python 脚本监听文件变化并script.unload()/create_script()实现热重载,避免频繁重启 App。 -
使用 frida-compile 模块化
复杂项目用
frida-compile(TypeScript 支持)将多个.ts文件编译打包成单个 JS,获得类型提示、代码补全等工程化能力,提升脚本可维护性。
常见错误与解决
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
java.lang.ClassNotFoundException | 类名拼写错误或 ProGuard 混淆 | 用 Java.enumerateLoadedClasses() 搜索 |
No such method | 方法被混淆,或参数类型签名错误 | 用 clazz.class.getDeclaredMethods() 枚举 |
Process terminated | 脚本语法错误或 Hook 导致 App 崩溃 | 加 try/catch,先 log 再修改逻辑 |
unable to find export | .so 文件未加载或导出名被 strip | 用 Module.enumerateExports() 确认名称 |
Script destroyed | Session 中断(App 崩溃 / 重启) | 监听 script.destroyed 事件自动重连 |
| frida-server 启动失败 | SELinux 拦截 | adb shell setenforce 0 |
推荐工具链
frida.re/docs、CodeShare codeshare.frida.re(社区 Hook 脚本库)、以及 GitHub 上的 frida-android-helper、fridump(内存 dump 工具)都是非常好的学习资源。