这次只有一项新功能,但它分量十足。我们要直面那个一直存在却少有人谈的问题:性能。
Frida 的插桩核心 Gum 使用 C 编写,也可以从 C 中使用;不过对于大多数场景,使用其 JavaScript 绑定会更合适。
然而,在某些情况下性能会成为问题。即使使用基于 V8 的运行时——这意味着 JavaScript 在运行时会接受性能分析,并根据热点位置进行优化……(顺便说一句,这实在令人惊叹,V8 真是一项了不起的工程成就!)
……进入和离开 JavaScript VM 仍要付出少量代价。在 iPhone 5S 上,如果使用 Interceptor.attach(),只指定空的 onEnter,这项开销大约是六微秒。
这听起来可能不多,但如果一个函数被调用一百万次,就会增加 6 秒开销。而 hook 可能只需做一件极其简单的事,因此大部分时间其实都花在进入和离开 VM 上。
需要向 API 传递回调时也有同类问题:API 可能遍历数百万个项目,并为每项调用一次回调;回调也许只是查看一个字节,再收集少数符合特定条件的项目。
最直接的做法是使用 NativeCallback 实现该回调,但很快就会发现这种方式无法扩展。
又或者,你正在编写模糊测试器,需要在紧密循环中调用 NativeFunction;进入/离开 VM 再加上 libffi 的成本会不断累积。
除了用 C 编写整个 agent,还可以构建原生库,并使用 Module.load() 加载。这确实可行,但意味着必须为每一种架构编译并部署到目标等。
另一种方案是使用 X86Writer/Arm64Writer 等 API 在运行时生成代码。这同样很痛苦,因为支持每种架构都需要大量工作。但直到现在,对于 frida-java-bridge 等模块来说,这仍是唯一可移植的选择。
而现在,我们终于有了好得多的方案:CModule 登场:

它接收包含 C 源代码的字符串,将其编译为机器码并直接写入内存。该功能使用 TinyCC 实现,因此只给 Frida 增加约 100 kB 的体积。
如图所示,所有全局函数都会自动导出为 NativePointer 属性,名称与 C 源代码中完全相同。
而且,它很快:

(在主频 3.1 GHz 的 Intel i7 上测得。)
还可以将这项新功能与 Interceptor 等 API 结合使用:
const m = new CModule(`
#include <gum/guminterceptor.h>
#define EPERM 1
int
open (const char * path,
int oflag,
...)
{
GumInvocationContext * ic;
ic = gum_interceptor_get_current_invocation ();
ic->system_error = EPERM;
return -1;
}
`);
const openImpl = Module.getExportByName(null, 'open');
Interceptor.replace(openImpl, m.open);(请注意,本例及后续示例使用了模板字面量等现代 JavaScript 特性,因此需要在 V8 运行时上执行,或使用 frida-compile 编译。)
还可以将它与 Interceptor.attach() 结合:
const openImpl = Module.getExportByName(null, 'open');
Interceptor.attach(openImpl, new CModule(`
#include <gum/guminterceptor.h>
#include <stdio.h>
void
onEnter (GumInvocationContext * ic)
{
const char * path;
path = gum_invocation_context_get_nth_argument (ic, 0);
printf ("open() path=\\"%s\\"\\n", path);
}
void
onLeave (GumInvocationContext * ic)
{
int fd;
fd = (int) gum_invocation_context_get_return_value (ic);
printf ("=> fd=%d\\n", fd);
}
`));太棒了。不过最后这个示例实际上会写入目标进程的 stdout;调试时这样做没有问题,但可能并不十分实用。
可以通过回调 JavaScript 来解决。来看看具体写法:
const openImpl = Module.getExportByName(null, 'open');
Interceptor.attach(openImpl, new CModule(`
#include <gum/guminterceptor.h>
extern void onMessage (const gchar * message);
static void log (const gchar * format, ...);
void
onEnter (GumInvocationContext * ic)
{
const char * path;
path = gum_invocation_context_get_nth_argument (ic, 0);
log ("open() path=\\"%s\\"", path);
}
void
onLeave (GumInvocationContext * ic)
{
int fd;
fd = (int) gum_invocation_context_get_return_value (ic);
log ("=> fd=%d", fd);
}
static void
log (const gchar * format,
...)
{
gchar * message;
va_list args;
va_start (args, format);
message = g_strdup_vprintf (format, args);
va_end (args);
onMessage (message);
g_free (message);
}
`, {
onMessage: new NativeCallback(messagePtr => {
const message = messagePtr.readUtf8String();
console.log('onMessage:', message);
}, 'void', ['pointer'])
}));不过这只是玩具示例:这样做实际上违背了用 C 编写 hook 来提升性能的初衷。真正的实现可以先获取 GLib.Mutex,再追加到 GLib.Array,并通过回调 JS 定期刷新缓冲数据。
就像可以从 C 调用 JavaScript 函数一样,也可以在两个世界之间共享数据:
const calls = Memory.alloc(4);
const openImpl = Module.getExportByName(null, 'open');
Interceptor.attach(openImpl, new CModule(`
#include <gum/guminterceptor.h>
extern volatile gint calls;
void
onEnter (GumInvocationContext * ic)
{
g_atomic_int_add (&calls, 1);
}
`, { calls }));
setInterval(() => {
console.log('Calls so far:', calls.readInt());
}, 1000);目前还没有内置 C API 的文档,但可以浏览 frida-gum/bindings/gumjs/runtime/cmodule 中的头文件来了解概况。对于 GLib 等非 Frida API,可以把函数名放进互联网搜索引擎查找文档。
我们的目标是只公开标准 C 库、GLib、JSON-GLib 和 Gum API 的最小子集,以尽量减小体积并最大化性能。纳入的功能应当是无法通过调用 JS 实现,或以这种方式实现时成本高得难以接受的功能。
可以把 JS 端想象成操作系统,接入其中的函数就是系统调用;CModule 只应用于 hook 高频函数,或实现高性能粘合代码,例如传给性能敏感 API 的回调。
还要记住,TinyCC 生成的机器码不如 Clang 或 GCC 高效,因此计算密集型算法用 JavaScript 实现反而可能更快(使用基于 V8 的运行时时)。但对于 hook 和粘合代码,这种差异并不显著;如果需要优化内层循环,随时可以使用 Arm64Writer 等生成机器码并接入 CModule。
一个重要限制是所有数据都只读,因此可写全局变量应声明为 extern,使用 Memory.alloc() 等方式分配,并通过构造函数的第二个参数以符号形式传入。(就像上一个示例中的 calls。)
CModule 被销毁时(例如脚本被卸载),可能还需要初始化并清理某些内容;为此我们提供了两个生命周期 hook:
const cm = new CModule(`
#include <stdio.h>
void
init (void)
{
printf ("init\\n");
}
void
finalize (void)
{
printf ("finalize\\n");
}
`);
cm.dispose(); // or wait until it gets GCed or script unloaded总之,这篇文章越来越长了;不过在结束之前,先看看如何将 CModule 与 Stalker API 配合使用:
const cm = new CModule(`
#include <gum/gumstalker.h>
static void on_ret (GumCpuContext * cpu_context,
gpointer user_data);
void
transform (GumStalkerIterator * iterator,
GumStalkerOutput * output,
gpointer user_data)
{
cs_insn * insn;
while (gum_stalker_iterator_next (iterator, &insn))
{
if (insn->id == X86_INS_RET)
{
gum_x86_writer_put_nop (output->writer.x86);
gum_stalker_iterator_put_callout (iterator,
on_ret, NULL, NULL);
}
gum_stalker_iterator_keep (iterator);
}
}
static void
on_ret (GumCpuContext * cpu_context,
gpointer user_data)
{
printf ("on_ret!\n");
}
`);
const mainThread = Process.enumerateThreads()[0];
Stalker.follow(mainThread.id, {
transform: cm.transform,
data: ptr(1337)
});这展示了如何用 C 同时实现 transform 回调和 callout;也可以采用混合方式,用 JS 编写 transform 回调,只用 C 编写部分 callout。
还值得一提的是,我重写了 ObjC.choose(),让它使用 CModule,现在速度大约提升了 100 倍。在 iPhone 6S 上用 Twitter 应用登录界面测试时,耗时从约 5 秒缩短到约 50 毫秒。
希望你喜欢这个版本。我很期待看到大家使用新的 CModule API 构建出怎样的东西。我尤其期待改进 REPL,让它支持在 .js 旁加载 .c 文件,以便快速制作原型。
祝使用愉快!
12.7.0 的变更
- 全新 CModule API,由 TinyCC 驱动。(你刚刚已经读过相关介绍。)
- 改进 TinyCC,以支持 macOS/x86 上的 Apple ABI。
- 现在向 JS 公开 Stalker.exclude(),可以将特定内存范围标记为排除。这有助于提升性能并减少噪声。
- 现在支持并发调用 Java.use(),感谢 @gebing 的精彩贡献。
- 改进 hexdump() 实现,将 length 选项限制在 ArrayBuffer 长度以内,再次感谢 @gebing 的精彩贡献。
12.7.1 的变更
- 更多 CModule 功能,包括 GLib.String、GLib.Timer 和 Json.Builder。
- 改进 TinyCC,以支持 iOS/arm64 上的 Apple ABI。
- 使用 CModule 重写 ObjC.choose(),现在速度约快 100 倍。
12.7.2 的变更
- 为 CModule 补充了一些缺失的引用计数 API。
12.7.3 的变更
- 现在会正确隐藏 CModule 内存范围。
- 现在会将外部分配的 CModule 内存告知 V8 垃圾回收器,使其能更好地决定何时执行 GC。
- 附加到 CModule 的符号现在也会在 V8 运行时中正确保持存活;CModule 自身也不再无限期(或直到脚本卸载)保持存活。
- 添加 CModule.dispose(),用于主动清理内存。
12.7.4 的变更
- frida-inject 工具现在支持 spawn()。感谢 @hunterli 贡献这项实用功能。
- 在 i/macOS 上仍持有 JS 锁时调用 thread_suspend(),V8 运行时不再发生死锁;要求 Stalker.follow() 跟踪另一线程时会间接触发这种情况。
12.7.5 的变更
- 全新 channels API,用于与已连接的 iOS 或 Android 设备建立 TCP 连接,也可与已连接 iOS 设备上的 lockdown 服务通信。
- DeviceManager.find_device() 及其同类方法背后的超时逻辑现在可以正常工作。
- java.lang.Class 的 Java 编组现在可以正常工作,也可以在没有实例的情况下内省实例字段。感谢 @gebing 贡献这些精彩修复!
12.7.6 的变更
- 现在可在 Android 10 上正确检测 Android linker。
- Android SELinux 策略修补器现在也能处理 Samsung S10 等设备,感谢 @cbayet 的精彩贡献。
- frida-inject 工具现在支持 -D/–device,可用于非本地设备。
- 改进错误处理,避免 i/macOS 进程在早期插桩期间意外终止时导致崩溃。
- iOS 崩溃报告器集成更加健壮,感谢 @mrmacete 贡献的出色修复。其中一项还确保针对同一消息类型并行调用 recv().wait() 时不会无限等待。
- Linux/arm64 现在支持跟踪线程创建。感谢 @alvaro_fe 的出色贡献!
- V8 运行时的 WebAssembly 支持现在也能在非 iOS 平台上正常工作。
- Gum.DarwinModule API 现已成为跨平台 Gum API 的一部分,可用于在非 Apple 系统上解析 Mach-O 文件。
12.7.7 的变更
- 关闭最后一个会话时,现在会保留永久化 agent;只要 HostSession 端(例如 frida-server)仍然存在,就可以复用它们。很多情况下因此能避免创建额外的 frida-agent 副本。感谢 @mrmacete 的出色改进。
- 方法返回 this 时,Java bridge 不再触发释放后使用。
- Android SELinux 策略修补器不再在旧版 Android 上打印警告。这项无害但令人困惑的回归由上一版本针对 Samsung S10 ROM 的修复引入。
- 改进与 SELinux 相关的错误消息。
- 初步支持 iOS/arm64e。
12.7.8 的变更
- 得益于 @Alien-AV 的杰出贡献,Java bridge 现已支持 Android 10。
- 改进 Android 应用的 spawn() 处理:应用没有启动器 activity 时可以使用 activity 参数。这项实用改进由 @muhzii 贡献。
- 得益于 @timstrazz 的优雅贡献,Android linker 查找逻辑已具备面向未来的适应性。
- 大幅提高 iOS 上的容错能力:launchd agent 卸载时现在会终止待处理进程。这意味着 frida-server 退出后不会留下卡在暂停状态的进程。感谢 @mrmacete 的出色改进。
12.7.9 的变更
- 修复上一版本最后一刻混入的构建回归,macOS 上恢复正常。
12.7.10 的变更
- MemoryAccessMonitor 现在可用于所有平台,甚至支持 Duktape 运行时。感谢 @alvaro_fe 的出色改进。
- Android linker 检测恢复正常。(12.7.8 引入的回归。)
- 改进 Gadget,支持在 i/macOS 上通过构造函数传入配置。
- 移除 Gadget 仅在 i/macOS 上实现的系统循环集成,以避免某些场景下的未定义行为。
12.7.11 的变更
- Frida 不再在没有 vDSO 的 Android 进程中崩溃。(12.7.8 引入的回归。)
- 改进解析 Mach-O 映像时的错误处理。
- 在 i/macOS 上恢复异常时正确处理 ARM 与 Thumb。感谢 @alvaro_fe!
- CModule 的 JSON-GLib 头文件现在按预期实现自包含。
12.7.12 的变更
- 完整的 iOS lockdown 集成和统一设备,使基于 Frida 的工具不必过多考虑受限设备与越狱设备的差异。与受限 iOS 设备交互时,现在会自动注入 Gadget,无需重新打包应用,只需保证应用可调试。
- Frida 终于可以在 Windows 上检测较新的 iOS 设备。
- 定位了 V8 中的错误,并从上游回移修复。感谢 @mrmacete 找到这个问题!
12.7.13 的变更
- 改进 frida-objc-bridge 中结构体和联合体的处理。感谢 @gebing!
- Node.js 绑定现在也公开 Crash 和 CrashParameters 的类型定义。
- 尝试附加受限 iOS 上的系统会话时会提前抛出异常,并提供更清晰的错误消息。
- 调整与 iOS Developer Disk Image 相关的错误消息,使其保持一致。
12.7.14 的变更
- 在 Android >= 10 上访问代码前,Frida 现在会确保代码可读。这是完整支持 Android 10 所缺的最后一块拼图。能够插桩系统进程意味着早期插桩(即 spawn())可以工作;启动 frida-server 时,也不会因为它试图预加载以加速第一次 spawn() 而使 system_server 崩溃。感谢 @Alien-AV 和 @esanfelix 的艰苦研究,让这个解决方案得以在某个周六深夜完成实现。:-)
12.7.15 的变更
- Node.js 绑定也公开 Crash 类型中的“summary”字段。
12.7.16 的变更
- frida-gadget-ios 元软件包附带类型定义,因此可从 TypeScript 中使用。
- Node.js 绑定为 Stdio 和 ChildOrigin 提供正确的类型。
12.7.17 的变更
- 更健壮地支持受限 iOS:现在可以在 attach() 之后、resume() 之前正确调用 kill()。
- 条件允许时,Frida 现在会直接与远程 iOS/Android agent 通信。实现方式是与 frida-server 建立新的 TCP 连接,并把其文件描述符传给 agent。这样 frida-server 就能退出数据路径,提高性能和可靠性。
- 客户端连接 frida-server 并 spawn() 进程后,如果还没来得及 resume() 或 kill() 就断开连接,不会再留下悬而未决的孤儿进程。现在会跟踪已生成进程,并在客户端突然断开时将其终止。
- 不再导致 Android 10 上的 Zygote 崩溃。事实证明缺少了一条 SELinux 规则。
- TCP 套接字现在设置了 TCP_NODELAY;除了 TCP,还支持使用 UNIX 套接字与远程 frida-server 或 frida-gadget 通信。
- NativeFunction 现在支持可变参数函数,不再需要为每种唯一的参数列表签名创建一个实例。感谢 @gebing!
12.7.18 的变更
- Node.js 绑定终于会在 UNIX 上链接所需的 OpenSSL 符号,不再依赖运气——以前通常只是碰巧所在进程已加载另一个全局可见且 ABI 兼容的 OpenSSL(!)。
12.7.19 的变更
- 现在也能在 V8 运行时中正确处理对无参数 NativeFunction 调用 apply() 的情况。感谢 @taviso 报告这个长期存在的错误。
- NativeFunction 在 call() 和 apply() 中处理可选参数的行为,现在与内置版本一致。
12.7.20 的变更
- Frida 现在同时支持明文和加密的 iOS lockdown 通道;在通道地址后附加“?tls=handshake-only”,仍可使用“TLS 握手后转明文”形式的通道。感谢 @mrmacete!
- 使用“lockdown:”作为通道地址,可以访问配对的 lockdown 通道本身。感谢 @mrmacete!
12.7.21 的变更
- 现在支持使用 checkra1n 越狱的 iOS 13。(此前已经支持受限 iOS 13。)
12.7.22 的变更
- iOS 软件包脚本的启动守护进程逻辑现在兼容 checkra1n,因此不必手动启动/停止 frida-server。
12.7.23 的变更
- 增强对 checkra1n 越狱的支持:Stalker 现在利用 RWX 页面,速度大幅提升。
- 提高在不支持 RWX 的 iOS 越狱环境中使用 Stalker 时的稳定性。感谢 @mrmacete!
- CModule 现在兼容更多 iOS 越狱方案。感谢 @mrmacete!
- CModule 运行时支持使用 ModuleMap 对象。
- 得益于 Jon Wilson 的出色贡献,现在支持 ARMBE8。
- 在 i/macOS 上 spawn() 时,Frida 现在会让子进程能够使用父进程文件描述符,与 Linux 当前行为一致。感谢 @wizche!
12.7.24 的变更
- 也为 Node.js v13 提供预构建包。
12.7.25 的变更
- 作为一项关键修复,全面调整 Python 和 Node.js 绑定中的日志处理程序 API:Node.js setter 的类型与 getter 不同,因为 setter 还允许 null。这种不一致会导致新版 TypeScript 编译器报错。感谢 @mrmacete!
- V8 平台集成不再截断时间戳。感谢 @DaveManouchehri 报告问题!
- Module.enumerateSymbols() API 会在可用时提供“size”属性,目前仅限 Linux/Android。感谢 @DaveManouchehri!
- 现在可以使用 Java.use(name, { cache: ‘skip’ }) 绕过缓存,在处理多个类加载器和冲突类名时很有用。感谢 @ChaosData 和 @H4oK3!
12.7.26 的变更
- Stalker 现在支持临时重新激活,从而可以跟踪排除内存范围内部的代码。
- NativeFunction 新增
traps: 'all'选项,即使 Frida 自身内存范围被标记为排除,也允许跟踪调用。 - 使用 Yama 时,Linux 上的线程枚举终于可以正常工作。感谢 Jon Wilson!
oleavr