突破声音壁垒:使用Mach消息对CoreAudio进行模糊测试(第一部分)
每秒钟,高权限的MacOS系统守护进程都会接受和处理数百条IPC消息。在某些情况下,这些消息处理程序会接受来自沙盒或非特权进程的数据。
在这篇博客文章中,我将探讨使用Mach IPC消息作为攻击向量来发现和利用沙盒逃逸。我将详细介绍如何使用自定义模糊测试工具、动态插桩以及大量调试/静态分析来识别coreaudiod系统守护进程中的高风险类型混淆漏洞。在此过程中,我将讨论遇到的一些困难和权衡。
坦率地说,这是我第一次涉足MacOS安全研究领域并构建自定义模糊测试工具。我希望这篇文章能为那些希望从事类似研究工作的人提供指导。
我开源了我构建的模糊测试工具,以及我在这个项目中编写的几个有用工具。所有这些都可以在这里找到:https://github.com/googleprojectzero/p0tools/tree/master/CoreAudioFuzz
方法:知识驱动的模糊测试
在这个研究项目中,我采用了一种结合模糊测试和手动逆向工程的混合方法,我称之为知识驱动的模糊测试。这种方法是从我的朋友Ned Williamson那里学到的,它平衡了自动化和有针对性的调查。模糊测试提供了快速测试各种输入并识别系统行为偏离预期区域的手段。然而,当模糊测试器的代码覆盖率停滞不前或出现特定障碍时,手动分析就会发挥作用,迫使我更深入地研究目标的内部工作原理。
知识驱动的模糊测试有两个关键优势。首先,研究过程永远不会停滞,因为提高模糊测试器代码覆盖率的目标始终存在。其次,实现这一目标需要对你正在模糊测试的代码有深入的理解。当你开始分类合法的、与安全相关的崩溃时,逆向工程过程将使你对代码库有广泛的了解,从而能够从知情角度分析崩溃。
我在研究过程中遵循的循环如下:
- 识别攻击向量
- 选择目标
- 创建模糊测试工具
- 模糊测试并产生崩溃
- 分析崩溃和代码覆盖率
- 迭代模糊测试工具
- 重复步骤4-6
识别攻击向量
标准浏览器沙盒通过限制直接操作系统访问来限制代码执行。因此,利用浏览器漏洞通常需要使用单独的“沙盒逃逸”漏洞。
由于进程间通信(IPC)机制允许两个进程相互通信,它们自然可以作为从沙盒进程到非受限进程的桥梁。这使得它们成为沙盒逃逸的主要攻击向量,如下所示。
我选择了Mach消息,这是MacOS操作系统中最低级别的IPC组件,作为本次研究的攻击向量。我选择它们主要是因为我想在最核心的层面上理解MacOS IPC机制,以及Mach消息的历史安全问题记录。
先前工作和背景
在利用链中利用Mach消息远非新想法。例如,Ian Beer在2016年发现了XNU内核中与处理task_t Mach端口相关的核心设计问题,该问题允许通过Mach消息进行利用。另一篇文章展示了2019年野外利用链如何使用Mach消息进行堆修饰技术。我还从Ret2 Systems的博客文章中获得了许多灵感,该文章讲述了如何利用Mach消息处理程序来发现并武器化Safari沙盒逃逸。
我不会花太多时间详细说明Mach消息的工作原理(这最好留给关于该主题的更全面的文章),但这里简要概述一下本文中的Mach IPC:
- Mach消息存储在内核管理的消息队列中,由Mach端口表示
- 如果进程拥有给定端口的接收权限,则可以从该端口获取消息
- 如果进程拥有给定端口的发送权限,则可以向该端口发送消息
MacOS应用程序可以向引导服务器注册服务,这是一个特殊的mach端口,默认情况下所有进程都拥有发送权限。这允许其他进程向引导服务器发送Mach消息查询特定服务,引导服务器可以响应该服务Mach端口的发送权限。MacOS系统守护进程通过launchd注册Mach服务。你可以在/System/Library/LaunchAgents和/System/Library/LaunchDaemons目录中查看它们的.plist文件,以了解注册的服务。例如,下面的.plist文件突出显示了为MacOS上的地址簿应用程序注册的Mach服务,使用标识符com.apple.AddressBook.AssistantService。
|
|
选择目标
决定研究Mach服务后,下一个问题是选择哪个服务作为目标。为了让沙盒进程向服务发送Mach消息,必须明确允许。如果进程使用Apple的App Sandbox功能,这是在.sb文件中完成的,使用TinyScheme格式编写。下面的代码片段显示了WebKit GPU进程沙盒文件的摘录。allow mach-lookup指令用于允许沙盒进程查找并向服务发送Mach消息。
|
|
这帮助我显著缩小了关注范围,从所有MacOS进程缩小到具有沙盒可访问Mach服务的进程:
除了检查沙盒配置文件外,我还使用了Jonathan Levin的sbtool实用程序来测试给定进程可以与之交互的Mach服务。该工具(虽然有点过时,但我能够编译它)在底层使用内置的sandbox_exec函数,提供可访问的Mach服务标识符的详细列表:
|
|
最终,我选择查看coreaudiod守护进程,特别是com.apple.audio.audiohald服务,原因如下:
- 这是一个复杂的进程
- 它允许来自多个有影响力应用程序的Mach通信,包括Safari GPU进程
- Mach服务有大量的消息处理程序
- 该服务似乎允许控制和修改音频硬件,这可能需要提升的权限
- coreaudiod二进制文件及其大量使用的CoreAudio框架都是闭源的,这将提供独特的逆向工程挑战
创建模糊测试工具
选择攻击向量和目标后,下一步是创建一个模糊测试工具,能够通过攻击向量(Mach消息)在目标内的适当位置发送输入。
覆盖率引导的模糊测试器是一个强大的武器,但只有当它的能量集中在正确的地方时才能发挥作用——就像放大镜聚焦阳光来点火一样。没有适当的聚焦,能量就会消散,影响甚微。
确定入口点
理想情况下,模糊测试器应该完美复制潜在攻击者可用的环境和能力。然而,这并不总是实用的。通常需要做出权衡,例如接受更高的误报率以换取更高的性能、简化的插桩或易于开发。因此,确定模糊测试的“正确位置”高度依赖于特定目标和研究目标。
选项1:进程间模糊测试
所有Mach消息都使用mach_msg API发送和接收,如下所示。因此,我认为模糊测试coreaudiod的Mach消息处理程序最直观的方法是编写一个调用mach_msg API的模糊测试工具,并允许我的模糊测试器修改消息内容以产生崩溃。这种方法看起来像这样:
然而,这种方法有一个很大的缺点:由于我们发送的是IPC消息,模糊测试工具将与目标处于不同的进程空间。这意味着代码覆盖率信息需要跨进程边界共享,而大多数模糊测试工具不支持这一点。此外,内核消息队列处理增加了显著的性能开销。
选项2:直接工具
虽然前期需要更多工作,但另一个选择是编写一个模糊测试工具,直接加载和调用感兴趣的Mach消息处理程序。这将具有巨大的优势,将我们的模糊测试器和插桩置于与消息处理程序相同的进程中,使我们能够更轻松地获取代码覆盖率。
这种模糊测试方法的一个显著缺点是它假设所有模糊测试器生成的输入都通过了内核的Mach消息验证层,这在真实系统中发生在消息处理程序被调用之前。正如我们稍后将看到的,情况并非总是如此。然而,在我看来,在同一进程空间中进行模糊测试的优势(速度和易于收集代码覆盖率)超过了潜在增加误报率的缺点。
方法如下:
- 识别处理传入mach消息的合适函数
- 编写模糊测试工具从coreaudiod加载消息处理代码
- 使用模糊测试器生成输入并调用模糊测试工具
- 希望获利
查找Mach消息处理程序
首先,我搜索了Mach服务标识符com.apple.audioaudiohald,但在coreaudiod二进制文件中没有找到对它的引用。接下来,我使用otool检查了它加载的库。逻辑上,CoreAudio框架似乎是容纳我们消息处理程序代码的良好候选。
|
|
然而,我惊讶地发现otool返回的路径不存在!
|
|
Dyld共享缓存
一些研究显示,自MacOS Big Sur以来,大多数框架二进制文件不存储在磁盘上,而是存储在dyld共享缓存中,这是一种预链接库的机制,允许应用程序运行更快。幸运的是,IDA Pro、Binary Ninja和Ghidra支持解析dyld共享缓存以获取其中存储的库。我还使用了这个有用的工具成功提取库以进行额外分析。
在IDA中获取CoreAudio框架后,我很快找到了对bootstrap_check_in的调用,其中服务标识符作为参数传递,证明CoreAudio框架二进制文件负责设置我想要模糊测试的Mach服务。然而,尽管进行了相当多的逆向工程,仍然不清楚消息处理代码在哪里发生。
事实证明,这是由于使用了Mach Interface Generator(MIG),这是Apple的一种接口定义语言,通过抽象掉大部分Mach层,使编写RPC客户端和服务器更容易。编译时,MIG消息处理代码被打包到一个称为子系统的结构中。可以轻松grep这些子系统以找到它们的偏移量:
|
|
接下来,我在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所需的输入之间取得了很好的平衡。我决定这是注入模糊测试输入的地方。
创建基本模糊测试工具
确定要模糊测试的函数后,下一步是编写一个程序来读取文件并将文件内容作为输入传递给目标函数。这可能就像将CoreAudio库与我的模糊测试工具链接并调用_HALB_MIGServer_server函数一样简单,但不幸的是,该函数未导出。
相反,我从Ivan Fratric和他的TinyInst工具(我们稍后会更多地讨论它)借用了一些逻辑,该工具从库中返回提供的符号地址。代码解析Mach-O二进制文件的结构,特别是它们的头部和加载命令,以定位和提取符号信息。这使得即使目标函数未导出,也可以在我的模糊测试工具中解析和调用目标函数。
因此,我的工具的高级功能如下:
- 加载CoreAudio库
- 从CoreAudio库获取目标函数的函数指针
- 从文件读取输入
- 使用输入调用目标函数
我的模糊测试工具的完整实现可以在这里找到。下面显示了调用工具从输入文件发送消息的示例:
|
|
收集合法的Mach消息
我现在有一种方法可以直接将数据传递到我想要模糊测试的MIG子系统(_HALB_MIGServer_server)。然而,我不知道处理程序期望的特定消息大小、选项或数据。虽然覆盖率引导的模糊测试器会随着时间的推移开始发现正确的消息格式,但在开始模糊测试时获取合法输入的种子语料库以提高效率是有利的。
为此,我使用LLDB在MIG子系统上设置断点并转储第一个参数(包含传入的Mach消息)。然后,我操作操作系统以导致Mach消息发送