在 Android 模拟器上用 Frida hook 原生函数:spawn 与 attach
本页目录
用 Frida hook 原生函数,就是三步:用 Process.getModuleByName(...).getExportByName(...) 拿到函数地址,用 Interceptor.attach 包住它,再用 send() 把结果发回来。需要从程序一开始就抓调用时用 spawn(frida -f),脚本在进程暂停时就已加载;attach(frida -p)只能看到 hook 生效之后的调用。函数必须在动态符号表里,在 Android 上意味着编译时要加 -rdynamic。
下面所有操作都针对我们自己写的 C 程序,跑在专门新建的 Android 14 模拟器上,frida-server 17.23.2、frida-tools 14.11.0。只对自己的进程或拿到授权的目标做插桩:Frida 脚本拥有目标进程的全部权限。
目标程序和 hook 脚本
tally.c 每秒五次给一个单词打分,把结果追加到日志里。要观察的是 score_word。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
/* Sum of letter positions. Not static, so the symbol stays in the binary. */
__attribute__((noinline)) int score_word(const char *word) {
int total = 0;
for (const char *p = word; *p; p++)
if (*p >= 'a' && *p <= 'z') total += *p - 'a' + 1;
return total;
}
int main(int argc, char **argv) {
const char *words[] = {"frida", "rea", "hook", "trace"};
int rounds = argc > 1 ? atoi(argv[1]) : 600;
printf("tally pid=%d rounds=%d\n", getpid(), rounds);
fflush(stdout);
for (int i = 0; i < rounds; i++) {
const char *w = words[i % 4];
int fd = open("tally.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
dprintf(fd, "%s=%d\n", w, score_word(w));
close(fd);
usleep(200000);
}
return 0;
}
trace.js 在进入时读字符串参数,返回时上报返回值:
const score = Process.getModuleByName('tally').getExportByName('score_word');
Interceptor.attach(score, {
onEnter(args) {
this.word = args[0].readUtf8String();
},
onLeave(retval) {
send({ fn: 'score_word', word: this.word, ret: retval.toInt32() });
}
});
send({ ready: true, pid: Process.id, arch: Process.arch, score_word: score.toString() });
this 是 onEnter 和 onLeave 共享的单次调用状态,单词就是这样传到返回处理里的。
模拟器和 frida-server
frida-server 需要 root,所以用 google_apis 镜像(Play Store 镜像不允许 adb root):
avdmanager create avd -n rea_frida_api34 \
-k "system-images;android-34;google_apis;arm64-v8a" -d pixel_6
emulator -avd rea_frida_api34 -no-window -no-audio -no-snapshot &
adb wait-for-device && adb root
curl -LO https://github.com/frida/frida/releases/download/17.23.2/frida-server-17.23.2-android-arm64.xz
xz -d frida-server-17.23.2-android-arm64.xz
adb push frida-server-17.23.2-android-arm64 /data/local/tmp/frida-server
adb shell chmod 755 /data/local/tmp/frida-server
adb shell '/data/local/tmp/frida-server -D'
frida-server 的版本要和客户端 frida 一致。然后用 NDK(这里是 27.1.12297006)交叉编译:
$NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android34-clang \
-O1 -g -rdynamic -o tally-android tally.c
llvm-nm -D tally-android | grep score # 0000000000001a70 T score_word
adb push tally-android /data/local/tmp/tally
adb shell chmod 755 /data/local/tmp/tally
-rdynamic 这个坑。 不加的话,Android 可执行文件不会把 score_word 放进动态符号表,脚本报 Error: /data/local/tmp/tally-nordyn: unable to find export 'score_word'。先用 llvm-nm -D 查一下再怀疑 Frida。如果是没法重编的二进制,就在反汇编器里找到函数偏移,改 hook module.base.add(offset)。
attach 到正在运行的进程
$ frida-ps -D emulator-5554 | grep tally
4992 tally
$ frida -D emulator-5554 -p 4992 -l trace.js -q -t 1
message: {'type': 'send', 'payload': {'ready': True, 'pid': 4992, 'arch': 'arm64', 'score_word': '0x558a1eca70'}} data: None
message: {'type': 'send', 'payload': {'fn': 'score_word', 'word': 'frida', 'ret': 38}} data: None
...
一秒内六条消息:ready 加上每 200 毫秒一次的五次调用。数值也对(f+r+i+d+a = 6+18+9+4+1 = 38)。-q -t 1 表示安静模式、一秒后退出。
frida-trace 不用写 JavaScript 就能做到同样的事,还能同时追多个函数:
$ frida-trace -D emulator-5554 -p 4992 -i 'score_word' -i 'open'
Started tracing 2 functions. Web UI available at http://localhost:61272/
/* TID 0x1380 */
186 ms open(path="tally.log", oflag=0x441, ...)
186 ms score_word()
391 ms open(path="tally.log", oflag=0x441, ...)
391 ms score_word()
它会在 __handlers__/ 下为每个函数生成一个处理文件,照 trace.js 的写法改一下就能打印参数。
spawn:在 main 之前装好 hook
attach 只能看到 hook 生效之后发生的事,启动时只跑一次的函数永远抓不到。spawn 会先以暂停状态启动程序、加载脚本,再恢复运行:frida -D emulator-5554 -f /data/local/tmp/tally -l trace.js(程序路径后面的参数会传进它的 argv)。我们的 spawn 运行是用 REA 记录成 JSON 的(见下一节):argv ["12"]、2000 毫秒窗口,共 11 条消息,即 ready 加上从第一轮循环开始的全部 10 次调用,第一条就是 frida。
窗口结束、detach 之后进程会继续运行:1000 毫秒的 spawn 窗口结束后,adb shell pidof tally 仍能打印出 PID。
spawn 要给路径,不能给相对名字。frida -f ./tally 报 Failed to spawn: unable to find application with identifier './tally',Frida 把它当成了应用标识符。改用绝对路径即可。
用 REA 把一次运行记录成 JSON
第三方开源工具 REA 把 Frida 包装成 rea instrument-with-frida 命令,另外提供给 agent 用的 MCP 工具。脚本仍然要自己写;REA 负责 attach 或 spawn、在固定窗口内收集所有 send()、detach,然后返回一条带脚本 SHA-256 的 JSON 记录。我们用的是 npm 上的 [email protected],它把 frida 17.23.2 锁为可选依赖(装完确认 node_modules/frida/build/frida_binding.node 存在)。
npx rea instrument-with-frida spawn /data/local/tmp/tally \
--device-id emulator-5554 --argument 12 \
--script-file trace.js --duration-ms 2000 --format json > spawn1.json
2.6 秒后退出码 0。删减后:
{
"evidence": {
"predicate_type": "rea.frida.instrumentation-observation",
"parameters": { "device_id": "emulator-5554", "mode": "spawn",
"program": "/data/local/tmp/tally", "argv": ["12"] },
"normalized_result": {
"state": "running",
"pid": 4950,
"messages": [
{ "type": "send", "payload": { "ready": true, "pid": 4950, "arch": "arm64",
"score_word": "0x5ede11da70" } },
{ "type": "send", "payload": { "fn": "score_word", "word": "frida", "ret": 38 } },
{ "type": "send", "payload": { "fn": "score_word", "word": "rea", "ret": 24 } },
{ "type": "send", "payload": { "fn": "score_word", "word": "hook", "ret": 49 } },
{ "type": "send", "payload": { "fn": "score_word", "word": "trace", "ret": 47 } }
],
"messages_truncated": false,
"source_path": "trace.js",
"source_sha256": "425a2db5c84d1bd9a7460f5e1297aa7cbc719d64092e007a2b5aacb35595c838",
"cleanup_error": null
},
"limitations": [
"Artifact identity is unavailable for this observation.",
"The target operating system and architecture were not reported by this observation."
]
}
}
source_sha256 与 shasum -a 256 trace.js 一致,保存下来的记录能证明是哪个脚本产生了哪次观察。provider.version 返回 null;目标的 stdout 不会被收集,只有 send() 消息。
attach 用法相同(instrument-with-frida attach 4992 --device-id emulator-5554 ... --duration-ms 1000 在 1.49 秒内拿到五条消息,地址和原生 frida 一样是 0x558a1eca70)。需要知道的几点:
- 脚本出错不会让命令失败。上面那个找不到导出的错误是以
"type": "error"消息返回的,命令退出码仍是 0;结果为空时先查messages。 list-frida-devices输出deviceId,list-frida-processes和 MCP 结果用的是device_id。--duration-ms上限 60000,消息采集上限 16 MiB(超了messages_truncated变true),attach 模式不接受--argument。- MCP 侧有会话工具,适合 agent 先看再决定:
start_frida_session(spawn 后暂停,99 毫秒)、load_frida_script(7 毫秒)、resume_frida_session、frida_session_status、unload_frida_script、close_frida_session。frida_session_status每次都返回完整消息历史,轮询时要自己去重;close_frida_session报告target_state: "unknown",但pidof显示进程仍在运行。
探索阶段用 frida-trace 更快;要留档对比或交给 agent 的运行,JSON 记录才是有用的部分。
为什么在 macOS 上超时
我们先试了 macOS 本机构建(cc -O1 -g -o tally tally.c,ad-hoc 链接器签名的 arm64 二进制),系统是 macOS 27.0.1,SIP 开启,DevToolsSecurity -status 显示开发者模式已启用。以普通用户运行原生 Frida 17.23.2 失败:frida -p <pid> -l trace.js 在 51 秒后报 Failed to attach: timeout was reached。通过 REA spawn 用了 1 分 40 秒才失败,报 Unable to resume spawned target: Timeout was reached,还留下了停住的目标进程(ps 状态 T)和 ~/.cache/frida/ 里的 frida-helper:
ps -o pid,stat,etime,command -p <pid> # STAT "T" = stopped, never resumed
kill <pid>
pgrep -fl frida-helper # kill leftovers too
原因在主机:非 root 的 Frida 连我们刚编译的二进制的 task port 都拿不到。我们没有试 root 运行或加 get-task-allow entitlement 重签,所以说不准哪个能解决。在新版 macOS 上,先把原生 frida -p 跑通再说别的。
我们没测 Linux、Windows 主机、真机、iOS、--remote-address 远程连接,也没测 Android 应用(只测了命令行可执行文件)。API 细节见 Frida 的 JavaScript API 文档;想先找到值得 hook 的函数,参见用 Ghidra 分析原生二进制。