有些人可能遇到过这种情况:内存中的某段数据看起来很有意思,你想定位负责处理它的代码。你或许尝试过 Frida 的 MemoryAccessMonitor API,却发现页面粒度很难使用。也就是说,可能必须收集大量样本,才有机会捕获访问该页面上特定字节的代码。在使用 4K 页面的系统上已经很困难,在使用 16K 页面的现代 Apple 系统上则更糟。
为解决这个问题,@hsorbo 和我倒满咖啡开始工作,实现了硬件断点和观察点支持。简而言之,Process.enumerateThreads() 返回的线程对象现在提供 setHardwareBreakpoint()、setHardwareWatchpoint(),以及之后用于取消它们的对应方法。将这些方法与 Process.setExceptionHandler() 结合使用:在异常处理器中调用取消方法并返回 true,表示异常已处理,应恢复执行。
演示时间
来实际试用这些新 API。目标选择 id Software 在 2024 年全新再版的 DOOM + DOOM II。

首先要弄清子弹数量存储在内存中的什么位置。编写一个小型 Agent 来帮助完成:
let matches = [];
function scan(pattern) {
const locations = new Set();
for (const r of Process.enumerateMallocRanges()) {
for (const match of Memory.scanSync(r.base, r.size, pattern)) {
locations.add(match.address.toString());
}
}
matches = Array.from(locations).map(ptr);
console.log('Found', matches.length, 'matches');
}
function reduce(val) {
matches = matches.filter(location => location.readU32() === val);
console.log('Filtered down to:');
console.log(JSON.stringify(matches));
}
function patternFromU32(val) {
return new MatchPattern(ptr(val).toMatchPattern().substr(0, 11));
}然后把它加载到游戏中:
$ frida -n doom.exe -l demo.js
…
[Local::doom.exe ]->已知当前有 50 发子弹,因此查找所有包含数值 50 的堆分配;该值以原生 uint32 编码:
[Local::doom.exe ]-> scan(patternFromU32(50))
Found 6947 matches结果相当多。发射一发子弹,再检查哪些位置现在包含数值 49,以此缩小范围:
[Local::doom.exe ]-> reduce(49)
Filtered down to:
["0x1fbf5191884"]找到了!既然知道子弹数量存储在何处,下一步就是找到开火时更新子弹数量的代码。为 Agent 再添加一个辅助函数:
function installWatchpoint(address, size, conditions) {
const thread = Process.enumerateThreads()[0];
Process.setExceptionHandler(e => {
console.log(`\n=== Handler got ${e.type} exception at ${e.context.pc}`);
if (Process.getCurrentThreadId() === thread.id &&
['breakpoint', 'single-step'].includes(e.type)) {
thread.unsetHardwareWatchpoint(0);
console.log('\tDisabled hardware watchpoint');
return true;
}
console.log('\tPassing to application');
return false;
});
thread.setHardwareWatchpoint(0, address, size, conditions);
console.log('Ready');
}调用它:
[Local::doom.exe ]-> installWatchpoint(ptr('0x1fbf5191884'), 4, 'w')
Ready接着切回游戏,再发射一发子弹:
[Local::doom.exe ]->
=== Handler got system exception at 0x7ffc2bc2fabc
Passing to application
=== Handler got single-step exception at 0x7ff6f0a21010
Disabled hardware watchpoint很好,看起来很有希望。来解析该地址的符号:
[Local::doom.exe ]-> ammoCode = ptr('0x7ff6f0a21010')
"0x7ff6f0a21010"
[Local::doom.exe ]-> ammoModule = Process.getModuleByAddress(ammoCode)
{
"base": "0x7ff6f0730000",
"name": "DOOM.exe",
"path": "C:\\Program Files (x86)\\Steam\\steamapps\\common\\Ultimate Doom\\rerelease\\DOOM.exe",
"size": 15495168
}
[Local::doom.exe ]-> offset = ammoCode.sub(ammoModule.base)
"0x2f1010"使用 r2 仔细查看:

可以看到,异常处理器中观察到的程序计数器位于触发观察点的 sub 指令之后一条指令上。
因此可以设置一个内联 Hook,每次开火时都会触发:
Interceptor.attach(Module.getBaseAddress('doom.exe').add(0x2f1010), function () {
const ammoLeft = this.context.rax.add(4).readU32();
console.log(`Shots fired! Ammo left: ${ammoLeft}`);
});[Local::doom.exe ]-> Shots fired! Ammo left: 42
Shots fired! Ammo left: 41
Shots fired! Ammo left: 40
Shots fired! Ammo left: 39
Shots fired! Ammo left: 38同样可以轻松制作无限弹药作弊功能:
Interceptor.attach(Module.getBaseAddress('doom.exe').add(0x2f100d), function () {
this.context.rbx = ptr(0);
console.log(`Shots fired! Pretending no ammo was actually used`);
});[Local::doom.exe ]-> Shots fired! Pretending no ammo was actually used
Shots fired! Pretending no ammo was actually used
Shots fired! Pretending no ammo was actually used
Shots fired! Pretending no ammo was actually used
Shots fired! Pretending no ammo was actually used看,无限弹药!
注意,也可以用 Memory.patchCode() 把 sub 替换为 3 字节 nop 来实现,X86Writer 可通过 putNopPadding(3) 完成。Interceptor Hook 的优势是脚本卸载时会自动回滚,而且便于执行任意代码。
ARM 上的 Windows
此版本另一项亮点是支持 ARM 上的 Windows。这意味着 arm64 版 Frida 可以注入原生 arm64 进程,也可注入模拟的 x86_64 和 x86 进程。
不过我们尚未提供二进制文件,因为仍在等待 GitHub 向开源项目提供 arm64 runner;目前只有 Team 和 Enterprise Cloud 客户可以使用。技术上可以从 x86_64 构建机交叉编译,但我们很快遇到 Meson 的 MSVC 支持问题,因此决定暂缓。
结语
此外还有许多令人兴奋的变化,请务必查看下面的变更日志。
尽情体验吧!
变更日志
- thread:支持硬件断点和观察点。
- fruity:修复 perform_on_lwip_thread() 中的死锁。
- windows:新增 arm64 支持。
- windows:将 Exceptor 迁移到 Microsoft 的 VEH API。
- linux:分离时处理进程已退出的情况。感谢 @ajwerner!
- linux:修复 MIPS 上的 clone() 包装器。
- java:处理 Android GC 周期处理器未导出的情况。感谢 @thinhbuzz!
- java:初步支持 Windows 上的 OpenJDK 17。感谢 @FrankSpierings!
- meson:将 frida-netif 加入公共 frida-core,使 frida-core devkit 包含所有所需符号。
- node:新增便利工厂函数 Cancellable.withTimeout()。感谢 @hsorbo!
- node:新增便利方法 Cancellable.combine()。感谢 @hsorbo!
oleavr