一、概述
Frida 可在运行时拦截函数调用、观察参数与返回值,并读取进程内存。它适用于静态分析难以确认的运行时密钥派生过程,以及加固应用在运行时释放的代码。
分析前先定位目标函数,确认参数含义,再用调用记录和实际解密结果验证观察到的数据。
上游是 APK 逆向与 Manifest 分析 的加固判定 —— 没有那个结论,动态分析就是盲试。
下游是 即时通讯取证 里加密数据库的解密,以及 进程与注入检测 里对本篇操作痕迹的识别。
动态注入会改变目标进程的运行状态。应记录工具版本、脚本内容、操作时间及产生的文件,并区分原有证据与分析操作留下的痕迹。
使用前需确认以下条件:
- 「Frida 能破解所有加密」 —— 它只能观察运行时行为。密钥在 TEE 内生成就看不到,
StrongBox 保护的密钥同样不可读。
- 「有 Frida 就不用静态分析了」 —— 必须先做完备的静态分析,否则你不知道该 hook 哪个函数、参数是什么含义。
- 「Hook 成功就等于取到了数据」 —— 拿到 key 还要实际解密成功、并能解析出表结构,才算验证通过(见 第 5 步)。
涉及的证据形态:
| 形态 |
具体内容 |
说明 |
| 设备端服务 |
/data/local/tmp/frida-server,端口 27042 |
需 root,与目标进程同 UID;工具本身要哈希记录 |
| 通道 |
adb forward 的 USB 转发,或 TCP/IP |
TCP 会把 27042 暴露在网络上,任何人可完全控制目标进程 |
| 脚本 |
.js 脚本、frida-trace 的自动生成的桩 |
脚本本身可能含敏感值,产出日志按证据材料管理 |
| 运行时观察 |
sqlite3_key、open、exec 的调用序列 |
目标是证明某个值从哪个调用点来,不是刷屏 |
| 落盘产物 |
hook 抓到的密钥、解密后的库 |
只在副本上操作,绝不在检材上解密 |
| 环境痕迹 |
/data/local/tmp/、/proc 下的注入映射、进程内存布局 |
本篇操作自己留下的,报告必须主动交代 |
调用记录用于核查数据库打开过程、密钥来源、目标代码的执行情况,以及加固代码的释放时机。
Hook 脚本可以改变返回值,因此观察结果需要交叉验证。进程内的调用记录也不能直接归因于某个用户;密钥是否被上传,需结合网络记录判断。
报告应注明观察条件、脚本是否修改参数或返回值,以及支持用户行为归因的独立证据。
本文不做的事:不做脱壳、不做破解保护机制、不修改检材上的原始 APK。frida-gadget 方案需要重打包重签名,只应在委托方明确同意制作分析副本时使用,绝不替换原始 APK。
抓到密钥后的用途是离线解密检材副本。任何在线使用检材内凭据的行为都超出取证范围。
读者前提:APK 逆向与 Manifest 分析 的加固判定结论、Java/Native 运行时概念、以及进程与内存的基本模型。
如果没有静态分析的基础,建议不要直接开始写脚本 —— 第 1 步 那一节就是为此存在的。
二、核心原理
2.1 架构:两端 + 一条通道
主机端(分析机):frida CLI(REPL) / frida-ps / frida-trace / frida-kill / frida-tools + Python 绑定
│ ① USB(ADB 转发,默认) 或 ② TCP/IP 默认端口 frida-server: 27042
▼
设备端(检材):frida-server(常驻,需 root) / frida-gadget(内嵌 APK,免 root)
与目标进程同 UID,注入后调用拦截生效
frida-server 是设备端常驻服务,必须 root、不改动检材,是取证默认首选。
frida-gadget 免 root,但必须重打包并重签名 APK,只在委托方明确同意制作分析副本时使用。
任何情况下都不要替换原始 APK —— gadget 方案改变检材本身,在正式案件中会破坏证据完整性。
通道选择:adb forward tcp:27042 tcp:27042 后用 frida -U(USB,推荐);远程时设备端 frida-server -l 0.0.0.0:27042、主机端 frida-ps -H 192.168.1.50:27042 -ai。
网络方式会把 27042 端口暴露在网络上,任何人都能连接并完全控制目标进程。 在受限网络环境的现场作业中,优先用 USB 转发;如必须用网络,做完立即 frida-kill 关闭服务并记录操作时间。
2.2 三种 Hook 机制
| 机制 |
作用对象 |
典型用途 |
Java.perform + Java.use |
Java/Kotlin 层方法 |
读数据库初始化参数、拦截加解密调用 |
Interceptor.attach |
任意 native 函数(含 libc、JNI) |
读 sqlite3_key、拦截 open、看 native 算法 |
Module.findExportByName + NativeFunction |
按地址直接调用 native 函数 |
主动调用函数验证行为 |
两条规则(详细示例见 第 4 步:Hook 的四种典型用法):
Java.perform(function () { // ① 必须在 Java 线程内执行
var DB = Java.use("android.database.sqlite.SQLiteDatabase");
DB.openOrCreateDatabase.overload( // ② 必须写 overload 精确指定重载
"java.io.File", "int", "android.database.sqlite.SQLiteCursorFactory"
).implementation = function (file, flags, factory) {
console.log(file.getAbsolutePath());
return this.openOrCreateDatabase(file, flags, factory); // ③ 必须回调原实现
};
});
不写 overload 会报 "more than one overload"。不回调原实现会导致应用崩溃或功能异常——在检材上这意味着状态被破坏,是动态分析中最容易造成证据污染的操作。
2.3 版本差异:-f 的行为变化
旧版(< 12)frida -f <包名> 启动后处于暂停态,需在 REPL 里输入 %resume;新版(≥ 12)-f 启动后自动恢复执行,旧的 --no-pause 参数已废弃。
常见错误:把旧教程里的 --no-pause 抄进新版本命令行,frida 直接退出。先 frida --version,再看版本对应的用法。
2.4 反 Frida 检测的常见手法
这一节列出检测类型与可观察痕迹,目的是识别检材的对抗性,不是提供规避手段。
| 检测类型 |
实现方式 |
观察点 |
| 字符串扫描 |
读自身内存/已加载库,搜 frida、gum-js-loop、gmain 等符号 |
so 列表中出现 frida-agent |
| 端口探测 |
遍历 /proc/net/tcp,查 27042(hex 0x69AA)是否 LISTEN |
ss -ltnp | grep 27042 |
| 进程名扫描 |
遍历 /proc/*/cmdline,查含 frida 的进程 |
ps -A | grep frida |
| D-Bus 检测 |
注册 frida 所用的 D-Bus 总线名,注册成功即判定被注入 |
日志无异常但行为变化 |
| 完整性校验 / 调试器检测 |
校验自身文件 hash、检查 /proc/self/maps 异常条目;检查 TracerPid、ptrace 自身状态 |
匿名可执行段数量异常;常规调试检测常与 Frida 同时出现 |
取证中的两类用途:
① 识别检材的对抗性。 若应用有明确的 Frida 检测,说明开发者有对抗意识,其保护强度评估应相应提高,报告也应说明这一发现。
② 降低动态分析的可用性。 检测存在时,优先考虑旁路方案(直接 dump 进程内存、读取已落盘的中间文件、静态分析),而不是硬碰。
不提供绕过反检测的具体代码。 理由有二:① 这类技术对商用 App 的保护机制构成规避手段,超出取证分析的必要范围;② 更重要的是,绝大多数案件并不需要走这条路 —— 落盘的数据直接按分区取走即可,取证路径见 第 01 篇。
三、操作步骤
第 1 步:静态分析先行(本步不能跳)
先做完整的 APK 静态分析(见 03 篇),再明确三件事才动手:
1. 目标:我要拿什么?(数据库密码 / 解密后的字节 / 接口参数)
2. 位置:它可能在哪里产生?(Java 层?JNI?native 库中的某函数?)
3. 验证:拿到后如何证明它是真密钥?(实际解密成功 + 表结构可读)
aapt dump badging base.apk ; apktool d -f -o app-decoded base.apk
jadx -d app-jadx base.apk --no-res
grep -rn "openOrCreateDatabase|SQLiteOpenHelper" app-jadx/sources/
grep -rn "SecretKeySpec|Cipher.getInstance" app-jadx/sources/
unzip -l base.apk | grep '.so$' | awk '{print $NF}' # 确认有无加固
第 2 步:准备设备端
adb shell su -c "id" # 前提:检材已 root(授权文件见 01 篇)
推送并启动 frida-server(版本必须与主机端 frida 完全一致,否则握手失败)
adb push frida-server-16.x.x-android-arm64 /data/local/tmp/frida-server
adb shell su -c "chmod 755 /data/local/tmp/frida-server"
adb shell su -c "/data/local/tmp/frida-server -D &" # 后台常驻
adb shell su -c "ps -A | grep frida"
adb forward tcp:27042 tcp:27042 # USB 转发,避免暴露网络端口
frida-ps -U | head -20 # 能列出进程 = 通道建立成功
版本必须一致:主机端 frida --version 与设备端 frida-server --version 不同会直接握手失败。这是「连不上」最常见的原因。
第 3–4 步:摸清进程,Hook 的四种典型用法
用法 A:frida-trace 批量生成调用日志(最快定位)
frida-ps -U -ai | grep -i sample # 列出应用(-a 应用 / -i 已安装标识)
frida-trace -U -f com.digiforensics.sample -p "*sqlite3*" -i # 所有 sqlite3 native 调用
frida-trace -U -f com.digiforensics.sample -j "openOrCreateDatabase" # Java 层数据库打开
frida-trace -U -f com.digiforensics.sample -i "*!open*" # libc open:能看到打开的文件路径
输出落在 ./__handlers__/ 目录下,是纯文本调用日志,可作为证据材料。
用法 B:Java.use 拦截 Java 层调用
// hook-crypto.js —— 在已授权检材上确认加解密的调用时序
// 用法: frida -U -f com.digiforensics.sample -l hook-crypto.js
Java.perform(function () {
var Cipher = Java.use("javax.crypto.Cipher");
Cipher.doFinal.overload("[B").implementation = function (input) {
var out = this.doFinal(input); // ★ 必须回调原实现
// 只记录长度与时序,不在日志中回显明文,避免敏感数据扩散
console.log("[doFinal] in=" + input.length + "B out=" + out.length + "B");
return out;
};
});
overload 为什么必需:Java 方法可重载,Frida 必须按参数列表精确指定要 Hook 的那一个。不写 overload 会报 "more than one overload"。同时注意:Hook 函数必须显式回调原实现,否则应用会崩溃或行为异常——这在检材上意味着状态被破坏。
用法 C:拦截 native 函数
// hook-sqlite.js —— 观察数据库打开时传入的 key
// 用法: frida -U -n "样本应用" -l hook-sqlite.js
var sqlite3 = Module.findExportByName(null, "sqlite3_key"); // SQLCipher 才有
if (sqlite3) {
Interceptor.attach(sqlite3, {
onEnter: function (args) { // args[1] 指向 key 字节,args[2] 为长度
var len = args[2].toInt32();
if (len > 0 && len < 128) {
console.log("[sqlite3_key] len=" + len + " key=" +
hexdump(Memory.readByteArray(args[1], len), {ansi: false}));
}
}
});
} else { console.log("[!] 未找到 sqlite3_key —— 该库可能使用应用自加密"); }
用法 D:读取内存中的字符串(探索性)——Process.enumerateRangesSync({protection:'r--'}) 遍历可读内存区,Memory.readUtf8String(r.base, Math.min(r.size, 4096)) 读内容后用正则筛出 api./token/secret/http(s):// 等模式,跨页读失败是正常的,用 try/catch 吞掉即可。
第 5 步:验证与现场清理
真正的验证在主机侧做:拿 key 去解密副本(★ 永远在副本上操作)
cp app.db app-copy.db && sha256sum app.db app-copy.db
sqlite3 app-copy.db "PRAGMA key='<取得的key>';"
sqlite3 app-copy.db ".tables" # 能列出表 = 密钥正确 ✓
sqlite3 app-copy.db "PRAGMA integrity_check;"
frida-kill -U # 作业结束立即停止服务
复核检材未变:设备上未必自带 md5sum(toybox/busybox 差异),缺失时先 tar 回本地再算
adb shell su -c "md5sum /data/data/com.digiforensics.sample/databases/app.db"
sha256sum app.db # 与提取时的哈希比对
必须记录的操作日志(写入证据链):分析时间(起止精确到分)、工具版本(两端 frida 版本号与 frida-server 二进制哈希)、分析对象(包名、APK 哈希、数据目录路径)、操作类型(spawn / attach / 注入的脚本——脚本本身也是证据)、影响面(是否重启应用、是否修改文件、是否退出登录)、取得结果(调用序列、参数、密钥值;是否写入报告需评估)。
验证标准是「能读出表结构且 integrity_check 返回 ok」,不是「长度对得上」或「看起来像十六进制串」—— 一个 16 字节的随机串有极小概率通过格式检查,但不可能通过实际解密。
四、常见陷阱
动态分析是移动取证里唯一一个必须在检材上执行动作的分析环节。
前面几篇的陷阱多发生在读与解析。这一篇的陷阱集中在做与不留痕 —— hook 崩了应用、网络端口没关、密钥拿到了却没验证、日志里全是明文。
这一章的每一条都要考虑一个共同前提:动态分析的所有动作都会在检材上留下痕迹,且其中一部分不可回退。
4.1 环境与连接
陷阱 1:两端 Frida 版本不一致,反复排查「连不上」
现象:frida-server 推上设备、起了进程、adb forward 也做了,frida-ps -U 报连接失败或握手错误。反复换端口、重启 adb、检查 USB 调试授权,全部无效。
原因:Frida 的主机端与设备端必须版本完全一致,两个数字都要对得上(16.2.1 与 16.2.1)。小版本不同也会直接握手失败,而这个失败的表现是「连接被拒」或「协议错误」——与「设备没 root」「USB 调试没授权」「端口被占用」在错误信息上非常相似。分析者的注意力会自然地落在更「物理」的因素上(权限、线缆、端口),因为它们更容易验证,而版本号这个纯粹的软件匹配问题不在任何一条错误信息的字面里。另外两端的架构也必须匹配(android-arm64 的 server 不能推到 x86 模拟器或 armeabi 设备上),这一点同样不会在错误信息里明说。
影响:时间成本之外,真正的问题是它会引发一系列「试探性操作」。
为了排除各种可能,分析者会不断重启 adb、反复 push、改端口、试 frida-ps -H。
其中有些操作会在检材上留下痕迹(重启服务、推送文件、进程反复起停),有些则可能触发应用的异常行为。
最后通道终于通了,但检材的状态已经不是刚拿到时的样子了,而这一段反复折腾的过程往往没有被记录进操作日志。
核查:版本与架构在推之前就核对,并把核对结果写进工作记录:
① 两端版本号,必须逐字符相同
frida --version
adb shell su -c "/data/local/tmp/frida-server --version"
16.2.1 == 16.2.1 ✓
② 架构必须匹配设备实际架构
adb shell getprop ro.product.cpu.abi
arm64-v8a → 对应 frida-server-*-android-arm64
③ 二进制哈希固定(工具也是证据,见陷阱 10)
sha256sum frida-server-16.2.1-android-arm64 | tee frida-server.sha256
④ 推送后先验证进程真的起来了,再建立转发
adb shell su -c "chmod 755 /data/local/tmp/frida-server"
adb shell su -c "/data/local/tmp/frida-server -D &"
adb shell su -c "ps -A | grep frida"
adb forward --remove tcp:27042 2>/dev/null ; adb forward tcp:27042 tcp:27042
frida-ps -U | head -5 # 能列出进程才算通道建立
排查顺序固定为:版本 → 架构 → root 权限 → 端口占用 → USB 调试授权。 先做前两项,因为它们最确定、验证成本最低,且排除方式是把两个版本号并排打出来看一眼。
陷阱 2:★ 把 frida-server 用 -l 0.0.0.0:27042 监听网络,作业结束未关闭
现象:现场没有 USB 线(设备在现场封存中、线在包里但不便连接),于是用 frida-server -l 0.0.0.0:27042 起了网络监听,作业做完只把分析机断开,没有执行 frida-kill,设备上的服务继续运行。
原因:27042 一旦监听在 0.0.0.0 上,同一网络内任何设备都能连接它,并对目标进程获得完全控制能力——包括读取全部内存、Hook 任意函数、修改返回值、调用任意 native 函数。而这不需要任何凭据。它的隐蔽性在于「不做坏事就没人发现」:这个服务不在界面上、不弹通知、不写日志,安卓的省电策略也不会杀掉它(常驻服务且无 wakelock 压力时存活率极高)。没有人会因为「27042 开着」而注意到它。 而从取证角度看,这个动作还有一个自己可能没想到的后果——检材状态在分析结束后仍然处于「被工具完全控制」的状态,且这个状态持续到有人手动关掉为止。
影响:这是一个把检材置于不受控状态的风险,且持续时间不可控。
后果分两类。
① 安全类。 网络内他人可读取检材内全部数据 —— 这批数据的性质可能正是案件涉及的高敏感信息,甚至篡改或植入内容。
② 证据类。 如果在此期间有人对检材做了操作,检材状态的变化会算在谁头上? 取证人员留下的、持续开放的攻击面,在移交或暂存环节是完全无法解释的。
委托方要求「设备保持原状」时,这个状态变化本身就是违约。
核查:优先 USB 转发;必须用网络时,把「关闭动作」作为作业的强制收尾项并记录时间:
首选:USB 转发,端口只在本机监听
adb forward tcp:27042 tcp:27042 # 只在主机侧建立,不在网络上开端口
frida-ps -U
确需网络时:绑定到具体网卡地址,不绑 0.0.0.0
adb shell su -c "/data/