这个版本允许宿主向 pattern 传入预处理器宏,修复了我在把 pattern 接入即将推出的 Luma 时遇到的一批 pattern 语言边界问题,并且感谢 @pandasauce,缩小了在 arm64 Linux 和 Android 上执行 hook 时的竞态窗口。Windows 方面也有修复:@tshivaneshk 修复了导入枚举,此外还为 Windows 9x 修复了一批 Barebone 问题。

宏定义

pattern 经常需要知道自己的数据来自哪里。磁盘上的文件格式会内联存放有效载荷,但当同一格式映射到内存后,头部后面跟随的可能变成一个指针。ImHex pattern 使用预处理器处理这类情况,现在我们的 pattern 也可以了:PatternCompiler 接受一个 defines 字典,其行为相当于在入口点顶部添加 #define。假设有一个 image.hexpat:

#pragma abi native

struct Header {
    u32 magic;
    u32 size;
};

struct Image {
    Header header;
#ifdef MAPPED
    u64 base;
#else
    u8 payload[header.size];
#endif
};

分别在定义和不定义 MAPPED 的情况下编译,同一份源码会产生两种不同的布局:

import frida

compiler = frida.PatternCompiler()

for defines in (None, {"MAPPED": 1}):
    options = {"platform": "darwin", "arch": "arm64"}
    if defines is not None:
        options["defines"] = defines
    module = compiler.compile("image.hexpat", **options)
    image = module.lookup("Image")
    fields = [(f.name, f.type_ref.display) for f in image.fields]
    print(f"defines={defines}: {fields}, size={image.size}")
defines=None: [('header', 'Header'), ('payload', 'u8[...]')], size=-1
defines={'MAPPED': 1}: [('header', 'Header'), ('base', 'u64')], size=16

宏定义会随模块一起传递,因此解码和 call_function() 看到的预处理器状态与编译时完全相同。语言服务器会在 workspace/didChangeConfiguration 时从 settings.patterns.defines 获取宏定义,使编辑器的诊断结果可以与宿主编译 pattern 的方式保持同步。正因如此,Luma 才能告诉 pattern 它正在解码文件还是已映射的镜像,而 pattern 无需了解任何 Luma 相关细节。

除此之外,我们用即将推出的 Luma 对真实世界的 pattern 进行了一周测试,暴露出实现中围绕模板、ref 参数、枚举名称和 array_index() 的一些边界情况。本版本已全部修复,详细内容见下方变更日志。

arm64 Linux 上的 Interceptor

Georgi Boiko(@pandasauce)追踪到了 Android 上的崩溃问题。它源于 arm64 重定向与线程之间的相互作用:这些线程恰好暂停在正在被 hook 的函数内部。在 arm64 上,默认 hook 会使用 ADRP+BR 或 LDR+BR+字面量序列重写函数开头的 16 字节;暂停在第一条指令之后任意位置的线程,恢复后可能进入只完成了一半修补的函数序言。

现在,对于至少有八个可重定位字节的默认 hook,Interceptor 会尝试在 Linux 和 Android 上于 B 指令可达范围内为 trampoline 寻找一个 slice,使重定向只需一条 B 指令。暂停的线程随后会从未经改动的原始字节恢复,而补丁本身只需一次对齐写入。恢复重定向也只需一次写入。为了更频繁地提供这样的邻近 slice,当完整的七页批次无法放入目标附近的空闲空间时,代码分配器现在会用单页重试邻近分配。

Arm 架构参考手册仍指出:当另一个核心可能正在执行某条任意指令时,用分支替换它的行为不可预测。因此,这项改动只是缩小了竞态窗口,并未彻底消除它,但实践中效果非常明显。还有一个需要注意的取舍:原本会使用 16 字节重定向的 hook,现在会更快耗尽目标附近的空间;而对于只有一条指令长的函数,由于其他方案都放不下,这些空间是执行 hook 的唯一选择。非常感谢 Georgi!

Windows

@tshivaneshk 注意到,Windows 上的 Module.enumerateImports() 会把每个导入的槽位都报告为 IAT 基址,并在导入模块中查找每个地址,而不是从 IAT 读取。现在它会在遍历查找表的同时遍历 IAT,报告每个条目及其绑定值;我们的测试也会检查报告的地址。感谢!

此外,Process.id 现在会在 QuickJS 中保持无符号。Windows 9x 会分配大于 2^31 的进程 ID,此前这些值在 JavaScript 运行时中会变成负数。

Barebone

说到 Windows 9x,我们使用 Barebone 后端与 Explorer 等程序进行的几次会话,暴露出注入用户进程的 agent 中存在五个错误:从模块加载破坏已退出线程的记录,到分离超时并导致客户机蓝屏。现已全部修复,详情见变更日志。

EOF

尽情使用吧!

变更日志

  • patterns:允许宿主通过 PatternCompileOptions.defines 定义预处理器宏,并将其传递到解码和调用过程;语言服务器则通过 settings.patterns.defines 接收。(上文已介绍。)
  • patterns:模板嵌套实例化达到 64 层后,报告无限模板递归。
  • patterns:将 pattern 传给 ref 参数,使格式化位域或结构体的函数能够找到其成员。
  • patterns:按名称查找模板局部变量和字段作为后备方案,用于多个外层类型共享的模板。
  • patterns:支持在元素内部调用 array_index()。
  • patterns:通过成员访问时保留枚举名称,并允许 formatted_value() 格式化标量和位域。
  • interceptor:当 B 指令范围内存在可用 slice 时,在 arm64 Linux 和 Android 上使用 4 字节重定向。感谢 @pandasauce!
  • codeallocator:在 Linux 和 Android 上用单页重试邻近批次分配。感谢 @pandasauce!
  • windows:修复导入槽位和地址。感谢 @tshivaneshk!
  • gumjs:在 QuickJS 中保持 Process.id 为无符号值。
  • barebone:修复注入用户进程的 agent 中五个 Windows 9x 问题:模块加载破坏已退出线程的记录;主线程停驻在后续注入可能复用的内存中;启动栈泄漏;工作线程从未被唤醒导致分离超时;换出内存上的故障未交还 Windows 处理。
  • swift:为 PatternCompiler.compile() 添加 defines。
  • subprojects:回退 termux-elf-cleaner 升级,因为其新代码需要 C++20,而宿主工具构建并未使用 C++20。