Frida 提供了许多构件,可以轻松跨多种操作系统和架构进行可移植插桩。但在不可移植的 场景中,一直有所欠缺。虽然我们提供了 Memory.alloc(Process.pageSize) 和 Memory.patchCode() 等原语,可以分配和修改内存中的代码,却没有任何东西帮助你真正 生成代码,或将代码从一个内存位置复制到另一个位置。
Frida 自身需要生成和转换大量机器码,例如实现 Interceptor 和 Stalker,所以我们 早已有 C API 来处理六种不同的指令集也不足为奇。最初这些 API 太过基础,我认为向 JavaScript 公开它们没有太大价值;但经过多年有趣的内部使用场景,它们已经演进到可以 很好覆盖关键功能的程度。
因此,在 10.4 中,我们终于向 JavaScript 公开所有这些 API。值得一提的是,新绑定会 自动生成,所以未来添加内容会非常轻松。
来看一个 x86 示例:
const getLivesLeft = Module.getExportByName('game-engine.so',
'get_lives_left');
const maxPatchSize = 64; // Do not write out of bounds, may be
// a temporary buffer!
Memory.patchCode(getLivesLeft, maxPatchSize, code => {
const cw = new X86Writer(code, { pc: getLivesLeft });
cw.putMovRegU32('eax', 9999);
cw.putRet();
cw.flush();
});这意味着,我们将目标函数的开头直接替换为:
mov eax, 9999
ret也就是说,假设返回类型是 int,我们刚刚将函数体替换为 return 9999;。
顺带一提,也可以使用 Memory.protect() 更改内存页保护属性,然后在各处写入代码;不过 Memory.patchCode() 非常方便,因为它还会:
- 确保刷新 CPU 缓存;
- 处理 iOS 上代码签名的边缘情况。
以上是一个简单示例。再来尝试一个更疯狂的:
const multiply = new NativeCallback(function (a, b) {
return a * b;
}, 'int', ['int', 'int']);
const impl = Memory.alloc(Process.pageSize);
Memory.patchCode(impl, 64, code => {
const cw = new X86Writer(code, { pc: impl });
cw.putMovRegU32('eax', 42);
const stackAlignOffset = Process.pointerSize;
cw.putSubRegImm('xsp', stackAlignOffset);
cw.putCallAddressWithArguments(multiply, ['eax', 7]);
cw.putAddRegImm('xsp', stackAlignOffset);
cw.putJmpShortLabel('done');
cw.putMovRegU32('eax', 43);
cw.putLabel('done');
cw.putRet();
cw.flush();
});
const f = new NativeFunction(impl, 'int', []);
console.log(f());虽然只是将 42 乘以 7 就绕了不少弯路,但这个例子旨在说明:调用函数(甚至回调 JavaScript)以及跳转到标签,其实都非常容易。
最后,看看如何将指令从一个内存位置复制到另一个位置。正确完成此操作通常比直接调用 memcpy() 复杂得多,因为有些指令依赖位置,需要根据新的内存位置进行调整。看看如何使用 Frida 的新重定位器 API 解决这个问题:
const impl = Memory.alloc(Process.pageSize);
Memory.patchCode(impl, Process.pageSize, code => {
const cw = new X86Writer(code, { pc: impl });
const libcPuts = Module.getExportByName(null, 'puts');
const rl = new X86Relocator(libcPuts, cw);
while (rl.readOne() !== 0) {
console.log('Relocating: ' + rl.input.toString());
rl.writeOne();
}
cw.flush();
});
const puts = new NativeFunction(impl, 'int', ['pointer']);
puts(Memory.allocUtf8String('Hello!'));只用几行代码,我们就制作了自己的 puts() 副本。很棒!
请注意,你还可以插入自己的指令,并使用 skipOne() 有选择地跳过指令,从而执行自定义 插桩。(Stalker 正是这样工作的。)
以上就是核心内容。全新的 API 参考文档位于:
- x86
- arm
- arm64
- mips
还要注意,Process.arch 可以方便地确定应使用哪个 writer/relocator。说到这里,你可能会
疑惑为什么 32 位和 64 位 x86 只有一个实现。原因是这些指令集非常接近,采用统一实现更
合理。这也让编写某种程度上可移植的代码更加容易,因为提供了一些元寄存器名称。例如,
xax 会根据所在进程的类型解析为 eax 或 rax。
尽情使用吧!
oleavr