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 的代码有深入的了解。在你开始分类合法的、与安全相关的崩溃时,逆向工程过程将使你对代码库有广泛的了解,从而能够从知情的角度分析崩溃。

我在本次研究中遵循的周期如下:

  1. 确定一个攻击向量
  2. 选择一个目标
  3. 创建一个 fuzzing harness
  4. Fuzz 并产生崩溃
  5. 分析崩溃和代码覆盖率
  6. 迭代 fuzzing harness
  7. 重复步骤 4-6

确定一个攻击向量

标准的浏览器沙箱通过限制直接的操作系统访问来限制代码执行。因此,利用浏览器漏洞通常需要使用单独的“沙箱逃逸”漏洞。

由于进程间通信 (IPC) 机制允许两个进程相互通信,它们自然可以充当从沙箱进程到非限制进程的桥梁。这使得它们成为沙箱逃逸的主要攻击向量,如下所示。

A diagram illustrating SANDBOX ESCAPE and PRIVILEGE ESCALATION. The sandbox escape shows a Web Browser Process within a SANDBOX RESTRICTED communicating with a Message Handler via MACH IPC. The privilege escalation shows an Unprivileged Process communicating with a Message Handler Highly Privileged Process via MACH 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 的简要概述:

  1. Mach messages 存储在内核管理的 message queues 中,由一个 Mach port 表示。
  2. 如果进程拥有该 port 的接收权限,它可以从给定的 port 中获取消息。
  3. 如果进程拥有该 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 服务的进程:

A Venn diagram illustrating process types on macOS. The outermost, largest oval represents All MacOS Processes. Within it, a smaller oval represents Processes with a Mach Service. The innermost, smallest oval represents Processes with a Sandbox Allowed Mach Service, indicating a subset of processes with increasing restrictions and specific Mach service permissions.

除了检查沙箱配置文件之外,我还使用 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 服务,原因如下:

创建一个 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 修改消息内容以产生崩溃。这种方法看起来像这样:

A diagram showing inter-process communication. A "SENDING PROCESS" calls mach_msg API, sending a message via "Mach IPC" to a "Kernel-Managed Message Queue". This queue then forwards the message via "Mach IPC" to a "Mach Message Handler" in the "RECEIVING PROCESS".

然而,这种方法有一个很大的缺点:由于我们发送的是 IPC 消息,fuzzing harness 将与目标位于不同的进程空间中。这意味着代码覆盖率信息需要在进程边界之间共享,而大多数 fuzzing 工具都不支持这一点。此外,内核 message queue 处理增加了显著的性能开销。

选项 2:直接 Harness

虽然前期需要更多的工作,但另一种选择是编写一个 fuzzing harness,直接加载和调用感兴趣的 Mach message handlers。这将具有巨大的优势,即将我们的 fuzzer 和 instrumentation 放在与 message handlers 相同的进程中,从而使我们能够更容易地获得代码覆盖率。

A diagram illustrating a SINGLE PROCESS communication. It shows Load Library & Call Message Handler communicating via a Fuzzing Harness to a Mach Message Handler all within the same process.

这种 fuzzing 方法的一个显着缺点是,它假设所有 fuzzer 生成的输入都通过内核的 Mach message 验证层,这在实际系统中发生在 message handler 被调用之前。正如我们稍后将看到的,情况并非总是如此。然而,在我看来,在同一进程空间中进行 fuzzing 的优点(速度和易于收集代码覆盖率)超过了潜在增加误报的缺点。

该方法如下:

  1. 确定一个适合处理传入 mach messages 的函数
  2. 编写一个 fuzzing harness,从 coreaudiod 加载消息处理代码
  3. 使用 fuzzer 生成输入并调用 fuzzing harness
  4. 盈利,希望如此

寻找 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 服务。然而,尽管进行了大量的逆向工程,但仍然不清楚消息处理代码发生在何处。

A screenshot of disassembled code. A function macOS_PlatformBehaviors::get_system_port is shown. A call to _bootstrap_check_in is highlighted, along with the string com.apple.audio.audiohald being passed as a service name.

事实证明,这是由于使用了 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 中。然后,调用必要的函数处理程序。

A flowchart of disassembled code within HALB_MIGServer_server. Annotations highlight Incoming msg rdi and steps to Get msg ID and Get subsystem offset. This offset is then used to Index into function handler based on msg ID" leading to a Call function block.

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

A debugger lldb window showing a breakpoint hit in CoreAudio_HALB_MIGServer_server. The process is stopped at the beginning of this function, with the instruction push rbp highlighted. The thread information indicates the queue is com.apple.audio.device.BuiltInSpeakerDevice.event

在这个例子中,跟踪从 MIG subsystem 调用的 message handler 显示,基于 Mach message 的 msgh_id 调用了 _XObject_HasProperty 函数。

A debugger lldb window showing two states of a stopped process. The first state shows the process stopped at a call rcx instruction within CoreAudio_HALB_MIGServer_server. After a step into si command, the second state shows the process stopped at the beginning of CoreAudio__XObject_HasProperty, as indicated by the red arrow and highlighted function name.

根据 msgh_id 的不同,可以从 MIG subsystem 访问几十个 message handlers。它们可以通过 MIG 添加的方便的 __X 前缀来轻松识别。

A list of function names, likely from a software library or framework, related to object and system context management. Each function name is prefixed with an f icon and highlighted in red, possibly indicating they are of interest for analysis or have been patched. Examples include __XObject_PropertyListener, __XIOContext_PauseIO, __XSystem_CreateIOContext, and __XObject_HasProperty.

_HALB_MIGServer_server 函数在接近低级别消息处理代码的同时,仍然类似于调用 mach_msg 所需的输入,因此达到了很好的平衡。我决定这是将 fuzz 输入注入到的地方。

创建一个基本 Fuzzing Harness

在确定了我想 fuzz 的函数之后,下一步是编写一个程序来读取一个文件,并将该文件的内容作为输入传递给目标函数。这可能很容易,只需将 CoreAudio 库与我的 fuzzing harness 链接并调用 _HALB_MIGServer_server 函数,但不幸的是,该函数没有导出。

相反,我借用了一些来自 Ivan Fratric 及其 TinyInst 工具的逻辑(我们稍后会更多地讨论它),该工具[从库中返回提供的符号的地址](https://googleprojectzero.blogspot.