这是一个内容丰富的版本,有太多值得介绍的内容。
V8
本次发布的主角是 V8。不过先介绍一点背景。
Frida 默认使用 Duktape JavaScript 运行时,以脚本方式访问其插桩核心 Gum。 最初我们只使用 V8,但过去 V8 依赖操作系统支持 RWX 页面,即同时可写和可执行的 内存页,因此最终又添加了基于 Duktape 的第二套 JS 运行时。考虑到 V8 的这一系统限制, 以及 Duktape 不支持最新 JS 语法和功能的事实,我决定把 Duktape 设为默认运行时, 使针对一个平台编写的脚本无需修改语法即可在其他平台运行。Duktape 基本上就是最低 共同标准。
如今 V8 已不再依赖操作系统支持 RWX 页面,实际上正转向默认在 RW- 与 R-X 之间 切换。这使它可用于现代 iOS 越狱环境;在未越狱 iOS 上,只要进程被标记为正在调试、 能够运行未签名代码,也可使用。不仅如此,V8 现在甚至能以 JIT-less 模式运行, 这意味着 Frida 可在每个平台上使用 V8,用户也无需为了使用最新 JavaScript 语法和功能 而通过 frida-compile 编译 agent。不过最后一点只适用于简单 agent;对于复杂 agent, 将其拆分为多个源文件仍很有价值。此外,frida-compile 便于使用 TypeScript,强烈 建议所有复杂的 Frida agent 都采用它。
基于这些变化,显然该把 V8 升级到最新版本了。本版本使用 7.6.48,与 V8 的集成也 比以往深入得多。C++ 内存分配和页级分配现在都由 Frida 管理,因此可以向 Process.enumerateRanges() 等 API 隐藏这些内存范围,也避免用 Frida 自身的分配污染 应用堆。这些细节听起来或许并不重大,但对于基于 Frida 实现内存转储工具至关重要。 此外,我们对被观察进程的干扰也更少,因而降低了进程在插桩状态下表现出不同于正常运行 行为的风险。
运行时选择
你可能还记得 session.enable_jit() API。它终于被弃用了,因为现在可在创建脚本时指定 所需运行时。例如使用 Python 绑定:
script = session.create_script(source, runtime='duk')使用 Node.js 绑定则是:
const script = await session.createScript(source, {
runtime: 'v8'
});Stalker
本版本的另一项重大变化是:得益于 John Coates 的出色贡献,Stalker 在 arm64 上不再依赖 RWX 页面。这意味着 Stalker 终于更容易在 iOS 上使用。
对于在 64 位 Windows 上使用 Stalker 跟踪 32 位进程的用户,它终于能处理新版 Windows 中的 WOW64 转换。这项烧脑的改进由 Florian Märkl 贡献。
Module.load()
有时你可能想加载自己的共享库,其中或许包含用 C/C++ 编写的 hook。在大多数平台上, 可使用 NativeFunction 调用 dlopen()(POSIX)或 LoadLibrary()(Windows)。 但新版 Android 的情况大不相同:其 dlopen() 实现会检查调用者并据此作出决定,其中 一项就是判断应用是否试图访问私有系统 API,因为这会让系统日后难以移除或破坏该 API。 因此从 Android 8 开始,遇到这种情况会返回 NULL。Frida 已为自身注入器解决这一难题, 但想加载自有库的用户此前基本只能自行处理。
从 Frida 12.5 开始,全新的 JavaScript API 会为你处理所有平台特有问题:
const hooks = Module.load('/path/to/my-native-hooks.so');
Interceptor.replace(Module.getExportByName('libc.so', 'read'),
hooks.getExportByName('replacement_read'));Android
本版本修复了大量 Android 特有问题。例如,对应用自带库调用 Module.getExportByName() 时,不再导致该库以不同基址被二次加载。仅这一项修复就足以 让你把所有设备升级到最新版。
iOS
得益于 Francesco Tamagni 的出色贡献,现在也支持 iOS Chimera 越狱。
其他变更
各平台还有许多其他改进。
按时间顺序如下:
- Child gating 也可在旧版 Windows 上工作。感谢 Fernando Urbano!
- UNIX 操作系统上的可执行文件不再导出动态符号,因此体积更小。
- Frida agent 和 gadget 加载到高地址(即设置了 MSB)时,不再于 32 位 Linux 上崩溃。
- Linux/Android 的两个 frida-helper-{32,64} 二进制文件只需其中一个;不支持跨架构的 构建则都不需要。这减小了占用并提高性能。
- 终于支持 Linux/ARM64,并在发布流程中上传二进制文件。
- Android 上早期插桩 Zygote 失败时,现在会提示 Magisk Hide。
12.5.1 中的变更
- 可为每个脚本指定运行时,enable_jit() 因而弃用。
12.5.2 中的变更
- 在 Linux 和 Android 上使用 V8 时,Gadget 不再于加载脚本时崩溃。非常感谢 Leon Jacobs 报告并协助定位。
12.5.3 中的变更
- Android linker 集成支持更多设备。
- Android Java 集成不再于部分 arm64 设备上因“Invalid instruction”崩溃。感谢 Jake Van Dyke 报告并协助定位。
- 添加缺失的 SELinux 规则后,现在支持 LineageOS 15.1。
12.5.4 中的变更
- hook 不再干扰 V8 页分配器集成。
- 修补 libc shim 中的漏洞后,Android 稳定性大幅提高。非常感谢 Giovanni Rocca 报告并协助定位!
12.5.5 中的变更
- Windows 现在能正确检测 Apple USB 设备。感谢 @xiofee!
12.5.6 中的变更
- Android Java 集成现在可规避 ART 异常传递逻辑中的错误:某条特定代码路径假设当前线程 至少存在一个 Java 栈帧,但 Frida JS 线程等纯原生线程并非如此。最简单的复现方式是 调用 Java.deoptimizeEverything(),随后用不存在的类名调用 Java.use()。感谢 Jake Van Dyke 报告并协助定位。
- 在无法监听 TCP 的进程中调用 Java.deoptimizeEverything() 时,Android Java 集成 不再导致进程崩溃。
- Android Java 集成重新支持 JNI checked 模式。
- 除 8 和 10 外还支持 Node.js 12,并为所有受支持平台提供预构建包。
- Node.js 绑定的 enableDebugger() 方法不再要求指定监听端口。
12.5.7 中的变更
- 在无法启动 logcat 生成崩溃报告的系统上,Android teardown 不再崩溃。
- 改进 Android 上的 SuperSU 集成 teardown 逻辑。
- Android Java 集成现已正确支持 JNI checked 模式,大幅提高 Android ROM 兼容性。 感谢 @muhzii 报告并协助测试。
- V8 后端 teardown 不再出现释放后使用,延迟绑定 WeakRef 时也不再崩溃。
12.5.8 中的变更
- Linux child-gating 现在能处理子进程切换架构,例如 32 位应用通过 fork+exec 运行 64 位可执行文件。非常感谢 @gebing 提供修复。
- fork+exec 后未跟踪子进程时,child gating 不再死锁。感谢 @gebing 修复。
- 在 Android 应用自有模块上查找模块导出不再失败。
12.5.9 中的变更
- libc shim 现在包含 memcpy(),使其可以被安全挂钩。感谢 Giovanni Rocca 调试并 贡献修复。
- 即使线程暂时释放了 JS 锁(例如调用 NativeFunction 时),Interceptor.flush() 也能正常工作。
- Android Java 集成不再于 ART 异常传递期间偶发崩溃,例如挂钩 ClassLoader.loadClass() 时。感谢 Jake Van Dyke 和 Giovanni Rocca 协助定位。这个问题自支持 ART 起就一直存在,值得庆祝修复成功。🎉
- 无法启动 JDWP 传输的进程中,Android Java 集成不再导致崩溃。
oleavr