Frida Stalker跟踪JNI程序时JVM崩溃及native.dll覆盖异常问题
Frida Stalker跟踪JNI Java程序的问题与解决方案
问题背景
- 使用Frida Stalker跟踪调用JNI的极简Java程序时,JVM关闭阶段固定触发访问违例崩溃
- 仅当
Stalker.trustThreshold设置为-1时才能成功跟踪JVM进程,即便添加-Xint参数禁用JIT编译也不改变该现象 - 若
Stalker.trustThreshold设为非-1值,JVM不会发生崩溃,但Stalker完全采集不到自定义JNI库native.dll的基本块(BB)覆盖数据
复现代码与报错信息
1. Java主程序代码
// Main.java public class Main { static { System.loadLibrary("native"); } public static void main(String[] args) throws Exception { Thread.sleep(20_000); // 预留足够时间附加到进程 new Main().sayHello(); } private native void sayHello(); }
2. JNI Native层实现代码
// Main.cpp #include "Main.h" #include <iostream> #include <fstream> #include <windows.h> void sayHello() { std::ofstream file; file.open("output.tmp"); file << "Hello World\n"; file.close(); std::cout << "Current Thread id = " << GetCurrentThreadId() << "\n"; } JNIEXPORT void JNICALL Java_Main_sayHello (JNIEnv *, jobject) { sayHello(); }
3. Windows环境编译运行命令
# 编译native动态库(Windows环境) cl.exe /LD /EHsc /I "%JAVA_HOME%\include" /I "%JAVA_HOME%\include\win32" Main.cpp /link /DLL /DEBUG /OUT:native.dll # 编译Java字节码 javac Main.java # 启动Java程序 java -cp . -Djava.library.path=%cd% Main
4. Frida跟踪脚本代码
# frida-agent.py import sys import frida def on_message(message, data): print(str(message)) def main(): device = frida.get_local_device() pid = -1 for proc in device.enumerate_processes(): if sys.argv[1] in [str(proc.pid), proc.name]: pid = proc.pid break session = device.attach(pid) print("Attaching to PID=" + str(pid)) script = session.create_script( """ Stalker.trustThreshold = -1; const moduleMap = new ModuleMap(); moduleMap.update(); const createCoverageMap = (events) => { moduleMap.update(); const coverageMap = {}; for (const event of events) { const [start, _] = event; const pStart = new NativePointer(start); const module = moduleMap.find(pStart); if (module) { const offset = pStart.sub(module.base).toInt32(); if (!(module.path in coverageMap)) { coverageMap[module.path] = 0; } coverageMap[module.path] += 1; } } return coverageMap; }; const stalkThread = (threadId) => { Stalker.follow(threadId, { events: { call: false, ret: false, exec: false, block: false, compile: true, }, onReceive: function (events) { const bbEvents = Stalker.parse(events, {stringify: false, annotate: false}); send({coverage: createCoverageMap(bbEvents)}); } }); } const currentThreads = Process.enumerateThreads(); for (const thread of currentThreads) { console.log("Stalking thread: " + thread.id); stalkThread(thread.id); } """ ) script.load() script.on("message", on_message) sys.stdin.read() sys.exit(0) if __name__ == "__main__": main()
5. 运行报错信息
执行python frida-agent.py 8232附加目标进程后,程序输出如下崩溃日志:
Current Thread id = 9632 # # Java运行时环境检测到致命错误: # # EXCEPTION_ACCESS_VIOLATION (0xc0000005) 出错地址pc=0x00000287f70dae25, 进程pid=8232, 线程tid=0x00000000000025a0 # # JRE版本: OpenJDK Runtime Environment (8.0_292-b10) (build 1.8.0_292-b10) # Java虚拟机: OpenJDK 64-Bit Server VM (25.292-b10 mixed mode windows-amd64 compressed oops) # 出错帧: # C 0x00000287f70dae25 # # 核心转储写入失败,Windows客户端版本默认不启用Minidump生成 # # 详细错误报告已保存至: # C:\workspace\simple-jni-app\hs_err_pid8232.log #
解决方案
问题1:修复JVM关闭阶段的访问违例崩溃
崩溃核心原因:附加时无差别对所有JVM线程注入Stalker,JVM关闭时直接销毁线程,未给Stalker留出unfollow清理时机,线程销毁后残留的Stalker转译代码指针被访问就会触发0xc0000005访问违例。
修复操作:
- 放弃全线程Stalk逻辑,仅跟踪会执行目标native方法的业务线程
- 注册线程销毁、进程退出回调,回调中第一时间对对应线程执行
Stalker.unfollow(threadId),等待Stalker完成代码块回收后再放行退出流程 - 不要在JVM内部工作线程(GC线程、JIT线程、信号处理线程等)上开启Stalker,这类线程生命周期完全由JVM管控,退出逻辑不允许外部插桩打断,是崩溃高发点。
问题2:非-1 trustThreshold下采集不到native.dll基本块数据
现象核心原因:Stalker信任阈值机制默认会跳过识别为系统模块、JVM核心模块的代码插桩,而native.dll加载时机晚于Stalker初始化的模块扫描,加上JVM调用JNI方法是通过存放在可写内存的函数指针完成跳转,Stalker预扫描逻辑没有追踪到该跳转路径,直接将执行流判定为可信路径跳过插桩。即便加-Xint禁用JIT,JVM解释器、字节码分发逻辑依然是内存中动态生成的可执行代码,Stalker默认信任这类内存段,不会自动跟进跨模块的JNI跳转。
修复操作:
- 不要在附加后立刻全量stalk线程,改为监听
native.dll加载事件,模块加载完成后拿到JNI导出函数Java_Main_sayHello的地址,在该地址下执行断点,断点命中时再对当前线程调用Stalker.follow() - 配置
Stalker.exclude()规则,显式排除Windows系统模块、JVM核心模块的跟踪范围,避免Stalker预扫描逻辑将跳转到native.dll的路径误判为可信系统路径直接跳过 Stalker.trustThreshold推荐设为2,同时显式标记native.dll的内存地址范围为插桩目标,强制Stalker对该模块内所有代码执行基本块插桩。
内容的提问来源于stack exchange,提问作者DEls
相关产品推荐
相关产品推荐

