Frida 动态 Hook

静态分析能告诉你程序结构是怎样的,但有两类关键信息只存在于运行时:由多因素派生的密钥(IMEI、序列号、用户输入、签名哈希混合后做 KDF,其中服务器下发的盐在静态代码里根本看不见)与加固后才释放的真实代码。Frida 是处理这两类需求最通用的工具——它把脚本注入目标进程,在运行时拦截函数调用、读取内存。

最后更新 2026-10-09版本 v1.0维护 DigiForensics查看历史 0

关键词:Frida、frida-server、Interceptor、Java.perform、动态分析、反检测 难度:高级 前置知识:APK 结构与逆向基础、Java/Native 运行时概念、进程与内存 相关文章:APK 逆向与 Manifest 分析、即时通讯取证

一、概述

静态分析能告诉你程序结构是怎样的,但有两类关键信息只存在于运行时:由多因素派生的密钥(IMEI、序列号、用户输入、签名哈希混合后做 KDF,其中服务器下发的盐在静态代码里根本看不见)与加固后才释放的真实代码。Frida 是处理这两类需求最通用的工具——它把脚本注入目标进程,在运行时拦截函数调用、读取内存。

跳过这一层会发生什么:加壳应用的数据库打不开时,常见做法是「换个工具再试试」或者「判定为不可取」。

实际上多数情况不是不可取,而是没人知道该在哪里拦。

真正的差距在成本上 —— 知道 hook 哪个函数,本篇案例里是半小时;不知道,代价是几天的盲目尝试加一份无法解释的日志。

本篇要解决的不是「怎么用 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 开着」而注意到它。 而从取证角度看,这个动作还有一个自己可能没想到的后果——检材状态在分析结束后仍然处于「被工具完全控制」的状态,且这个状态持续到有人手动关掉为止。

误判代价:这是一个把检材置于不受控状态的风险,且持续时间不可控。

安全验证 当前请求需要先完成一次滑块验证。