在喝掉数不清的咖啡、经历许多愉快的编程时光后,@hsorbo 和我很高兴为大家带来 Frida 17.0.0。距离上一次主版本升级已近三年,我们一直苦于找不到合适的时机引入破坏性变更,现在终于决定是时候了。
运行时桥接
长期以来,最困扰我们的一点是运行时桥接,也就是 frida-{objc,swift,java}-bridge,都捆绑在 Frida 的 GumJS 运行时中。这带来了几个主要痛点:
- 惯性:它们受制于 Frida 的发布周期。
- 臃肿:用户可能并不需要某个特定的运行时桥接。
- 可扩展性:我们希望看到面向各种运行时的桥接,但加入 Frida 的桥接越多,惯性和臃肿问题就越严重。
- 可发现性:社区维护的桥接更难被发现,因为使用它们需要另一套工作流。
不过,我一直不愿停止捆绑这些桥接,因为要求自定义 agent 必须经过构建步骤,似乎会增加太多阻力。而且,一想到这会破坏书籍、博客文章和 CodeShare 等处的示例,我也难以接受。
正因为构建阻力,我们在 15.2 中引入了 frida.Compiler API,同时 frida-tools 还附带了基于它构建的 CLI 工具 frida-compile。我们的 REPL 也得到改进,支持直接加载 .ts(TypeScript),其背后使用的正是 frida.Compiler。
但这仍然多了一步,对于一次性脚本,以及使用 Frida REPL 或 frida-trace 进行的早期原型开发来说,这一步还是太麻烦了。而且,它会破坏大量现有示例。为解决这个问题,刚刚发布的 frida-tools 14.0.0 已将这三个桥接内置进 REPL 和 frida-trace agent。
我们的桥接也已迁移到 ESM,因此可以由最新版 frida-compile 使用。(感谢 @yotamN 将 frida-java-bridge 迁移到 ESM ♥️)
从源码构建 Frida 的用户还可能注意到构建速度有所提升。由于不再捆绑这些桥接,我们终于可以移除 Gum 对 frida-compile 的依赖,并让 Gum 本身不再依赖 Node.js + npm。
我们仍然保留 GumJS 自己的运行时,它实现了 console.log() 等内置功能;不过,将它移植到 ESM,并直接逐个嵌入各模块后,我们就不再需要 JavaScript 打包器了。这意味着 Gum 自身的构建速度更快:在运行 Linux 的 i9-12900K 系统上,构建时间从约 24 秒降至约 6 秒。
可以在桥接文档中查看快速参考教程。
旧式枚举 API
过去,我们所有同步枚举 API 都是这样的:
Process.enumerateModules({
onMatch(module) {
console.log(module.name);
},
onComplete() {
}
});此外还有一个名称带 Sync 后缀的等效方法;在这个例子中就是 Process.enumerateModulesSync()。当初的想法是,底层实现未来可能变为异步;但在当时,大多数实现并非异步,所以带 Sync 后缀的实现只是套在那个看起来像异步的 API 外的一层薄包装。
后来,随着支持的平台越来越多,我意识到所有这些假装异步的实现始终都是快速且低开销的操作。因此,提供异步形式毫无意义。而少数从一开始就真正异步的 API,例如 Memory.scan(),继续保持异步仍然合理。
不过,我不愿直接破坏 API,于是选择为每个不带后缀的实现增加一项检查:如果省略回调参数,它就会像带 Sync 后缀的对应方法一样工作。为了推动用户迁离旧式 API,我还更新了 TypeScript 绑定,使其中只包含现代形式。
对应的现代写法如下:
for (const module of Process.enumerateModules()) {
console.log(module.name);
}其中 Process.enumerateModules() 返回一个 Module 对象数组。
这些旧式 API 现在终于全部移除了。使用 TypeScript 编写 agent 的用户无需做任何改动,除非你使用的是非常古老的类型定义版本。
内存读写 API
过去,你会这样访问内存:
const playerHealthLocation = ptr('0x1234');
const playerHealth = Memory.readU32(playerHealthLocation);
Memory.writeU32(playerHealthLocation, 100);现代等效写法是:
const playerHealthLocation = ptr('0x1234');
const playerHealth = playerHealthLocation.readU32();
playerHealthLocation.writeU32(100);每个写入方法都会返回 NativePointer 本身,以支持链式调用:
const playerData = ptr('0x1234');
playerData
.add(4).writeU32(13)
.add(4).writeU16(37)
.add(2).writeU16(42)
;这些旧版 API 现在也已移除;它们从 TypeScript 绑定中消失的时间和旧式枚举 API 一样久。因此,大多数用户应该也不会察觉这项变化。
静态 Module API
接下来介绍同样会影响那些在 19.0.0(与 Frida 17 同时发布)之前一直使用最新版 TypeScript 绑定的用户的破坏性变更。以下静态 Module 方法现已移除:
- Module.ensureInitialized()
- Module.findBaseAddress()
- Module.getBaseAddress()
- Module.findExportByName()
- Module.getExportByName()
- Module.findSymbolByName()
- Module.getSymbolByName()
从这些方法迁移都很直接。
不过,先来看看其中比较特殊的一个:
Module.getSymbolByName(null, 'open')现在要这样实现:
Module.getGlobalExportByName('open')对于其余方法,需要先查找 Module,再访问其上的所需属性或方法。例如,不再这样写:
Module.getExportByName('libc.so', 'open')新的写法是:
Process.getModuleByName('libc.so').getExportByName('open')Module.getBaseAddress() 的等效写法因此是:
Process.getModuleByName('libc.so').base这意味着 Module 内省现在只有一种方式,而且 API 设计会引导你编写高性能代码。例如,过去你可能会想这样写:
const openImpl = Process.getExportByName('libc.so', 'open');
const readImpl = Process.getExportByName('libc.so', 'read');但现在,在写出下面这样的代码之前,你可能会多考虑一下:
const openImpl = Process.getModuleByName('libc.so').getExportByName('open');
const readImpl = Process.getModuleByName('libc.so').getExportByName('read');而会改成这样:
const libc = Process.getModuleByName('libc.so');
const openImpl = libc.getExportByName('open');
const readImpl = libc.getExportByName('read');这种写法既更易读,性能也更好。
最后同样重要的是,Module.enumerateExports() 等静态枚举 API 现在也已移除。不过,它们很早以前就从 TypeScript 绑定中删除了,所以大多数用户应该无需处理。如果确实需要迁移,方式与上面完全相同。
EOF
大致就是这些。祝各位逆向愉快!
oleavr