这个版本让我非常兴奋。在 Android 上,我们消除了两大系统不稳定来源:过去对 Zygote 和 system_server 侵入性很强的插桩。感谢 @mrmacete,Apple 平台上的 DebugSymbol 性能也得到显著提升,并包含多项稳定性修复。此外,各方面还普遍增强了稳健性。

Android

下面仔细看看 Android 端的稳定性工作。

在深入 Zygote 和 system_server 之前,先说明一项重要基础工作:感谢 @AeonLucid,SELinux 用户空间库已变基到更新得多的上游版本,在保留 Android M 特有行为的同时支持现代二进制策略格式。

过去,Zygote 处理依赖注入 frida-agent,以便通过子进程门控功能观察 fork() 转换。必须在这里注入代码,是因为除非应用标记为可调试,否则系统不允许以暂停状态启动它。

这种注入代价高昂:它需要停止并重启线程,会在之后的所有子进程中留下痕迹,并且在多个方面都很脆弱。

例如,必须小心隐藏文件描述符,防止 Zygote 调用 abort()。如果 frida-core(通常作为 frida-server 的一部分)恰好在不合适的时机执行注入,例如应用或服务启动期间,就可能使该进程崩溃,并拖垮用户空间的大部分内容。

我们通过 ptrace() 跟踪 Zygote,直到它到达一个被认为表示空闲状态的系统调用,以此缓解问题。但我一直没有为清理实现相同逻辑,所以那部分始终存在风险。

更糟的是,系统调用跟踪机制本身存在错误,会随机失败。我最初打算修复它们,但在发布此版本前有了另一个想法:能否用轻量且专用的方案替代这一切复杂性?

我与 @hsorbo 结对开始工作。最终方案从外部完成全部插桩,完全避开 ptrace(),转而依赖 /proc/$pid/mem:

  1. 打开 /proc/$pid/mem 并扫描相关内存范围,定位 android.os.Process.setArgV0Native() 的 ArtMethod(或 Dalvik 等效项)。这是 libandroid_runtime.so 中实现的 native 方法,因此先确定模块基址,再使用 Gum 的 ELF 解析器(Gum.ElfModule)计算精确函数指针。该方法也非常适合子进程门控:它运行时 SELinux 转换已经完成,软件包名称也已知,并作为新的 argv0 传入。
  2. 不再像以前那样做内联挂钩(技术上仍然可行),而是直接替换方法结构中的函数指针。
  3. 选择一段包含可执行代码的内存范围;其中的代码要么已经运行且不会再次运行,要么只会在 fork() 后运行,例如子进程使用特定多媒体 API 时才会到达的代码。
  4. 将 “zymbiote” 载荷写入该区域。它很小——arm64 上目前为 920 字节——因此无需覆盖太多内容。
  5. 更新方法结构的函数指针,使其指向载荷。
  6. 载荷恢复原始函数指针、调用真实实现,然后连接到抽象 UNIX socket。它发送 PID、PPID 和软件包名称,再等待 ACK。
  7. 如果该软件包有待处理的 spawn() 请求,或已启用启动门控,frida-core 会暂缓发送 ACK,直到应用使用给定 PID 调用 resume()。这样客户端便有机会 attach() 并应用早期插桩。
  8. 如果 connect() 失败或发生 socket 错误,载荷直接返回,因此 frida-core 崩溃不会拖垮用户空间。
  9. 如果通信成功并收到 ACK,载荷尾调用 raise(SIGSTOP)。尾调用对于保证下一步安全十分重要。
  10. frida-core 观察到进程处于停止状态后——意味着它不再于载荷内部执行——会回滚所有更改并发送 SIGCONT。任何未由 Frida 插桩的子进程都会保持完全原始的状态。过去并非如此,一些应用会因为 RASP 系统检测到先前加载 frida-agent 后残留(但已失效)的痕迹而崩溃。

这就是最终采用的方案,结果相当直截了当。整个载荷只有 295 行 C 代码(包括用于尾调用的少量内联汇编)。由于完全不再涉及 ptrace(),与其他工具的互操作性也显著改善。

system_server 端的工作量小得多。我们已有一个用于非 root Android 的小型辅助程序 frida-helper.dex。我们会把它写入临时文件,让 app_process 指向它,并与其通信以枚举已安装应用、运行中进程等。

本版本将该方案通用化:辅助程序现在由 Linux 和 Droidy 后端共享,并扩展支持其他请求类型,例如启动 activity 和发送广播。

除了显而易见的稳定性收益,这也意味着 frida-core 不再依赖 frida-java-bridge。因此,未来操作系统版本或 Play Store 更新引入的 libart.so 兼容性问题不会损害 Frida 的核心功能。

不再向 system_server 注入内部 agent 的一个缺点是,我们失去了禁用系统默认应用启动超时的能力。我认为可以接受,因为必要时仍可向 system_server 注入一个小脚本来实现同样效果。

结束语

本版本还包含许多其他改进。请务必查看下面的变更日志了解完整细节。

祝使用愉快!

变更日志

  • android:将 SELinux 用户空间库变基到更新的上游版本,在保留 Android M 特性的同时支持现代二进制策略格式。(感谢 @AeonLucid)
  • android:改用轻量级 Zygote 挂钩:修补 android.os.Process.setArgV0Native(),使其通过小型载荷跳转并回连 frida-core 进行插桩。此方案取代此前在 Zygote 中放置内部 agent 的做法,并移除对 frida-java-bridge 的依赖。(共同作者:@hsorbo)
  • android:全面迁移到 frida-helper.dex,消除向 system_server 注入代码的需求。
  • libc-shim:修复长期存在的 Android SELinux getline() 分配器不匹配问题,该问题会导致堆损坏和未定义行为。
  • darwin:通过重写的解析器加快按地址查找 Objective-C 方法,并用 GumModuleRegistry 信号替换 symbolutil 缓存失效器。后者修复了交换 dyld 通知场景中的 SIGBUS。(感谢 @mrmacete)
  • fruity:在关闭状态下执行 TCP 写入时抛出 CLOSED,避免无限轮询循环。(感谢 @mrmacete)
  • fruity:修复可能导致 USB 工作线程死锁的 USB 启动/关闭竞态,使枚举和关闭过程具有确定性。
  • linux:修复 ptrace 信号等待器转发和系统调用跟踪不同步,使存在 ptrace 内部停止和真实信号时的 spawn/attach 流程更加稳健。
  • linux:修复 arm64 ucontext 记录解析。(感谢 @MarlinDiary)
  • android:处理新版 Android 中的 __pthread_start 符号后缀,使线程枚举不再错误地把系统视为不受支持。(感谢 @MarlinDiary)
  • android:在 enumerateRanges() 中处理 APK 库。(感谢 @monkeywave)
  • arm64:不可行时避免尝试 Interceptor 快速修补。(感谢 @Jiay1C)
  • interceptor:添加 FORCE attach 标志,即使函数太小而无法安全修补,也允许内联挂钩。这可能覆盖函数末尾之后的字节,应谨慎使用。
  • elf:必要时强制挂钩 RTLD 通知器,并改进 ELF 模块哈希解析和验证,包括正确解析 ELF32 的 GNU 哈希。
  • frida-node:使用 Symbol 描述而非强制转换,避免 XPC 相关用法中的 “Cannot convert a Symbol value to a string” 错误。(感谢 @mrmacete)