GumTrace 使用记录
基于 Frida Gum Stalker 的 ARM64 真机指令级追踪工具。本文记录 Android 端的完整使用流程。
一、环境准备
- macOS 开发机(用于编译)
- Android 手机,已 root,arm64 架构
- 手机已开启 USB 调试,adb 可连接
- 手机已启动 frida-server(测试用 16.5.9)
- Android NDK(测试用 r27b)
- CMake >= 3.10
注意:如果手机 SELinux 为 Enforcing,可能需要先执行
adb shell su -c 'setenforce 0',否则 /data/local/tmp/libGumTrace.so 可能无法被目标进程 dlopen。二、项目结构
GumTrace/
├── build_android.sh # Android 构建脚本
├── CMakeLists.txt # CMake 配置
├── example.js # 官方示例(已不完全适配当前 main.cpp)
├── ollvm9_demo.js # 旧版 ollvm 测试脚本(API 签名已过期)
├── test_ollvm9.js # 当前可用的测试脚本
├── libs/
│ ├── FridaGum-Android-17.8.3-fix.a
│ └── FridaGum-Android-17.8.3.h
└── src/
├── main.cpp # 导出 init/run/unrun 接口
├── GumTrace.cpp # 核心 Stalker 追踪引擎
└── ...
三、编译 GumTrace
1. 设置 NDK 路径
export ANDROID_NDK_HOME=/Users/chennan/Library/Android/sdk/ndk/27.1.12297006
2. 执行构建脚本
./build_android.sh
产物位置:
build_android/libGumTrace.so
四、推送到手机
adb push build_android/libGumTrace.so /data/local/tmp/ adb shell chmod 755 /data/local/tmp/libGumTrace.so
五、安装测试应用
测试目标为看雪 OLLVM 练习应用 com.kanxue.ollvm_ndk_9,追踪其 libnative-lib.so。
adb install -r ollvm9.apk
六、Frida 脚本使用
1. init 接口签名
当前 main.cpp 中的 init 函数签名为:
void init(const char *module_names, char *trace_file_path, int thread_id, GUM_OPTIONS *options)
对应的 Frida NativeFunction 声明:
gumtrace_init = new NativeFunction(
dlsym(soHandle, Memory.allocUtf8String('init')),
'void',
['pointer', 'pointer', 'int', 'pointer'] // 第四个是指针,不是 int
);
2. GUM_OPTIONS 结构
struct GUM_OPTIONS {
uint64_t mode; // 0=Stand, 1=DEBUG, 2=Stable
};
创建方式:
let options = Memory.alloc(8); options.writeU64(0); // Stand 模式
3. 模式选择
| mode | 说明 | 适用场景 |
|---|---|---|
| 0 | Stand 模式,默认策略 | 一般追踪 |
| 1 | DEBUG 模式,高频刷新 | 调试、小范围测试 |
| 2 | Stable 模式,排除系统 range,提高 trust threshold | 长时间、大文件追踪 |
七、完整测试脚本(test_ollvm9.js)
脚本逻辑:
- 等待
libnative-lib.so加载 - Hook
Java_com_kanxue_ollvm_1ndk_MainActivity_UUIDCheckSum(偏移 0xFF30) - 函数进入时加载
libGumTrace.so并调用init/run - 函数返回时调用
unrun - 通过 Java FileInputStream 读取生成的 trace 文件并打印前 30 行
let traceSoName = 'libGumTrace.so';
let targetSo = 'libnative-lib.so';
let targetPackage = 'com.kanxue.ollvm_ndk_9';
let gumtrace_init = null;
let gumtrace_run = null;
let gumtrace_unrun = null;
function loadGumTrace() {
let dlopen = new NativeFunction(Module.findExportByName(null, 'dlopen'), 'pointer', ['pointer', 'int']);
let dlsym = new NativeFunction(Module.findExportByName(null, 'dlsym'), 'pointer', ['pointer', 'pointer']);
let soHandle = dlopen(Memory.allocUtf8String('/data/local/tmp/' + traceSoName), 2);
console.log('GumTrace loaded:', soHandle);
if (soHandle.isNull()) return false;
gumtrace_init = new NativeFunction(dlsym(soHandle, Memory.allocUtf8String('init')), 'void', ['pointer', 'pointer', 'int', 'pointer']);
gumtrace_run = new NativeFunction(dlsym(soHandle, Memory.allocUtf8String('run')), 'void', []);
gumtrace_unrun = new NativeFunction(dlsym(soHandle, Memory.allocUtf8String('unrun')), 'void', []);
return true;
}
function startTrace() {
if (!loadGumTrace()) return;
let tracePath = '/data/data/' + targetPackage + '/ollvm_trace.log';
let moduleNames = Memory.allocUtf8String(targetSo);
let outputPath = Memory.allocUtf8String(tracePath);
let threadId = 0;
let options = Memory.alloc(8);
options.writeU64(0);
console.log('start trace, output:', tracePath);
gumtrace_init(moduleNames, outputPath, threadId, options);
gumtrace_run();
}
function readTraceFile() {
Java.perform(function () {
let File = Java.use('java.io.File');
let FileInputStream = Java.use('java.io.FileInputStream');
let BufferedReader = Java.use('java.io.BufferedReader');
let InputStreamReader = Java.use('java.io.InputStreamReader');
let tracePath = '/data/data/' + targetPackage + '/ollvm_trace.log';
let file = File.$new(tracePath);
if (!file.exists()) {
console.log('trace file not found:', tracePath);
return;
}
console.log('trace file size:', file.length(), 'bytes');
let br = BufferedReader.$new(InputStreamReader.$new(FileInputStream.$new(file)));
console.log('=== trace begin ===');
let line, count = 0;
while ((line = br.readLine()) != null && count < 30) {
console.log(line);
count++;
}
console.log('=== trace end (first ' + count + ' lines) ===');
br.close();
});
}
function stopTrace() {
console.log('stop trace');
gumtrace_unrun();
setTimeout(readTraceFile, 300);
}
function attachTarget() {
let targetModule = Process.findModuleByName(targetSo);
if (!targetModule) return false;
Interceptor.attach(targetModule.base.add(0xFF30), {
onEnter() {
startTrace();
this.tracing = true;
},
onLeave() {
if (this.tracing) stopTrace();
}
});
return true;
}
function hook_dlopen(soName) {
Interceptor.attach(Module.findExportByName(null, 'android_dlopen_ext'), {
onEnter: function (args) {
let path = args[0].readCString();
if (path && path.indexOf(soName) >= 0) this.is_can_hook = true;
},
onLeave: function (retval) {
if (this.is_can_hook) {
console.log(soName, 'load success');
attachTarget();
}
}
});
}
setImmediate(function () {
if (!attachTarget()) hook_dlopen(targetSo);
});
八、运行测试
步骤 1:启动 Frida 并注入脚本
frida -U -f com.kanxue.ollvm_ndk_9 -l test_ollvm9.js
如果命令行会暂停在主线程,输入 %resume 继续。
步骤 2:点击 CHECK 按钮
也可以通过 adb 模拟点击(坐标根据屏幕调整):
adb shell input tap 540 360
步骤 3:查看输出
Frida 控制台会打印 trace 文件大小和前 30 行内容,例如:
trace file size: 1010814 bytes === trace begin === [libnative-lib.so] 0x75d4a53f40!0xff40 stp x22, x21, [sp, #0x70]; ... [libnative-lib.so] 0x75d4a53f70!0xff70 blr x8; ... call jni func: GetStringLength(...) args1: gjbwH5S0EOyYLj7G7oMuDE1piOLQ6IEI4kb8 ret: 0x24 === trace end (first 30 lines) ===
九、trace 文件格式说明
[模块名] 0x绝对地址!0x相对偏移 助记符 操作数; 寄存器信息 mem_r=读地址 mem_w=写地址
示例:
[libnative-lib.so] 0x75d4a53f70!0xff70 blr x8; x8=0x763e8b3d80 call jni func: GetStringLength(0xb4000076430df500, 0x7fc500adb8) args1: gjbwH5S0EOyYLj7G7oMuDE1piOLQ6IEI4kb8 ret: 0x24
十、常见问题
1. dlopen libGumTrace.so 返回 null
- 检查
/data/local/tmp/libGumTrace.so是否存在且权限为 755 - 检查 SELinux 是否为 Permissive
- 检查 so 架构是否和手机一致(arm64)
2. trace 文件为空或不存在
- 确认输出路径应用有写权限(推荐
/data/data/<pkg>/...) - 确认目标函数确实被执行
- Stand 模式下 buffer 写盘频率低,小函数执行完可能正好在 buffer 里,但
unrun()会 flush
3. 无法直接 adb pull /data/data/ 下的文件
- 应用私有目录普通 adb 无法访问
- 方案 A:手机有 su 时
adb shell su -c 'cp /data/data/... /data/local/tmp/' - 方案 B:通过 Frida 脚本用 Java FileInputStream 读取(本文采用)
- 方案 C:输出到
/sdcard/Android/data/<pkg>/files/(部分机型/系统版本受限)
4. 原 ollvm9_demo.js 不能用
原脚本中 gumtrace_init 的签名是 ['pointer', 'pointer', 'int', 'int'],但当前 main.cpp 已改为 ['pointer', 'pointer', 'int', 'pointer'],需要同步修改。
十一、大文件追踪建议
- 使用 Stable 模式(mode=2),减少系统模块干扰
- 只追踪必要的 so,避免追踪
libart.so、libc.so等高调用频率库 - 确保手机存储空间充足
- 长时间追踪建议边追边 pull 或分段,避免单文件过大
提示:本文档对应的项目版本为 GumTrace main.cpp 使用
GUM_OPTIONS* 作为 init 第四个参数的版本。如果后续项目 API 变更,需同步调整 Frida 脚本。
浙公网安备 33010602011771号