这是一次重大的新版本发布,背后是数周高强度编码和多得有些过分的咖啡。不过在深入介绍之前,我们需要先快速回顾一下过去。

多年来,基于 V8 的运行时一直很好地服务着我们。但最终,我们需要支持那些并不适合运行 V8 的受限系统,于是引入了第二个运行时。

它运行得不错,但也留下了一些取舍:

  • 两个运行时所支持的语言特性差异巨大。我们试图通过把精简运行时设为默认值来缓解部分问题,因为它随处可用,也是功能方面的最低共同标准。
  • 使用 frida-compile 等工具将现代 JavaScript 编译为可在两个运行时上执行的旧版 JavaScript 时,必须牺牲性能。
  • 当非简单 agent 中充斥大量代码和数据时,不仅能明显看出 V8 有多快——这并不意外——还能看出它很擅长紧凑地组织对象,避免浪费宝贵的 RAM。更拉大两个运行时差距的是,V8 可以直接运行现代 JavaScript,无需执行一个臃肿版本,后者要包含兼容垫片来补齐 Map、Set 等缺失的运行时功能。
  • 示例代码和文档往往显得晦涩,只为避免让那些可能尝试在默认运行时上执行现代代码的用户感到困惑。
  • 垃圾回收器实现上的差异,可能会在一个运行时中掩盖用户的错误,而在另一个更积极释放资源的运行时中立即爆炸。一个例子是:外部代码仍在使用 NativeCallback 时,却没有让它保持存活。
  • 糟糕的用户体验:以上种种汇总起来,会形成一个极其令人沮丧和困惑的使用故事。
  • 新功能和改进必须实现两次。显而易见,这对维护者来说非常痛苦。

时间快进到 2019 年,QuickJS 引起了我的注意。不过当时我正忙于其他事情,等到后来仔细研究时,我发现它支持 ES2020,而且作为解释器,性能也令人印象深刻。

但一想到要从头实现新的运行时,再看看另外两个运行时各自大约有 2.5 万行代码,这项工作就让人不堪重负。

可我还是不断回到 QuickJS 网站,仔细研读技术细节,甚至一度开始深入阅读其公共 API。

随后我注意到,它不支持协作式多线程使用,也就是让多个线程步调一致地执行 JavaScript。这让前方的工作显得更加艰巨;但我又想起自己已经为 Duktape 贡献过这项支持,而且并没有那么难。

最终我鼓起了勇气。从 GumJS 庞大的测试套件中挑了一个极其简单的测试作为第一个挑战,然后从现有两个运行时中较新的那个复制粘贴 ScriptBackend 和 Script 实现。先重命名,再把所有模块(Interceptor、Stalker 等)存根化,只想先让一个几乎为空的“外壳”能够编译并运行。

到了这一步,我彻底上瘾,根本停不下来。喝掉了大量咖啡,不知不觉就实现了核心部分和第一个模块,随后又是一个,再来一个。

在深入使用 QuickJS API,并反复查看其内部实现以确保理解引用计数规则等细节后,实现协作式多线程 API 所需的工作突然变得非常清晰。只有做到这一点,它才会成为真正的运行时,而不只是玩具。

我们需要能够在调用 NativeFunction 时暂停 JS 执行。这是因为被调用函数可能会阻塞等待另一个线程已经持有的锁,而另一个线程可能刚刚调用了被 hook 的函数,正在等待进入 JS 运行时。因此,如果调用 NativeFunction 前不释放 JS 锁,就会发生死锁。

另一个场景是调用 Thread.sleep() 或其他阻塞 API;如果持有 JS 锁执行它们,就会造成线程饥饿。

总之,QuickJS 多线程 API 最终实现起来很直接,于是我继续推进,直到终于全部完成!🎉

这时我非常好奇这个全新运行时的性能,首先想知道进入和离开运行时的成本。

我在 iPhone 6 上试运行了 GumJS 的一个测试:使用 Interceptor hook 一个几乎为空的 C 函数,提供空的 JS 回调,然后反复调用该 C 函数,并测量每次调用所用的实际时间。

其思路是模拟用户 hook 高频调用函数时的情况,以了解基础开销。

结果如下:

# QuickJS
<min: 1.0 us, max: 7.0 us, median: 2.0 us> ok 1 /GumJS/Script/Interceptor/Performance/interceptor_on_enter_performance#QJS
<min: 2.0 us, max: 54.0 us, median: 2.0 us> ok 2 /GumJS/Script/Interceptor/Performance/interceptor_on_leave_performance#QJS
<min: 3.0 us, max: 18.0 us, median: 3.0 us> ok 3 /GumJS/Script/Interceptor/Performance/interceptor_on_enter_and_leave_performance#QJS
# Duktape
<min: 2.0 us, max: 8.0 us, median: 3.0 us> ok 4 /GumJS/Script/Interceptor/Performance/interceptor_on_enter_performance#DUK
<min: 2.0 us, max: 6.0 us, median: 3.0 us> ok 5 /GumJS/Script/Interceptor/Performance/interceptor_on_leave_performance#DUK
<min: 4.0 us, max: 89.0 us, median: 4.0 us> ok 6 /GumJS/Script/Interceptor/Performance/interceptor_on_enter_and_leave_performance#DUK
# V8
<min: 13.0 us, max: 119.0 us, median: 14.0 us> ok 7 /GumJS/Script/Interceptor/Performance/interceptor_on_enter_performance#V8
<min: 15.0 us, max: 127.0 us, median: 16.0 us> ok 8 /GumJS/Script/Interceptor/Performance/interceptor_on_leave_performance#V8
<min: 26.0 us, max: 198.0 us, median: 28.0 us> ok 9 /GumJS/Script/Interceptor/Performance/interceptor_on_enter_and_leave_performance#V8

哇,这看起来很有希望!那么基线内存占用呢,也就是运行时单个实例本身会消耗多少内存?

QJS 内存基线

提升相当明显——只有上一个运行时的五分之一!

接下来,我想了解使用 REPL 时 Frida 内部堆的近似初始大小。其中包括 frida-agent、JS 运行时以及所加载 REPL agent 使用的全部内存:

QJS REPL 内存

太棒了,为其他用途腾出了 1 MB!

说到这里,希望你也和我一样对这个新版本感到兴奋。我们已经用这个基于 QuickJS 构建的全新运行时替换了先前的默认运行时。

作为实验,我还决定在构建官方二进制文件时不包含 V8 运行时。这意味着二进制文件比以往任何时候都小得多。

我知道有些使用场景可能离不开 V8 运行时,所以希望你尝试一下新的 QuickJS 运行时,并告诉我它的表现。如果它在你的特定场景中彻底失灵,也不必担心,请告诉我,我们会一起想办法。

如果希望自行构建启用 V8 运行时的 Frida,只需调整这一行。但如果你确实离不开它,请务必告诉我,以便我们决定未来是否还需要继续支持这个运行时。

这个大版本的另一项变更只影响 i/macOS:我们终于跟随 Apple 的脚步,放弃对 32 位程序的支持。目前仍会保留相关代码路径,但官方二进制文件已经轻量得多,顶层构建系统也有所精简。例如,make core-macos-thin 现在只是 make core-macos。

Frida 本身的变化就这些,但还有更多。我们还发布了 frida-tools 9.0,经过全新升级,现在可以到处使用现代 JavaScript 特性。其中包括 frida-trace,语法升级后,其生成的 hook 模板可读性大幅提高。最后,我们还发布了 frida-compile 10.0:Babel 依赖以及对应的命令行开关都已移除,速度更快,也简单得多。

希望你喜欢这个新版本!

14.0.0 的变更

  • 使用基于 QuickJS 的全新 GumJS 运行时替换默认运行时。
  • 默认禁用 V8。
  • 在 V8 上保留 Interceptor.attach() 的回调对象。
  • 从全局访问 API 中移除“enumerate”陷阱。

14.0.1 的变更

  • QJS:修复嵌套的全局访问请求。
  • qml:更新到新的 frida-core API。

14.0.2 的变更

  • QJS:在调用期间让 NativeCallback 保持存活。
  • QJS:加速 NativeCallback 构造逻辑。
  • QJS:暂时禁用堆栈限制。
  • iOS:将 iOS 崩溃报告器集成移植到 iOS 14。
  • iOS:移除 32 位打包逻辑。
  • Android:为“system_server”agent 使用默认运行时。
  • 现代化内部 JavaScript agent。

14.0.3 的变更

  • 在 Windows 上也禁用 V8。
  • iOS:改进打包脚本。

14.0.4 的变更

  • iOS:修复工具链升级引起的 arm64e 回归。

14.0.5 的变更

  • QJS:修复 Interceptor 错误处理。

14.0.6 的变更

  • ObjC:修复被替换方法的生命周期,使其不再与类包装器绑定,并在链式使用场景中保持存活。感谢 @Hexploitable 和 @mrmacete 的协助!
  • 修复 act == oact 时 Exceptor sigaction() 注册失败的问题。感谢 @hluwa!
  • 改进 Linux libc 检测。
  • 修复在 Linux 上枚举和修改线程时偶发的挂起。
  • 修复 PC 与 CPSR Thumb 位处理不一致的问题。
  • 修复 Linux/armhf 和 Linux/arm64 上的构建回归。
  • 发布适用于 32 位和 64 位 Raspberry Pi 的二进制文件。

14.0.7 的变更

  • 避免在执行 JS 代码期间崩溃时发生死锁,例如调用带有 exceptions: 'propagate' 的 NativeFunction,或 GumJS 本身存在错误时。感谢 @mrmacete!
  • 修复 macOS/arm64 上的 CModule。
  • 发布适用于 32 位 Raspberry Pi 的 Python 和 Node.js 二进制文件。
  • 发布 Fedora 33 而非 Fedora 32 的二进制文件。
  • 发布 Ubuntu 20.10 的二进制文件。

14.0.8 的变更

  • 通过在上传连接中加入一些双向通信,提高受限 iOS 上的上传可靠性。这可以防止复杂远程配置中的 gadget 上传触发 DoS 保护。感谢 @mrmacete!