打破声音屏障第一部分:使用Mach消息对CoreAudio进行模糊测试
特邀作者:Dillon Franke,高级安全工程师,20% 时间用于 Project Zero
每秒,高权限的 macOS 系统守护进程都会接收并处理数百条 IPC 消息。在某些情况下,这些消息处理器会接受来自沙盒化或非特权进程的数据。
在这篇博文中,我将探讨如何将 Mach IPC 消息用作攻击向量,以发现和利用沙盒逃逸。我将详细介绍如何使用自定义的 fuzzing 工具、动态插桩以及大量的调试/静态分析,在 coreaudiod 系统守护进程中发现一个高风险的类型混淆漏洞。在此过程中,我还会讨论遇到的一些困难和权衡。
坦率地说,这是我首次涉足 macOS 安全研究领域,也是我第一次构建自定义的 fuzzing 工具。我希望这篇博文能为那些希望从事类似研究工作的人提供指导。
我将开源我构建的 fuzzing 工具,以及我在这个项目中编写的几个有用工具。所有这些都可以在这里找到:https://github.com/googleprojectzero/p0tools/tree/master/CoreAudioFuzz
方法:知识驱动的 Fuzzing
在这个研究项目中,我采用了一种结合了 fuzzing 和手动逆向工程的混合方法,我称之为知识驱动的 fuzzing。这种方法是我从朋友 Ned Williamson 那里学到的,它在自动化和针对性调查之间取得了平衡。Fuzzing 提供了快速测试各种输入并识别系统行为偏离预期区域的手段。然而,当 fuzzer 的代码覆盖率停滞不前或遇到特定障碍时,手动分析就派上了用场,迫使我更深入地研究目标的内部工作原理。
知识驱动的 fuzzing 有两个关键优势。首先,研究过程永远不会停滞,因为提高 fuzzer 代码覆盖率的目标始终存在。其次,实现这个目标需要对正在 fuzzing 的代码有深入的理解。当你开始对真实的、与安全相关的崩溃进行分类时,逆向工程过程已经让你对代码库有了广泛的了解,从而能够从知情角度分析崩溃。
我在本次研究中遵循的循环如下:
- 识别攻击向量
- 选择目标
- 创建 fuzzing 工具
- Fuzzing 并产生崩溃
- 分析崩溃和代码覆盖率
- 迭代改进 fuzzing 工具
- 重复步骤 4-6
识别攻击向量
标准的浏览器沙盒通过限制直接操作系统访问来限制代码执行。因此,利用浏览器漏洞通常需要使用单独的“沙盒逃逸”漏洞。
由于进程间通信(IPC)机制允许两个进程相互通信,它们自然可以作为从沙盒化进程到非受限进程的桥梁。这使得它们成为沙盒逃逸的主要攻击向量,如下图所示。

我选择了 Mach 消息,这是 macOS 操作系统中最低级别的 IPC 组件,作为本次研究的重点攻击向量。我选择它们主要是因为我希望在最核心的层面理解 macOS IPC 机制,以及 Mach 消息在历史上的安全问题的记录。
先前工作和背景
在利用链中利用 Mach 消息远非新想法。例如,Ian Beer 在 2016 年发现了一个与处理 task_t Mach 端口相关的 XNU 内核核心设计问题,这允许通过 Mach 消息进行利用。另一篇博文展示了 2019 年一个野外利用链如何利用 Mach 消息进行堆整理技术。我也从 Ret2 Systems 的博文中获得了许多灵感,该文讲述了如何利用 Mach 消息处理器来发现并武器化 Safari 沙盒逃逸。
我不会花太多时间详细说明 Mach 消息的工作原理(这更适合一篇更全面的文章),但这里简要概述一下本文涉及的 Mach IPC:
- Mach 消息存储在由 Mach 端口表示的内核管理消息队列中
- 如果一个进程拥有某个端口的接收权限(receive right),它可以从该端口获取消息
- 如果一个进程拥有某个端口的发送权限(send right),它可以向该端口发送消息
macOS 应用程序可以向引导服务器(bootstrap server)注册服务,引导服务器是一个特殊的 Mach 端口,默认情况下所有进程都拥有其发送权限。这允许其他进程向引导服务器发送 Mach 消息来查询特定服务,引导服务器可以响应该服务的 Mach 端口的发送权限。macOS 系统守护进程通过 launchd 注册 Mach 服务。你可以查看 /System/Library/LaunchAgents 和 /System/Library/LaunchDaemons 目录下的 .plist 文件来了解注册的服务。例如,下面的 .plist 文件突出显示了 macOS 上为通讯录应用程序注册的 Mach 服务,标识符为 com.apple.AddressBook.AssistantService。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>POSIXSpawnType</key>
<string>Adaptive</string>
<key>Label</key>
<string>com.apple.AddressBook.AssistantService</string>
<key>MachServices</key>
<dict>
<key>com.apple.AddressBook.AssistantService</key>
<true/>
</dict>
<key>ProgramArguments</key>
<array>
<string>/System/Library/Frameworks/AddressBook.framework/Versions/A/Helpers/ABAssistantService.app/Contents/MacOS/ABAssistantService</string>
</array>
</dict>
</plist>
选择目标
决定研究 Mach 服务后,下一个问题是选择哪个服务作为目标。为了让沙盒化进程向服务发送 Mach 消息,必须明确允许。如果进程使用苹果的 App Sandbox 功能,这是在 .sb 文件中完成的,使用 TinyScheme 格式编写。下面的代码片段显示了 WebKit GPU 进程沙盒文件的摘录。allow mach-lookup 指令用于允许沙盒化进程查找并向服务发送 Mach 消息。
# File: /System/Volumes/Preboot/Cryptexes/Incoming/OS/System/Library/Frameworks/WebKit.framework/Versions/A/Resources/com.apple.WebKit.GPUProcess.sb
(with-filter (system-attribute apple-internal)
(allow mach-lookup
(global-name "com.apple.analyticsd")
(global-name "com.apple.diagnosticd")))
(allow mach-lookup
(global-name "com.apple.audio.audiohald")
(global-name "com.apple.CARenderServer")
(global-name "com.apple.fonts")
(global-name "com.apple.PowerManagement.control")
(global-name "com.apple.trustd.agent")
(global-name "com.apple.logd.events"))
这帮助我将关注范围从所有 macOS 进程大幅缩小到拥有沙盒可访问 Mach 服务的进程:

除了检查沙盒配置文件,我还使用了 Jonathan Levin 的 sbtool 工具来测试给定进程可以交互哪些 Mach 服务。该工具(虽然有点过时,但我成功编译了)在底层使用内置的 sandbox_exec 函数,提供了一个很好的可访问 Mach 服务标识符列表:
❯ ./sbtool 2813 mach
com.apple.logd
com.apple.xpc.smd
com.apple.remoted
com.apple.metadata.mds
com.apple.coreduetd
com.apple.apsd
com.apple.coreservices.launchservicesd
com.apple.bsd.dirhelper
com.apple.logind
com.apple.revision
…Truncated…
最终,我选择查看 coreaudiod 守护进程,特别是 com.apple.audio.audiohald 服务,原因如下:
- 它是一个复杂的进程
- 它允许来自多个有影响的应用程序(包括 Safari GPU 进程)的 Mach 通信
- 该 Mach 服务有大量的消息处理器
- 该服务似乎允许控制和修改音频硬件,这可能需要提升的权限
- coreaudiod 二进制文件及其大量使用的 CoreAudio 框架都是闭源的,这将提供一个独特的逆向工程挑战
创建 Fuzzing 工具
选择攻击向量和目标后,下一步是创建一个 fuzzing 工具,能够通过攻击向量(Mach 消息)在目标内的适当位置发送输入。
覆盖率引导的 fuzzer 是一个强大的武器,但前提是它的能量集中在正确的地方——就像放大镜聚焦阳光来点火一样。没有适当的聚焦,能量就会消散,收效甚微。
确定入口点
理想情况下,fuzzer 应该完美复制潜在攻击者可用的环境和能力。然而,这并不总是可行的。通常需要做出权衡,例如为了更高的性能、简化的插桩或易于开发而接受更高的误报率。因此,确定 fuzzing 的“正确位置”高度依赖于特定的目标和研究目标。
选项 1:进程间 Fuzzing
所有 Mach 消息都是使用 mach_msg API 发送和接收的,如下所示。因此,我认为 fuzzing coreaudiod 的 Mach 消息处理器最直观的方法是编写一个调用 mach_msg API 的 fuzzing 工具,并允许我的 fuzzer 修改消息内容以产生崩溃。这种方法看起来像这样:

然而,这种方法有一个很大的缺点:由于我们发送的是 IPC 消息,fuzzing 工具将与目标处于不同的进程空间。这意味着代码覆盖率信息需要跨进程边界共享,而大多数 fuzzing 工具不支持这一点。此外,内核消息队列处理增加了显著的性能开销。
选项 2:直接工具
虽然前期需要更多工作,但另一个选择是编写一个 fuzzing 工具,直接加载并调用感兴趣的 Mach 消息处理器。这将具有巨大的优势,即让我们的 fuzzer 和插桩与消息处理器处于同一进程中,使我们能够更容易地获取代码覆盖率。

这种 fuzzing 方法的一个显著缺点是,它假设所有 fuzzer 生成的输入都通过了内核的 Mach 消息验证层,而在真实系统中,这发生在消息处理器被调用之前。正如我们稍后将看到的,情况并非总是如此。然而,在我看来,在同一进程空间中进行 fuzzing 的优点(速度和易于收集代码覆盖率)超过了潜在误报增加的缺点。
方法如下:
- 识别一个合适的函数来处理传入的 Mach 消息
- 编写一个 fuzzing 工具,从 coreaudiod 加载消息处理代码
- 使用 fuzzer 生成输入并调用 fuzzing 工具
- 希望有所收获
寻找 Mach 消息处理器
首先,我搜索了 Mach 服务标识符 com.apple.audio.audiohald,但在 coreaudiod 二进制文件中没有找到对它的引用。接下来,我使用 otool 检查了它加载的库。从逻辑上讲,CoreAudio 框架似乎是存放我们消息处理器代码的好候选。
$ otool -L /usr/sbin/coreaudiod
/usr/sbin/coreaudiod:
/System/Library/PrivateFrameworks/caulk.framework/Versions/A/caulk (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (compatibility version 150.0.0, current version 2602.0.255)
/usr/lib/libAudioStatistics.dylib (compatibility version 1.0.0, current version 1.0.0, weak)
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 2602.0.255)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 1700.255.5)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.120.2)
然而,我惊讶地发现 otool 返回的路径并不存在!
$ stat /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio
stat: /System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio: stat: No such file or directory
Dyld 共享缓存
一些研究告诉我,从 macOS Big Sur 开始,大多数框架二进制文件并不存储在磁盘上,而是存储在 dyld共享缓存中,这是一种预链接库的机制,以允许应用程序运行得更快。幸运的是,IDA Pro、Binary Ninja 和 Ghidra 支持解析 dyld 共享缓存以获取其中存储的库。我还使用了这个有用的工具来成功提取库以进行额外分析。
在 IDA 中获取 CoreAudio 框架后,我很快找到了一个对 bootstrap_check_in 的调用,并将服务标识符作为参数传递,证明 CoreAudio 框架二进制文件负责设置我想要 fuzzing 的 Mach 服务。然而,尽管进行了相当多的逆向工程,仍然不清楚消息处理代码在哪里。

原来这是因为使用了 Mach Interface Generator(MIG),这是苹果的一种接口定义语言,通过抽象掉大部分 Mach 层,使编写 RPC 客户端和服务器更容易。编译时,MIG 消息处理代码被打包到一个称为子系统(subsystem)的结构中。可以轻松地 grep 这些子系统来找到它们的偏移量:
$ nm -m ./System/Library/Frameworks/CoreAudio.framework/Versions/A/CoreAudio | grep -i subsystem
(undefined) external _CACentralStateDumpRegisterSubsystem (from AudioToolboxCore)
00007ff840470138 (__DATA_CONST,__const) non-external _HALC_HALB_MIGClient_subsystem
00007ff840470270 (__DATA_CONST,__const) non-external _HALS_HALB_MIGServer_subsystem
接下来,我在 IDA 中搜索对 _HALS_HALB_MIGServer_subsystem 符号的交叉引用,这识别了解析传入 Mach 消息的 MIG 服务器函数!该例程如下所示,第一个参数(rdi 寄存器)是传入的 Mach 消息,第二个参数(rsi 寄存器)是返回给客户端的消息。MIG 服务器函数从 Mach 消息中提取 msgh_id 参数,并用它来索引到 MIG 子系统中。然后,调用必要的函数处理器。

我通过在 coreaudiod 进程上设置一个 LLDB 断点(在禁用 SIP 之后)来进一步确认这一点,断点设在 _HALB_MIGServer_server 函数上。然后,我调整了系统音量,断点被命中:

在这个例子中,追踪从 MIG 子系统调用的消息处理器显示,根据 Mach 消息的 msgh_id 调用了 _XObject_HasProperty 函数。

根据 msgh_id,可以从 MIG 子系统访问几十个消息处理器。它们很容易通过 MIG 添加的方便的 __X 前缀来识别。

_HALB_MIGServer_server 函数在接近低级消息处理代码的同时,仍然类似于调用 mach_msg 所接受的输入,这达到了一个很好的平衡。我决定这就是注入 fuzz 输入的地方。
创建基本的 Fuzzing 工具
确定了我想要 fuzzing 的函数后,下一步是编写一个程序来读取文件,并将文件内容作为输入传递给目标函数。这可能就像将 CoreAudio 库与我的 fuzzing 工具链接并调用 _HALB_MIGServer_server 函数一样简单,但不幸的是,该函数没有被导出。
相反,我从 Ivan Fratric 和他的 TinyInst 工具(我们稍后会更多地讨论它)中借用了一些逻辑,该工具从库中返回给定符号的地址。该代码解析 Mach-O 二进制文件的结构,特别是它们的头部和加载命令,以定位和提取符号信息。这使得在我的 fuzzing 工具中解析和调用目标函数成为可能,即使它没有被导出。
因此,我的工具的高级功能如下:
- 加载 CoreAudio 库
- 从 CoreAudio 库获取目标函数的函数指针
- 从文件读取输入
- 使用输入调用目标函数
我的 fuzzing 工具的完整实现可以在这里找到。下面是一个调用工具从输入文件发送消息的示例:
$ ./harness -f corpora/basic/1 -v
*******NEW MESSAGE*******
Message ID: 1010000 (XSystem_Open)
------ MACH MSG HEADER ------
msg_bits: 2319532353
msg_size: 56
msg_remote_port: 1094795585
msg_local_port: 1094795585
msg_voucher_port: 1094795585
msg_id: 1010000
------ MACH MSG BODY (32 bytes) ------
0x01 0x00 0x00 0x00 0x03 0x30 0x00 0x00 0x41 0x41 0x41 0x41 0x41 0x41 0x11 0x00 0x41 0x41 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
------ MACH MSG TRAILER ------
msg_trailer_type: 0
msg_trailer_size: 32
msg_seqno: 0
msg_sender: 0
------ MACH MSG TRAILER BODY (32 bytes) ------
0xf5 0x01 0x00 0x00 0xf5 0x01 0x00 0x00 0x14 0x00 0x00 0x00 0xf5 0x01 0x00 0x00 0x14 0x00 0x00 0x00 0x7e 0x02 0x00 0x00 0xa3 0x86 0x01 0x00 0x4f 0x06 0x00 0x00
Processing function result: 1
*******RETURN MESSAGE*******
------ MACH MSG HEADER ------
msg_bits: 1
msg_size: 36
msg_remote_port: 1094795585
msg_local_port: 0
msg_voucher_port: 0
msg_id: 1010100
------ MACH MSG BODY (12 bytes) ------
0x00 0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00
收集合法的 Mach 消息
我现在有了一种方法,可以直接将数据传递到我想要 fuzzing 的 MIG 子系统(_HALB_MIGServer_server)。然而,我不知道处理器期望的特定消息大小、选项或数据。虽然覆盖率引导的 fuzzer 会随着时间的推移发现正确的消息格式,但在刚开始 fuzzing 时获取一个合法的输入种子语料库以提高效率是有利的。
为此,我使用 LLDB 在 MIG 子系统上设置断点,并转储第一个参数(包含传入的 Mach 消息)。然后,我操作操作系统以导致 Mach 消息被发送到 coreaudiod。macOS 的 Audio MIDI Setup 应用程序最终非常适合这个目的,因为它允许创建、编辑和删除音频设备。

Fuzzing 并产生崩溃
有了一个小型种子语料库和输入传递机制,下一步是配置一个 fuzzer 来使用创建的 fuzzing 工具并获取代码覆盖率。我使用了由 Ivan Fratric 构建和维护的优秀 Jackalope fuzzer。我选择 Jackalope 主要是因为其高度的可定制性——它允许轻松实现自定义变异器、插桩和样本传递。此外,我很欣赏它在 macOS 上的无缝使用,特别是其由 TinyInst 驱动的代码覆盖率功能。相比之下,我尝试使用 Frida 收集 macOS 系统守护进程的代码覆盖率但失败了。
我使用以下命令启动 Jackalope fuzzing 运行:
$ jackalope -in in/ -out out/ -delivery file -instrument_module CoreAudio -target_module harness -target_method _fuzz -nargs 1 -iterations 1000 -persist -loop -dump_coverage -cmp_coverage -generate_unwind -nthreads 5 -- ./harness -f @@
迭代改进 Fuzzing 工具
这个工具很快产生了许多崩溃,表明我走对了路。然而,我很快了解到,最初的崩溃通常并不表示安全漏洞,而是 fuzzing 工具本身的设计缺陷或无效假设。
迭代 1:目标初始化
我的 fuzzing 方法的一个困难是,我的目标函数(Mach 消息处理器)期望 HAL 系统处于特定状态才能开始接收 Mach 消息。通过简单地用我的 fuzzing 工具调用库函数,这些假设被打破了。
这导致错误开始出现。如下图所示,该工具绕过了 coreaudiod 进程在启动时通常会处理的大部分引导功能。

代码覆盖率以及错误消息在帮助确定 fuzzing 工具忽略的一些初始化步骤方面非常有用。例如,我注意到我的数据流在大多数 Mach 消息处理器中总是很早就失败,并记录消息 Error: there is no system。

原来我需要先初始化 HAL 系统,才能正确与 Mach API 交互。在我的情况下,在我的 fuzzing 工具中调用 _AudioHardwareStartServer 函数处理了大部分必要的初始化。
迭代 2:API 调用链
我的第一个 fuzzing 工具尝试很酷,但它做了一个相当大的假设:所有可访问的 Mach 消息处理器都彼此独立运行。我很快了解到这个假设是错误的。当我运行 fuzzer 时,开始出现如下错误消息:

该错误似乎表明 SetPropertyData Mach 处理器期望通过之前的 Mach 消息注册一个客户端。显然,我正在 fuzzing 的 Mach 处理器是有状态的,并且彼此依赖才能正常工作。我的 fuzzing 工具需要考虑这一点,才能有希望获得目标上的良好代码覆盖率。
这突显了 fuzzing 世界中的一个常见问题:大多数覆盖率引导的 fuzzer 接受单个输入(一堆字节),而我们想要 fuzzing 的许多东西接受完全不同的数据格式,例如几个不同类型的参数,甚至是几个函数调用。这篇谷歌文章很好地解释了这个问题,Ned Williamson 的2019 年 OffensiveCon 演讲也是如此。
为了绕过这个限制,我们可以使用一种我称之为 API 调用链的技术,它将每个 fuzz 输入视为一个可以读取的流,以构建多个有效输入。因此,每个 fuzzing 迭代都能够生成多个 Mach 消息。这个简单但重要的见解允许 fuzzer 使用相同的代码覆盖率引导的输入来探索单独函数调用之间的相互依赖性。
FuzzedDataProvider 类是 LibFuzzer 的一部分,但可以作为头文件包含用于任何 fuzzing 工具,它是消费 fuzz 样本并将其转换为更有意义的数据类型的绝佳选择。考虑以下伪代码:
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
FuzzedDataProvider fuzz_data(data, size); // 初始化 FDP
while (fuzz_data.remaining_bytes() >= MACH_MSG_MIN_SIZE) { // 继续直到消耗完所有字节
uint32_t msg_id = fuzz_data.ConsumeIntegralInRange<uint32_t>(1010000, 1010062);
switch (msg_id) {
case '1010000': {
send_XSystem_Open_msg(fuzz_data);
}
case '1010001': {
send_XSystem_Close_msg(fuzz_data);
}
case '1010002': {
send_XSystem_GetObjectInfo_msg(fuzz_data);
}
... continued
}
}
}
这段代码将一堆字节转换为一种机制,可以以确定性的方式重复调用带有 fuzz 数据的 API。更重要的是,覆盖率引导的 fuzzer 将能够探索和识别一系列能提高代码覆盖率的 API 调用。从 fuzzer 的角度来看,它只是在修改一个字节数组,完全不知道底层发生的额外复杂性。
例如,我的 fuzzer 很快发现,与 audiohald 服务的大多数交互都需要先调用 _XSystem_Open 消息处理器来注册客户端,然后才能调用大多数 API。随着时间的推移,fuzzer 保存到其语料库的输入自然反映了这一事实。
迭代 3:模拟有缺陷/不需要的功能
有时覆盖率会停滞不前,fuzzer 难以探索新的代码路径。例如,假设我们正在 fuzzing 一个 HTTP 服务器,它一直卡住,因为它试图在启动时读取和解析配置文件。如果我们的重点是服务器的请求解析和响应逻辑,我们可能会选择模拟我们不关心的功能,以便将 fuzzer 的代码覆盖率探索集中在其他地方。
在我的 fuzzing 工具的情况下,调用初始化例程导致我的工具尝试向引导服务器注册 com.apple.audio.audiohald Mach 服务,这引发了错误,因为它已经被 launchd 注册了。由于我的工具不需要注册 Mach 服务来注入消息(记住,我们的工具直接调用 MIG 子系统),我决定模拟该功能。
在处理纯 C 函数时,可以使用函数拦截来轻松修改函数的行为。在下面的例子中,我声明了一个新版本的 bootstrap_check_in 函数,它只是返回 KERN_SUCCESS,有效地将其空操作化,同时告诉调用者它是成功的。
#include <mach/mach.h>
#include <stdarg.h>
// Forward declaration for bootstrap_check_in
kern_return_t bootstrap_check_in(mach_port_t bootstrap_port, const char *service_name, mach_port_t *service_port);
// Custom implementation of bootstrap_check_in
kern_return_t custom_bootstrap_check_in(mach_port_t bootstrap_port, const char *service_name, mach_port_t *service_port) {
// Ensure service_port is non-null and set it to a non-zero value
if (service_port) {
*service_port = 1; // Set to a non-zero value
}
return KERN_SUCCESS; // Return 0 (KERN_SUCCESS)
}
// Interposing array for bootstrap_check_in
__attribute__((used)) static struct {
const void* replacement;
const void* replacee;
} interposers[] __attribute__((section("__DATA,__interpose"))) = {
{ (const void *)custom_bootstrap_check_in, (const void *)bootstrap_check_in }
};
对于 C++ 函数,我使用 TinyInst 的 Hook API 来修改有问题的功能。在一个特定场景中,我的 fuzzer 不断使目标崩溃,因为 CFRelease 函数被用 NULL 指针调用。一些进一步的分析告诉我,这是一个与安全无关的 bug,其中用户的输入(假定包含有效的 plist 对象)没有得到适当的验证。如果 plist 对象无效或为 NULL,下游的函数调用将包含 NULL,并发生中止。

因此,我编写了以下 TinyInst hook,它检查传递给函数的 plist 对象是否为 NULL。如果是,我的 hook 提前返回函数调用,绕过有问题的代码。
void HALSWriteSettingHook::OnFunctionEntered() {
printf("HALS_SettingsManager::_WriteSetting Entered\n");
if (!GetRegister(RDX)) {
printf("NULL plist passed as argument, returning to prevent NULL CFRelease\n");
printf("Current $RSP: %p\n", GetRegister(RSP));
void *return_address;
RemoteRead((void*)GetRegister(RSP), &return_address, sizeof(void *));
printf("Current return address: %p\n", GetReturnAddress());
printf("Current $RIP: %p\n", GetRegister(RIP));
SetRegister(RAX, 0);
SetRegister(RIP, GetReturnAddress());
printf("$RIP register is now: %p\n", GetRegister(ARCH_PC));
SetRegister(RSP, GetRegister(RSP) + 8); // Simulate a ret instruction
printf("$RSP is now: %p\n", GetRegister(RSP));
}
}
接下来,我修改了 Jackalope 以使用我的插桩,通过 CreateInstrumentation API。这样,我的 hook 在每次 fuzzing 迭代中都被应用,烦人的 NULL CFRelease 调用停止了。下面的输出显示 hook 防止了因传递给麻烦 API 的 NULL plist 对象而导致的崩溃:
Instrumented module CoreAudio, code size: 7516156
Hooking function __ZN11HALS_System13_WriteSettingEP11HALS_ClientPK10__CFStringPKv in module CoreAudio
HALS_SettingsManager::_WriteSetting Entered
NULL plist passed as argument, returning to prevent NULL CFRelease
Current $RSP: 0x7ff7bf83b358
Current return address: 0x7ff8451e7430
Current $RIP: 0x7ff84533a675
$RIP register is now: 0x7ff8451e7430
$RSP is now: 0x7ff7bf83b360
Total execs: 6230
Unique samples: 184 (0 discarded)
Crashes: 3 (2 unique)
Hangs: 0
Offsets: 13550
Execs/s: 134
用于使用自定义插桩重现和构建此 fuzzer 的代码可以在这里找到:https://github.com/googleprojectzero/p0tools/tree/master/CoreAudioFuzz/jackalope-modifications
迭代 4:改进样本结构
以 fuzzing 为中心的审计技术的一个优点是,它突出了你正在审计的代码中的知识空白。当你解决这些空白时