准备迎接一个令人兴奋的新版本。这一次,我们终于为长期缺少关爱的 Stalker 引擎 带来大量改进。它已经存在约十年,但直到 2017 年末发布 Frida 10.5 时,我们才 开始释放它的巨大潜力。

此前,我们可以对现有线程调用 Stalker.follow(),不仅观察线程,还能以任意方式修改其 指令流。它也可以与 Interceptor 结合,在选定的关键点之间对当前线程进行插桩。这使我们 得以构建 AirSpy 等工具。

但是,如果想对一次 NativeFunction 调用执行 Stalker.follow() 呢?这看起来很简单, 重入问题却让它极其困难。我们很容易一路跟踪到私有堆等内部执行路径,随后又需要为插桩 本身分配内存……由此产生的各种场景复杂得令人头疼。

我们的解决办法是让 Stalker 能够排除特定内存范围。发现调用进入这类位置时,它只会在 那里发出一条调用指令,而不会继续跟踪执行。我们自动排除了 frida-agent 自身的内存范围, 从而无需处理那些棘手的重入问题。

我们还专门处理了对当前线程调用 Stalker.follow() 的情况:将这项工作排队,直到即将离开 我们的运行时并返回用户代码时再执行;对于 JS 线程,则是在返回主循环时执行。

但如何结合使用 Stalker 与 NativeFunction 仍是一个悬而未决的大问题。现在终于解决了:

const open = new NativeFunction(
    Module.getExportByName(null, 'open'),
    'int', ['pointer', 'int'],
    { traps: 'all' }
);

Stalker.follow({
  events: {
    call: true
  },
  onReceive(e) {
    console.log(JSON.stringify(Stalker.parse(e)));
  }
});

const fd = open(Memory.allocUtf8String('/foo/bar'), 0);
console.log('open() =>', fd);

为 NativeFunction 设置 traps: 'all' 选项后,如果调用来自一个因进入排除范围而暂时 暂停 Stalker 的线程,它就会重新激活 Stalker。本例正是如此,因为 frida-agent 的所有 代码都被标记为排除。

Objective-C 方法也能实现相同效果:

Stalker.follow({
  events: {
    call: true
  },
  onReceive(e) {
    console.log(JSON.stringify(Stalker.parse(e)));
  }
});

const NSAutoreleasePool = ObjC.classes.NSAutoreleasePool;
const NSFileManager = ObjC.classes.NSFileManager;

const fileExistsAtPath = NSFileManager['- fileExistsAtPath:']
    .clone({ traps: 'all' });

const pool = NSAutoreleasePool.alloc().init();
try {
  const manager = NSFileManager.defaultManager();
  const result = fileExistsAtPath.call(manager, '/foo/bar');
  console.log('fileExistsAtPath() =>', result);
} finally {
  pool.release();
}

Android 上的 Java 方法也一样:

Stalker.follow({
  events: {
    call: true
  },
  onReceive(e) {
    console.log(JSON.stringify(Stalker.parse(e)));
  }
});

Java.perform(() => {
  const JFile = Java.use('java.io.File');
  const exists = JFile.exists.clone({ traps: 'all' });

  const file = JFile.$new('/foo/bar');
  const result = exists.call(file);
  console.log('exists() =>', result);
});

太棒了。不过,这些示例只触及了 Stalker 能力的表面。进程内模糊测试是一个非常酷的 用例,frida-fuzz 就是很好的例子。其他用例还包括逆向分析、代码覆盖率测量、用于 测试的故障注入、挂钩内联系统调用等。

这就是本次发布的主线。感谢 @andreafioraldi 提供出色的错误报告,并帮助测试这些 棘手的变更。

收尾

另一个值得一提的新功能是 ArrayBuffer.wrap() API,它让你能够像访问 JavaScript 数组一样,方便而高效地访问内存区域:

const header = Memory.alloc(16);

const bytes = new Uint8Array(ArrayBuffer.wrap(header, 16));
bytes[0] = 1;
bytes[0] += 2;
bytes[1] = 2;

console.log(hexdump(header, { length: 16, ansi: true }));
console.log('First byte is:', bytes[0]);

这意味着可以把直接内存访问交给 JavaScript API,无需在运行时内外复制内存。唯一缺点是, 无效指针不会产生 JS 异常,而会导致进程崩溃。

现在还可以通过 ArrayBuffer 新增的 unwrap() 方法访问任意 ArrayBuffer 的后备存储。 例如,使用 frida-fs 等现有模块获得 ArrayBuffer 后,可以将其传给原生代码。

感谢 @DaveManouchehri 贡献 ArrayBuffer.wrap() API 的初稿,也特别感谢 @CodeColorist 提议并协助完善 unwrap() 功能。

12.8.0 中的变更

  • Stalker 重新激活功能可正常工作。
  • 正确处理 Stalker 线程生命周期;在 i/macOS 上跟踪线程直至其终止时不再崩溃。
  • Stalker 采用更安全的垃圾回收逻辑。
  • Stalker transform 回调出错并抛出 JS 异常时,现在会触发 Stalker.unfollow(),避免错误 因进程崩溃而被吞掉。
  • 稳健支持从 Stalker transform 中调用 unfollow()。
  • Stalker 支持不具备 AVX2 的旧款 x86 CPU。
  • 支持禁用 Stalker 队列自动排空。
  • NativeFunction 通过全新的 Interceptor 栈展开 API 更好地处理异常。
  • Java 和 ObjC API 可为方法通过 clone(options) 指定 NativeFunction 选项,并可通过 ObjC.Block() 的第二个参数为 block 指定选项。
  • ObjC 类和协议缓存逻辑终于可以正常工作。感谢 @gebing!
  • Windows 预构建 Python 3 扩展终于像其他平台一样,支持 Windows 上所有 >= 3.4 的 Python 3 版本。
  • ArrayBuffer wrap() 与 unwrap()。
  • DebugSymbol API 在 Linux/Android 上具备更好的错误处理。
  • Android 10 系统进程中的 Java 集成不再于 recompileExceptionClearForArm64() 崩溃。
  • i/macOS 上的 GumJS devkit 再次支持 V8。

12.8.1 中的变更

  • CModule 的 Stalker 集成恢复正常。

12.8.2 中的变更

  • Thumb IT 块终于能够正确重定位。这意味着可在 Android 等 32 位 ARM 目标上挂钩更多 函数。感谢 @bigboysun!

12.8.3 中的变更

  • 引入 Java.ClassFactory.get(),以便同时使用多个类加载器而无需担心类名冲突。因此, 给 loader 属性赋值现已视为弃用。为保持向后兼容仍会保留该属性,但不支持与新 API 同时使用。
  • Java.enumerateLoadedClasses() 除名称外也会提供类句柄。
  • JNI 的 GetByteArrayRegion() 函数现已纳入 Env 包装器。感谢 @iddoeldor!

12.8.4 中的变更

  • 当 PLT/GOT 条目尚未预热时,内部 hook 不再导致 Linux/ELF 目标崩溃。

12.8.5 中的变更

  • Python 绑定终于能在 Python 2.x 上提供正确编码的错误消息。

12.8.6 中的变更

  • Android linker 检测终于能在沙箱进程中再次正常工作。这是 12.7.8 引入的回归。 感谢 @DaveManouchehri 报告并协助定位!

12.8.7 中的变更

  • Node.js IOStream 绑定获得两项关键稳定性改进。取消逻辑中存在竞态条件,导致 cancellable 并非总会被使用;teardown 逻辑中也有一个错误,可能在所有 I/O 操作完成 前关闭流。感谢 @mrmacete 提供这些出色修复!

12.8.8 中的变更

  • 在 Android/Linux 的早期插桩场景中,即使调用 Gadget 入口点时持有动态链接器锁, Gadget 也不再死锁。由于 Exceptor 现在使用 dlsym() 避免早期插桩期间的 PLT/GOT 问题,必须确保从入口点线程而非 Gadget 线程初始化 Exceptor。

12.8.9 中的变更

  • Stalker 的 JavaScript 集成不再于 EventSink::stop() 中发生释放后使用,即 Stalker.unfollow() 之后的释放后使用。

12.8.10 中的变更

  • Gadget 再次能够在没有调试器的 iOS 上运行。这是 12.8.8 引入的回归。感谢 @ddzobov 报告!

12.8.11 中的变更

  • 当 Mach 异常处理 API 的使用者只请求部分处理器时,i/macOS Exceptor 的 API hook 不再发生越界写入。这类使用者通常是崩溃报告器或分析框架。
  • 现在为 Electron v8(稳定版)和 v9(测试版)提供预构建包,不再为 v7 提供。

12.8.12 中的变更

  • 大幅重构 Android Java 集成,现在使用 Proxy 对象和 CModule 延迟解析内容;也不再使用 eval 动态生成方法与字段包装器,因此每个生成的包装器所需内存更少。这些变更降低了 内存用量,并让 Java.use() 更快完成。
  • 在试图隐藏私有 API 的 Android 版本(即 Android >= 9)上,Android Java 集成可不受 限制地访问方法和字段。
  • Android 设备枚举速度大幅提高。当本地 ADB 守护进程足够新(即 2017 年某个时间点之后 的 ADB)时,不再运行任何 adb shell 命令来确定设备名称。
  • 终于消除了 Linux 类操作系统上一个长期存在的内存泄漏,它会影响新版 Android 中 zygote 和 system_server 等受限进程。问题出在线程退出后不久回收线程局部数据的 逻辑:判断线程确实完成退出的机制会失败,永远不认为线程已经消失。这会造成垃圾不断 堆积,需要遍历的垃圾集合越来越长。我们不仅会在徒劳的 GC 尝试上花费越来越多时间, 还会因每 50 毫秒重试一次 GC 而消耗 CPU。
  • Python 绑定允许从 Cancellable 获取文件描述符,以便集成到事件循环和其他 poll() 风格的场景中。值得一提的是,frida-tools 7.0.1 已基于此带来一项重大改进:CLI 工具 退出前不再延迟最多 500 毫秒,因此 frida-ls-devices 和 frida-ps 等短生命周期 程序现在响应非常迅速。
  • Duktape 的 source map 处理现在也适用于 REPL 加载的脚本。由于 REPL 会附加自己的代码, 内联 source map 并不位于脚本最后一行;现在堆栈跟踪始终包含有意义的文件名和行号。
  • Duktape:内置 JavaScript 运行时(即 GumJS 胶水代码、ObjC 和 Java)现在使用启用 loose 选项的 Babel 处理,以减小体积并提高性能。API 不会泄漏现代 JavaScript 数据结构,因此无需让 Babel 完全符合规范。
  • V8:压缩内置 JavaScript 运行时,以减小占用并提高代码速度。此前仅对 Duktape 执行。
  • 改进 enumerate_processes() 中的 Linux 进程名启发式规则。

12.8.13 中的变更

  • Java.performNow() 恢复正常工作。
  • Python 绑定的 setup.py 现在会先查找本地 .egg,再尝试下载,并要求下载在两分钟内 完成。感谢 @XieEDeHeiShou 提供这些改进!

12.8.14 中的变更

  • 现在正确支持 iOS 模拟器,包括 Gadget 形式,以及从 macOS 附加到运行中的模拟器进程。 感谢 @insitusec 协助修复这些问题!
  • Gadget 现在也会在 iOS 上向上一级目录查找 .config,但仅当其父目录名为 “Frameworks”时如此。感谢 @insitusec 的建议!

12.8.15 中的变更

  • 全新且功能完整的 iOS/arm64e 支持,包括新的 NativePointer 方法:sign()、 strip()、blend()。
  • 现在支持最新版 iOS Unc0ver 越狱。感谢 @mrmacete 提交拉取请求,并感谢 @Pwn20wnd 协助!❤️
  • 改进 Chimera 越狱支持,确保初始化 pspawn_payload-stg2.dylib。感谢 @mrmacete!
  • 当 agent 入口点立即返回时,i/macOS 注入器不再失败。
  • 改进受限 iOS 需要 Gadget 时的错误消息。
  • 改进 Windows 注入器的错误处理,避免 DLL 注入失败时导致目标进程崩溃。感谢 @dasraf9!
  • 支持注入 i/macOS 上仍在运行的新生目标;同时不再一律认为挂起进程需要为注入做准备。
  • 改进 iOS 容错,可处理查询最前端 iOS 应用名称失败的情况。
  • 改进 Android 容错;zygote 和 system_server 进程终止后无需重启 frida-server。
  • 现在可在 Android 10 启动期间运行 frida-server,因为 LD_LIBRARY_PATH 不再干扰 frida-helper-32 的启动。感谢 @enovella 协助定位!
  • 在类 UNIX 平台上无法处理 SIGABRT 时不再陷入无限循环。
  • Exceptor 的 POSIX 后端现在支持嵌套信号。感谢 @bannsec!
  • 正确处理无效的 Windows ANSI 字符串。感谢 @clouds56!
  • Java.perform() 在 Android < 5 上重新正常工作。
  • 改进 NativeFunction 的可变参数处理,现在会提升小于 int 的可变参数。感谢 @0x410c 报告!

12.8.16 中的变更

  • 大型 CModule 实例现在可在使用 16K 页的 iOS 系统上工作。感谢 @mrmacete 发现并 修复这个长期存在的问题!
  • Stalker 也可在 iOS/arm64e 的 arm64 进程中工作。感谢 @AeonLucid 报告并协助定位!

12.8.17 中的变更

  • 对 i/macOS 上仍在运行的新生目标的注入支持引发了回归,因此暂时撤销。具体而言, iOS 12.4 的 notifyd 不会设置 libSystemInitialized。需要进一步调查原因, 因而目前先回退这部分逻辑。

12.8.18 中的变更

  • 全新改进的 Java.scheduleOnMainThread(),可调用 getApplicationContext() 等 API。 感谢 @giantpune 报告!
  • 能够在新版 Android 上挂钩 CriticalNative 方法。感谢 @abdawoud 报告!

12.8.19 中的变更

  • 如果脚本未经加载就被销毁,现在会得到正确清理。这样最终释放脚本核心时,也会释放其 对 Exceptor 的引用。此前若存在未加载脚本,Exceptor 线程会留在目标进程中,导致 detach 后再次 attach 时无限挂起。感谢 @mrmacete 发现并修复这个长期问题!

12.8.20 中的变更

  • remove_remote_device() API 恢复正常工作。这是 12.7.17 不幸引入的回归。感谢 @CodeColorist 报告!