这次有太多内容可讲。先从一项重要的新功能开始:
Realm
Frida 支持 Android 已有相当长时间,但有一项功能一直被用户反复要求提供——这些用户大多以为自己遇到了错误。对话通常是这样开始的:“我在硬件加速的 Android 模拟器 X 中使用 Frida,附加到进程 Y 时,Process.enumerateModules() 中缺少 JNI 库 Z。但我能在 Process.enumerateRanges() 和 /proc/$pid/maps 中看到它。为什么?”
你可能已经猜到,我们说的是 Android 的 NativeBridge。它通常用于搭载 Intel 处理器的 Android 设备,使其能够运行仅支持 ARM 的应用,也就是包含一个或多个只为 ARM 构建的 JNI 组件的应用。
但在 Frida 的语境中,我们通常谈论的是基于 VirtualBox、运行 x86 Android 系统的模拟器。该系统附带由专有 ARM 转译器 libhoudini 提供的 NativeBridge 支持。
这类模拟器有很多,例如 BlueStacks、LDPlayer、NoxPlayer 等。上述产品主要针对游戏运行进行了优化,而现在 Google 官方的 Android 11 AVD 也已开箱提供 NativeBridge 支持。
多年来,我一直在思考 Frida 该如何支持此类场景,但每次思考都会有点头疼。我确实觉得我们最终应该支持它,只是始终难以确定 API 应有的形态。
然后 2020 年到来,Apple 宣布向 ARM 迁移,Rosetta 突然再次变得重要。“好吧,”我想,“现在已经有两个平台需要支持在进程中容纳运行旧代码的模拟 Realm 了。”
当然还有 Windows,只是我们尚不支持 ARM 上的 Windows。我们确实应该支持;如果有人有兴趣尝试,请务必联系我们。
总之,我很高兴地宣布,我们面向 x86 和 x86_64 的 Android 二进制文件现在可以开箱支持此类进程。你可能已经熟悉下面这个 frida-core API,其 Python 形式如下:
session = device.attach(target)(如果你的代码只处理本地系统,也可以使用 frida.attach()。)
如果 target 存在模拟 Realm,现在可以这样做:
session = device.attach(target, realm='emulated')默认值为 realm='native',而且两个 Realm 实际上可以同时使用。使用我们的命令行工具时,传入 --realm=emulated 即可作用于模拟 Realm。
在 Android 上使用此功能时有一个重要注意事项:Java 层插桩必须在 native Realm 中应用。
最后需要说明,这项新功能目前只在 Android 上得到支持,但未来支持 macOS 上的 Rosetta 应该并不困难。如果你愿意帮忙,请务必联系我们。
将 Android Java 挂钩改为内联方式
此前,Frida 的 Java bridge 在 Android 上替换 Java 方法时,会修改内存中的方法元数据,使目标方法变为 native 方法(如果它原本不是)。这样我们就能安装一个与指定方法 JNI 签名相匹配的 NativeCallback。
这带来了一些挑战,因为 ART 运行时还包含依赖指定方法特性的其他内部状态。我们设计了一些变通方案来绕开部分问题,但仍有一些特别棘手的边缘情况没有解决,其中一个例子就是 ART VM 维护的 JIT 分析数据。
我考虑已久的一个想法是停止修改方法元数据,转而对 AOT 生成的机器代码做内联挂钩——当然仅针对非 native 方法。这样仍未覆盖在虚拟机解释器中运行的方法,但我们假设可以通过挂钩虚拟机内部实现来处理它们。
我做了一个早期原型,进一步探索这种方案。它看起来可行,但仍有许多挑战需要解决。与 @muhzii 头脑风暴之后,他在业余时间继续完善这个粗糙的概念验证。后来有一天,当我看到他刚刚提交的出色拉取请求时,兴奋得差点从椅子上摔下来。
得益于 Muhammed 的杰出工作,现在大家都能在 Android 上享受大幅改进的 Java 插桩体验。这不仅提高了稳定性,也意味着直接调用不再绕过你的替换方法。太棒了!
去优化
对于在 Android 上使用 Java.deoptimizeEverything()、以确保挂钩不会因优化而被跳过的用户,现在有了粒度更细的替代方案。感谢 @alkalinesec 为 Java bridge 带来的优秀贡献,现在可以使用 Java.deoptimizeBootImage()。它确保只有启动映像 OAT 文件中的代码会被去优化。在某些情况下,应用代码本身去优化后会变慢,而可靠触发挂钩又不需要对它去优化;此时这项功能能显著提升性能。
CModule
这里还有一项非常令人兴奋的更新。故事中的下一位英雄是 @mephi42,他开始将 Frida 移植到 S390x。我们的 CModule 实现在底层依赖 TinyCC,而 TinyCC 尚不支持这一体系结构。不过系统中可能装有 C 编译器,因此 @mephi42 提议:在 TinyCC 无法提供帮助的系统上支持使用 GCC。
我非常喜欢这个想法。不仅因为它能扩大体系结构支持,还因为它有潜力生成快得多的代码——TinyCC 优化的是较小的编译器体积和较快的编译速度,而非较快的代码。
不用说,随着每个支持 GCC 的拉取请求到来,我也越来越兴奋。最后一个合入后,它又启发我添加了在 i/macOS 上使用 Apple clang 的支持。
最终我们得到如下用法:
const cm = new CModule(`…`, {}, { toolchain: 'external' });其中 toolchain 可以是 any、internal 或 external。默认值是 any,这意味着如果 TinyCC 支持你的 Process.arch,我们就使用它,否则回退到 external。
不过故事并未结束。在实现 i/macOS 支持时,我并不清楚如何融合 JavaScript 端提供的符号,也就是 CModule 构造函数的第二个参数。
GCC 实现使用链接器脚本,这是一个非常优雅但 Apple 链接器并不支持的方案。随后我突然意识到:我们已经有自己的动态链接器,注入器就在使用它。
接好这部分后,另一个结论也显而易见:我们可以轻松支持完全跳过 Clang,让用户传入预编译的共享库。
这样设计是为了支持交叉编译,同时也使人们能够用 Swift 和 Rust 等语言实现 CModule——基本上,任何能与 C 互操作的语言都可以。
因此我们现在还支持:
const cm = new CModule(blob);其中 blob 是一个包含共享库、用于构造 CModule 的 ArrayBuffer。目前这部分只在 i/macOS 上实现,但目标是在所有平台上提供支持。(欢迎贡献!)
另外,从 frida-tools 9.2 开始,REPL 的 -C 开关也支持此功能,既能方便地使用外部工具链,又不会失去实时重新加载能力,从而大幅缩短开发反馈周期。
再进一步,CModule API 现在还提供 CModule.builtins 属性,脚手架工具可以用它取得内置头文件和预处理器定义。
说到这里,frida-tools 中现在已经有了这样的工具:
$ mkdir pewpew
$ cd pewpew
$ frida-create cmodule
Created ./meson.build
Created ./pewpew.c
Created ./.gitignore
Created ./include/glib.h
Created ./include/gum/gumstalker.h
Created ./include/gum/gumprocess.h
Created ./include/gum/gummetalarray.h
Created ./include/gum/guminterceptor.h
Created ./include/gum/gumspinlock.h
Created ./include/gum/gummetalhash.h
Created ./include/gum/gummemory.h
Created ./include/gum/gumdefs.h
Created ./include/gum/gummodulemap.h
Created ./include/json-glib/json-glib.h
Created ./include/gum/arch-x86/gumx86writer.h
Created ./include/capstone.h
Created ./include/x86.h
Created ./include/platform.h
Run `meson build && ninja -C build` to build, then:
- Inject CModule using the REPL: frida Calculator -C ./build/pewpew.dylib
- Edit *.c, and build incrementally through `ninja -C build`
- REPL will live-reload whenever ./build/pewpew.dylib changes on disk
$ meson build && ninja -C build
…
[2/2] Linking target pewpew.dylib
$ frida Calculator -C ./build/pewpew.dylib
…
init()
[Local::Calculator]->没错,它会实时重新加载!如果做到极致,你可以使用文件监视工具,在 pewpew.c 每次变化时运行 ninja -C build;之后只需保存,就能立即看到插桩在目标进程中生效。
值得注意的是,使用内部 CModule 工具链时也可以采用上述做法。把头文件放在磁盘上,有利于编辑器提供代码补全等功能。
结束语
此外还有许多令人兴奋的变化,请务必查看下面的变更日志。
祝使用愉快!
14.2.0 的变更
- 全新的 Realm API,用于为原生进程内的模拟 Realm 插桩。目前只在 Android 上实现。
- 添加 Java.deoptimizeBootImage()。感谢 @alkalinesec!
- 向 frida-server 添加 –disable-preload/-P。它适用于操作系统兼容性问题,例如 Frida 附加到某些系统进程时会导致其崩溃。
- 修复旧版 Android 上的 libc 检测。
- 修复在 Android 上解析 vDSO 导出时的崩溃。感谢 @ant9000!
- 恢复 Android 上的 libhoudini 支持。
- 修复 Android 11 转译器上的 ARM 缓存刷新。
- 修复 Android 5.x 的链接器偏移。感谢 @muhzii!
- 开始重构 CModule 内部实现,为多个后端做准备。感谢 @mephi42!
- 修复 ARM 上 CModule 的聚合初始化。
- 修复 ModuleApiResolver 快速路径产生错误匹配的问题。
14.2.1 的变更
- 修复 V8 运行时中 CModule 构造函数的错误路径。
- Android 上的 “system_server” agent 改用 V8 运行时。
14.2.2 的变更
- 修复 Darwin.Mapper 对没有 fixup 的页面的 arm64e 处理。这个问题纯粹因为“运气”而一直未被发现,直到我们的二进制文件最终发生足够多的变化才暴露出来。
14.2.3 的变更
- 升级为对 ART 运行时使用内联挂钩。感谢 @muhzii!
- 修复 i/macOS 上的直接传输回归。该问题由 GLib 升级引入,因为 GLib.Socket 在 Apple 操作系统上新增了 GLib.Credentials 支持。典型症状是 frida-server 被 Jetsam 终止。
- 修复 32 位 Windows 上 libffi 对 stdcall、thiscall 和 fastcall 的支持。
- 扩展 Memory.alloc(),支持在指定地址附近分配。感谢 @muhzii!
- 修复 x86_64 上 RIP 相对间接分支的重定位。感谢 @dkw72n!
- 改进 JVM C++ 分配器 API 探测逻辑,在放弃前先查询调试符号。感谢 @Happyholic1203!
- 升级 SELinux 库以支持最前沿的 Android 系统。
- 添加用于生成 GIR 的 gum-linux-x86_64-gir 目标。感谢 @meme!
14.2.4 的变更
- 修复使用 ART 解释器时的 Android 性能回归,例如使用 deoptimizeEverything() 或 deoptimizeBootImage() 时,JS 回调会变得极其频繁。将热点回调移到 CModule 以提高速度。
- 修复 Linux 上 Node.js 绑定中的 V8 调试器支持。
- 修复 libdwarf 后端出现 ELF 初始化错误时的崩溃。
14.2.5 的变更
- 修复 14.2.4 在旧版 Android 系统上引入的回归。
14.2.6 的变更
- 修复与旧版 NativeBridge v3 及更新版本的兼容性;这些版本需要指定命名空间。
14.2.7 的变更
- 修复 printf() 渲染 %p 时不带 “0x” 前缀的系统上 frida-java-bridge 的崩溃。
- 修复 ARM64 上 jni_ids_indirection_ 偏移的解析。感谢 @muhzii!
14.2.8 的变更
- 修复 i/macOS 上 GLib SO_NOSIGPIPE 的回归。该问题通常会导致 frida-server 因 SIGPIPE 而终止。感谢 @mrmacete!
- 重构 CModule 内部实现并为 GCC 后端奠定基础。感谢 @mephi42!
- 为只关心事件、不需要生命周期挂钩或代码转换的 Stalker C API 使用者添加 EventSink.make_from_callback()。
- 在代码块起始处发出 Stalker BLOCK 事件。这最符合直觉,因为通常会期待 BLOCK 事件至少与 COMPILE 事件一样多;这种行为也最适合测量覆盖率。
- 添加 Stalker 预取支持,可用于优化类似 “AFL fork server” 的用例。
14.2.9 的变更
- 在 Darwin CodeSegment 后端处理 permanent 条目。从 iOS 14.3 开始,在 A12+ 设备上,当目标 VM 映射条目标记为 “permanent” 时,mach_vm_remap() 可能返回 KERN_NO_SPACE。感谢 @mrmacete!
- 接入 CModule 的 GCC 支持。感谢 @mephi42!
- 添加 Apple 操作系统上使用 Clang 的 CModule 后端。
- 支持链接预构建的 CModule。(目前仅限 i/macOS。)
- 完成 CModule 工具链选择 API。
- 添加供工具使用的 CModule.builtins 属性。
- 默认生成 frida-core GIR。感谢 @meme!
- 修复 Linux/MIPS 上的回归。
14.2.10 的变更
- 改进 frida-inject,使其支持双向标准输入输出。
- 在 frida-python 中添加 Termux 支持:现在可以运行
pip install frida-tools。
14.2.11 的变更
- 改进 frida-inject,使其支持原始终端模式。
- 为 Darwin 添加内部策略守护进程。
- 改进 Gum.Darwin.Mapper 以支持严格内核。
14.2.12 的变更
- 修复 GC 后 ART 方法挂钩的可靠性。感谢 @muhzii!
14.2.13 的变更
- 修复 x86 上 Instruction 操作数的解析,确保立即数始终由 Int64 而非 number 表示。感谢 @muhzii!
- 修复进程未附加终端时的 frida-inject。感谢 @muhzii!
- 向 CModule 公开 GLib 的 Base64 和 Checksum 原语。感谢 @mrmacete!
14.2.14 的变更
- 修复 Gadget 在 i/macOS 上过早加载时的崩溃。
- 将 frida-inject 的 stdin 通信改为可选。感谢 @muhzii!
- 支持在 unc0ver 6.x 上启动 iOS 应用。感谢 @mrmacete!
- 规避启动 iOS 应用时的单步延迟,避免随机失败。感谢 @mrmacete!
- 修复 libc shim 中 read() 签名不匹配的问题,该问题会在较新的 Apple 工具链上导致编译错误。感谢 @Manouchehri!
- 修复较新版 Android 上 enumerate_applications() 截断名称的问题。感谢 @pancake 报告并协助定位!
- 修复目标无法加载 frida-agent 时的挂起。
- 修复附加 Windows 服务的支持。
- 注册新 Windows 服务前清理陈旧服务。
- 添加构建选项,以便使用已安装的资源而不是将其嵌入。
- 更新 iOS 打包以使用已安装的资源。
- 添加对受限 Android 的基本支持。感谢 @enovella_ 参与愉快而高效的结对编程!
- 扩展 Arm64Writer API 以支持更多立即数。
- 改进 Stalker,使其支持 arm64 上暂时未对齐的栈。
- 修复 Stalker follow() 在没有 sink 时的崩溃。
- 实现 Stalker 失效支持,使插桩可以在不丢弃全部转译代码的情况下更新。感谢 @p1onk 协助!
- 添加 Gum.DarwinModule.enumerate_function_starts()。
- 添加 Gum.DarwinGrafter,用于 AOT grafting,以便预处理二进制文件,使其在无法于运行时修改代码时仍可插桩。感谢 @mrmacete 协助!
- 添加 Memory.allocate_near()。
- 改进 Stalker 在所有受支持体系结构上的性能和稳健性:
- 改进调用探针,使其在目标位置而非调用点探测,并利用新的失效基础设施。
- 支持在调用探针内部添加或移除调用探针。
- 重构 Callout 处理,使用户数据可在失效时销毁,同时移除 Callout 锁。
- 遇到自修改代码时重新编译,而不是分配新代码块。
- 为代码和数据使用独立 slab,避免因元数据存储耗尽而只能使用部分 slab。
- 将第一个代码/数据 slab 内联到 ExecCtx。跟踪很少触及代码的线程时可显著减少内存用量;线程完全没有唤醒时亦然。
- 当 trust_threshold 为 0 时,不再存储原始代码。
- 简化 Stalker 代码块元数据,减少每个代码块的内存占用。
- 修复 ART 上 Java.enumerateMethods() 的结果。此前静态初始化方法会以 ‘$init’ 被错误地纳入枚举结果,实际应完全跳过。感谢 @muhzii!
- 修复 Java 方法挂钩期间使用的 Android/ART 近地址内存分配路径。感谢 @muhzii!
- 修复通用 Java 数组类型的处理。这样,从运行时获取的数组对象可在之后编组数组类型时复用,这对保留类型信息很有必要,尤其是在类型为动态类型时。感谢 @muhzii!
- 将 Android/ART StackVisitor 移植到 x86、x64 和 ARM32。感谢 @P-Sc!
- 修复 Android 上的 ARM 缓存刷新。事实证明,Linux/ARM 上的 cacheflush() 要求传入范围。这个 32 位 Android/ARM 回归由 14.2.0 引入。
- 为 32 位 ARM 添加一些缺失的 TinyCC builtin。感谢 @giantpune 报告并协助定位!
- 修复 Stalker 代码块回收逻辑中的回绕。
- 修复从大数值构造 V8 NativePointer。
- 修复 Windows 上 Stalker 的本地线程操作。
- 修复 CModule 临时目录清理逻辑。
- 移除遗忘的 InspectorServer 调试代码。
- 修复 V8 调试器集成。感谢 @taviso 报告!
14.2.15 的变更
- 修复与最新版 unc0ver iOS 越狱的兼容性。感谢 @mrmacete!
- 添加 Anbox 支持。感谢 @asabil!
- 添加 Java.deoptimizeMethod()。感谢 @liuyufei!
- 处理替换可能被去虚拟化的 ART 方法。感谢 @liuyufei!
14.2.16 的变更
- 为 32 位 ARM 添加许多缺失的 TinyCC builtin。感谢 @giantpune 报告并协助定位!
- 修复 arm64 上使用 ADRP 时 Android ART trampoline 的对齐问题。此前替换某些方法时会抛出
Error: invalid argument异常。感谢 @pandasauce 报告并协助定位!
14.2.17 的变更
- 从 chained fixup 中枚举 Darwin 导入,以支持最新的 arm64e 二进制文件。感谢 @mrmacete!
- 修复受限 iOS 注入器对 chained fixup 的处理。感谢 @mrmacete!
- qml:使用 no_keywords 编译以兼容 GLib。感谢 @suy!
14.2.18 的变更
- 修复较新 XNU 版本上的 i/macOS 注入器:尝试窃取目标进程 POSIX 线程端口的发送权时,mach_port_extract_right() 会以 KERN_INVALID_CAPABILITY 失败。这会使注入器误以为我们已解除注入,随后释放仍在使用的内存。
- 修复受限 iOS 注入器中的 ___error 符号名。感谢 @mrmacete!
- 修复 Linux 后端枚举路径中含空格的模块。感谢 @suy!
- 修复 Stalker 对 x64 直接分支地址的处理。
- python:添加 RPC 导出列表功能。感谢 @NewbieGoose!
oleavr