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 参考文档位于:

还要注意,Process.arch 可以方便地确定应使用哪个 writer/relocator。说到这里,你可能会 疑惑为什么 32 位和 64 位 x86 只有一个实现。原因是这些指令集非常接近,采用统一实现更 合理。这也让编写某种程度上可移植的代码更加容易,因为提供了一些元寄存器名称。例如, xax 会根据所在进程的类型解析为 eax 或 rax。

尽情使用吧!