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)

脚本逻辑:

  1. 等待 libnative-lib.so 加载
  2. Hook Java_com_kanxue_ollvm_1ndk_MainActivity_UUIDCheckSum(偏移 0xFF30)
  3. 函数进入时加载 libGumTrace.so 并调用 init / run
  4. 函数返回时调用 unrun
  5. 通过 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.solibc.so 等高调用频率库
  • 确保手机存储空间充足
  • 长时间追踪建议边追边 pull 或分段,避免单文件过大
提示:本文档对应的项目版本为 GumTrace main.cpp 使用 GUM_OPTIONS* 作为 init 第四个参数的版本。如果后续项目 API 变更,需同步调整 Frida 脚本。
posted @ 2026-06-29 16:55  公众号python学习开发  阅读(12)  评论(0)    收藏  举报