突破音障 Part I: 使用 Mach Messages 对 CoreAudio 进行 Fuzzing
Project Zero
来自 Google 的 Project Zero 团队的新闻和更新
2025年5月9日 星期五
突破音障 Part I: 使用 Mach Messages 对 CoreAudio 进行 Fuzzing
Dillon Franke 的客座文章,高级安全工程师,Project Zero 的 20% 时间投入
每秒钟,高权限的 MacOS 系统守护进程都会接受并处理数百条 IPC 消息。在某些情况下,这些消息处理程序会接受来自沙箱或非特权进程的数据。
在这篇博文中,我将探讨使用 Mach IPC 消息作为攻击向量来发现和利用沙箱逃逸。我将详细介绍如何使用自定义的 fuzzing harness、动态 instrumentation 以及大量的调试/静态分析来识别 coreaudiod
系统守护进程中的高风险类型混淆漏洞。在此过程中,我将讨论我遇到的一些困难和权衡。
坦率地说,这是我第一次涉足 MacOS 安全研究和构建自定义 fuzzing harness。我希望这篇文章能为那些希望开展类似研究工作的人提供指导。
我开源了我构建的 fuzzing harness,以及我编写的、在整个项目中对我非常有用的几个工具。所有这些都可以在这里找到:https://github.com/googleprojectzero/p0tools/tree/master/CoreAudioFuzz
方法:知识驱动的 Fuzzing
对于这个研究项目,我采用了一种混合方法,将 fuzzing 和手动逆向工程相结合,我称之为知识驱动的 fuzzing。这种方法从我的朋友 Ned Williamson 那里学来,它平衡了自动化和有针对性的调查。Fuzzing 提供了快速测试各种输入并识别系统行为偏离预期的地方的手段。然而,当 fuzzer 的代码覆盖率达到瓶颈或出现特定障碍时,手动分析就会发挥作用,迫使我更深入地研究目标的内部运作。
知识驱动的 fuzzing 提供了两个关键优势。首先,研究过程永远不会停滞,因为提高 fuzzer 的代码覆盖率的目标始终存在。其次,实现这个目标需要对你正在 fuzzing 的代码有深入的了解。在你开始分类合法的、与安全相关的崩溃时,逆向工程过程将使你对代码库有广泛的了解,从而能够从知情的角度分析崩溃。
我在本次研究中遵循的周期如下:
- 确定一个攻击向量
- 选择一个目标
- 创建一个 fuzzing harness
- Fuzz 并产生崩溃
- 分析崩溃和代码覆盖率
- 迭代 fuzzing harness
- 重复步骤 4-6
确定一个攻击向量
标准的浏览器沙箱通过限制直接的操作系统访问来限制代码执行。因此,利用浏览器漏洞通常需要使用单独的“沙箱逃逸”漏洞。
由于进程间通信 (IPC) 机制允许两个进程相互通信,它们自然可以充当从沙箱进程到非限制进程的桥梁。这使得它们成为沙箱逃逸的主要攻击向量,如下所示。
我选择了 Mach messages,MacOS 操作系统中最低级别的 IPC 组件,作为本次研究的重点攻击向量。我选择它们主要是因为我渴望在最核心的层面上理解 MacOS IPC 机制,以及 Mach messages 历史上存在安全问题的记录。
以前的工作和背景
在利用链中使用 Mach messages 绝不是一个新颖的想法。例如,Ian Beer 在 2016 年发现了一个核心设计问题,该问题涉及 XNU 内核处理 task_t
Mach ports 的方式,允许通过 Mach messages 进行利用。另一篇文章展示了一个在 2019 年在野外发现的利用链如何利用 Mach messages 进行堆grooming 技术。我还从 Ret2 Systems 的博文中获得了许多灵感,该博文介绍了如何利用 Mach message handlers 来寻找和武器化 Safari 沙箱逃逸。
我不会花太多时间详细介绍 Mach messages 的工作原理(最好留给一篇更全面的文章来介绍这个主题),但以下是本博文中 Mach IPC 的简要概述:
- Mach messages 存储在内核管理的 message queues 中,由一个 Mach port 表示。
- 如果进程拥有该 port 的接收权限,它可以从给定的 port 中获取消息。
- 如果进程拥有该 port 的发送权限,它可以向给定的 port 发送消息。
MacOS 应用程序可以使用 bootstrap server 注册服务,bootstrap server 是一个特殊的 Mach port,默认情况下所有进程都拥有其发送权限。这允许其他进程向 bootstrap server 发送 Mach message,询问有关特定服务的信息,bootstrap server 可以使用该服务的 Mach port 的发送权限进行响应。MacOS 系统守护进程通过 launchd
注册 Mach 服务。你可以在 /System/Library/LaunchAgents
和 /System/Library/LaunchDaemons
目录中查看它们的 .plist
文件,以了解注册的服务。例如,下面的 .plist
文件突出显示了使用标识符 com.apple.AddressBook.AssistantService
在 MacOS 上为 Address Book 应用程序注册的 Mach 服务。
<?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 messages,它必须被明确允许。如果进程使用 Apple 的 App Sandbox 功能,这会在一个 .sb
文件中完成,使用 TinyScheme 格式编写。下面的代码片段显示了 WebKit GPU 进程的沙箱文件的摘录。allow mach-lookup
指令用于允许沙箱进程查找和向服务发送 Mach messages。
# 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 服务标识符列表:
❯ ./sbtool2813mach
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
服务,原因如下:
- 它是一个复杂的进程
- 它允许来自几个有影响力的应用程序的 Mach 通信,包括 Safari GPU 进程
- Mach 服务有大量的 message handlers
- 该服务似乎允许控制和修改音频硬件,这可能需要提升的权限
coreaudiod
二进制文件及其大量使用的 CoreAudio Framework 都是闭源的,这将提供独特的逆向工程挑战
创建一个 Fuzzing Harness
一旦我选择了一个攻击向量和一个目标,下一步就是创建一个 fuzzing harness,能够通过攻击向量(Mach message)在目标中的适当位置发送输入。
覆盖引导的 fuzzer 是一种强大的武器,但前提是它的能量集中在正确的位置——就像放大镜集中阳光来生火一样。如果没有适当的焦点,能量就会消散,几乎没有影响。
确定一个入口点
理想情况下,fuzzer 应该完美地复制潜在攻击者可用的环境和能力。然而,这并不总是实际的。通常需要做出权衡,例如接受更高的误报率以提高性能、简化 instrumentation 或易于开发。因此,确定“正确的位置”进行 fuzzing 高度依赖于具体的目标和研究目标。
选项 1:进程间 Fuzzing
所有 Mach messages 都是使用 mach_msg
API 发送和接收的,如下所示。因此,我认为 fuzzing coreaudiod
的 Mach message handlers 最直观的方法是编写一个调用 mach_msg
API 的 fuzzing harness,并允许我的 fuzzer 修改消息内容以产生崩溃。这种方法看起来像这样:
然而,这种方法有一个很大的缺点:由于我们发送的是 IPC 消息,fuzzing harness 将与目标位于不同的进程空间中。这意味着代码覆盖率信息需要在进程边界之间共享,而大多数 fuzzing 工具都不支持这一点。此外,内核 message queue 处理增加了显著的性能开销。
选项 2:直接 Harness
虽然前期需要更多的工作,但另一种选择是编写一个 fuzzing harness,直接加载和调用感兴趣的 Mach message handlers。这将具有巨大的优势,即将我们的 fuzzer 和 instrumentation 放在与 message handlers 相同的进程中,从而使我们能够更容易地获得代码覆盖率。
这种 fuzzing 方法的一个显着缺点是,它假设所有 fuzzer 生成的输入都通过内核的 Mach message 验证层,这在实际系统中发生在 message handler 被调用之前。正如我们稍后将看到的,情况并非总是如此。然而,在我看来,在同一进程空间中进行 fuzzing 的优点(速度和易于收集代码覆盖率)超过了潜在增加误报的缺点。
该方法如下:
- 确定一个适合处理传入 mach messages 的函数
- 编写一个 fuzzing harness,从
coreaudiod
加载消息处理代码 - 使用 fuzzer 生成输入并调用 fuzzing harness
- 盈利,希望如此
寻找 Mach Messager Handler
首先,我搜索了 Mach 服务标识符 com.apple.audioaudiohald
,但在 coreaudiod
二进制文件中没有找到对其的引用。接下来,我使用 otool
检查了它加载的库。从逻辑上讲,CoreAudio framework 似乎是我们消息处理代码的一个不错的候选者。
$ 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 Shared Cache
一些研究表明,从 MacOS Big Sur 开始,大多数 framework 二进制文件不存储在磁盘上,而是存储在 dyld shared cache 中,这是一种预链接库的机制,可以使应用程序运行得更快。值得庆幸的是,IDA Pro、Binary Ninja 和 Ghidra 支持解析 dyld shared cache 以获取其中存储的库。我还使用了这个有用的工具来成功提取库以进行额外分析。
一旦我在 IDA 中有了 CoreAudio Framework,我很快就找到了对 bootstrap_check_in
的调用,该服务标识符作为参数传递,证明 CoreAudio framework 二进制文件负责设置我想 fuzz 的 Mach 服务。然而,尽管进行了大量的逆向工程,但仍然不清楚消息处理代码发生在何处。
事实证明,这是由于使用了 Mach Interface Generator (MIG),这是 Apple 的一种接口定义语言,通过抽象掉大部分 Mach 层,可以更轻松地编写 RPC 客户端和服务器。编译后,MIG 消息处理代码被捆绑到一个称为 subsystem 的结构中。可以很容易地 grep 这些 subsystem 以找到它们的偏移量:
$ 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
符号的交叉引用,它确定了 MIG server 函数,该函数解析传入的 Mach messages!该程序如下所示,第一个参数(rdi 寄存器)是传入的 Mach message,第二个参数(rsi 寄存器)是要返回给客户端的消息。MIG server 函数从 Mach message 中提取 msgh_id
参数,并使用它来索引到 MIG subsystem 中。然后,调用必要的函数处理程序。
我通过在 _HALB_MIGServer_server
函数的 coreaudiod
进程(在禁用 SIP后)上设置一个 LLDB 断点来进一步确认这一点。然后,我调整了系统上的音量,并且命中了断点:
在这个例子中,跟踪从 MIG subsystem 调用的 message handler 显示,基于 Mach message 的 msgh_id
调用了 _XObject_HasProperty
函数。
根据 msgh_id
的不同,可以从 MIG subsystem 访问几十个 message handlers。它们可以通过 MIG 添加的方便的 __X
前缀来轻松识别。
_HALB_MIGServer_server
函数在接近低级别消息处理代码的同时,仍然类似于调用 mach_msg
所需的输入,因此达到了很好的平衡。我决定这是将 fuzz 输入注入到的地方。
创建一个基本 Fuzzing Harness
在确定了我想 fuzz 的函数之后,下一步是编写一个程序来读取一个文件,并将该文件的内容作为输入传递给目标函数。这可能很容易,只需将 CoreAudio 库与我的 fuzzing harness 链接并调用 _HALB_MIGServer_server
函数,但不幸的是,该函数没有导出。
相反,我借用了一些来自 Ivan Fratric 及其 TinyInst 工具的逻辑(我们稍后会更多地讨论它),该工具[从库中返回提供的符号的地址](https://googleprojectzero.blogspot.