打破声音屏障,第二部分:利用CVE-2024-54529

访问原始链接 Google 翻译

在本系列的第一部分中,我详细介绍了进入macOS安全研究的历程,通过我称之为知识驱动模糊测试的过程,在coreaudiod系统守护进程中发现了类型混淆漏洞(CVE-2024-54529)和双重释放漏洞(CVE-2025-31235)。虽然第一篇文章侧重于发现漏洞的过程,但本文将深入探讨利用类型混淆漏洞的复杂过程。

我将解释将潜在可利用的崩溃转化为有效漏洞利用的技术细节:这是一段充满死胡同、创造性问题解决和最终成功的旅程。

漏洞回顾:快速概览

如果您还没有阅读,我强烈建议您在继续之前阅读我关于此漏洞的详细分析。

简单回顾一下,CVE-2024-54529是coreaudiod进程使用的CoreAudio框架中com.apple.audio.audiohald Mach服务内的一个类型混淆漏洞。多个Mach消息处理程序,例如_XIOContext_Fetch_Workgroup_Port,会根据Mach消息中的ID从对象映射(Object Map)中获取一个HALS_Object,然后对其进行操作,假设它是特定类型(ioct)而没有进行适当的验证。

当代码尝试对存储在HALS_Object中的对象指针进行虚函数调用时,这种错误的假设导致了崩溃,如下面的堆栈跟踪所示:

Process 82516 stopped
* thread #8, queue = 'com.apple.audio.system-event', stop reason = EXC_BAD_ACCESS (code=1, address=0xffff805cdc7f7daf)
    frame #0: 0x00007ff81224879a CoreAudio`_XIOContext_Fetch_Workgroup_Port + 294
CoreAudio`_XIOContext_Fetch_Workgroup_Port:
    0x7ff81224879a <+291>: mov    rax, qword ptr [rdi]
->  0x7ff81224879d <+294>: call   qword ptr [rax + 0x168]
    0x7ff8122487a3 <+300>: mov    dword ptr [rbx + 0x1c], eax
    0x7ff8122487a6 <+303>: mov    rdi, r13
(lldb) bt
* thread #8, queue = 'com.apple.audio.system-event', stop reason = EXC_BAD_ACCESS (code=1, address=0xffff805cdc7f7daf)
  * frame #0: 0x00007ff81224879a CoreAudio`_XIOContext_Fetch_Workgroup_Port + 294
    frame #1: 0x00007ff812249c81 CoreAudio`HALB_MIGServer_server + 84
    frame #2: 0x00007ff80f359032 libdispatch.dylib`dispatch_mig_server + 362
    frame #3: 0x00007ff811f202ed CoreAudio`invocation function for block in AMCP::Utility::Dispatch_Queue::install_mig_server(unsigned int, unsigned int, unsigned int (*)(mach_msg_header_t*, mach_msg_header_t*), bool, bool) + 42
    frame #4: 0x00007ff80f33e7e2 libdispatch.dylib`_dispatch_client_callout + 8
    frame #5: 0x00007ff80f34136d libdispatch.dylib`_dispatch_continuation_pop + 511
    frame #6: 0x00007ff80f351c83 libdispatch.dylib`_dispatch_source_invoke + 2077
    frame #7: 0x00007ff80f3447ba libdispatch.dylib`_dispatch_lane_serial_drain + 322
    frame #8: 0x00007ff80f3453e2 libdispatch.dylib`_dispatch_lane_invoke + 377
    frame #9: 0x00007ff80f346393 libdispatch.dylib`_dispatch_workloop_invoke + 782
    frame #10: 0x00007ff80f34f0db libdispatch.dylib`_dispatch_root_queue_drain_deferred_wlh + 271
    frame #11: 0x00007ff80f34e9dc libdispatch.dylib`_dispatch_workloop_worker_thread + 659
    frame #12: 0x00007ff80f4e2c7f libsystem_pthread.dylib`_pthread_wqthread + 326
    frame #13: 0x00007ff80f4e1bdb libsystem_pthread.dylib`start_wqthread + 15

理解目标

利用此类漏洞似乎很简单:如果我们能控制rax寄存器偏移0x168处被解引用的地址,我们就可以劫持控制流。但这并不那么简单。从堆中获取的HALS_Object在call指令执行之前被解引用了多次:

因此,漏洞利用需要建立一个指针链。首先,我们需要在HALS_Object的偏移0x68处设置一个值,指向我们在内存中控制的区域。这个区域反过来需要在其自身的偏移0x0处包含一个指针,指向一个同样受我们控制的伪造虚函数表(vtable)。有了这个链,我们就可以在伪造虚函数表的偏移0x168处写入目标地址以劫持控制流。方法如下所示:

初始利用尝试与CFString障碍

最直接的利用路径似乎是通过API向HALS_Object的易受攻击偏移(0x68)写入任意数据。我最初的想法是创建一个CFString对象,并找到一种方法将其指针放置到HALS_Object的易受攻击偏移处。

我在coreaudiod中找到了一个看起来不错的API,可以调用它将偏移0x68设置为攻击者控制的CFString:

然而,这种方法很快遇到了障碍。CFString类型有一个不可控制的头部,这意味着即使我可以控制CFString的内容,我也无法控制对象的头部。为了使此漏洞利用生效,我需要CFString偏移0x0处的数据是一个指向我控制数据的指针。CFString的头部使这变得不可能。

这意味着我需要一种新方法。我必须找到另一种方式来控制易受攻击偏移处的内存。

工具集

由于我最初寻找合适对象原语的尝试证明是徒劳的,很明显我需要一种更好的方法来可视化coreaudiod堆并理解其上的对象。为此,我构建了几个自定义工具。

其中最有用的一个是我使用Ivan Fratric的TinyInst Hook API编写的自定义对象转储工具。该工具钩入进程并遍历HALS_ObjectMap链表,转储堆上当前每个HALS_Object的原始内容、大小、类型和子类型。这给了我一个强大的方法来检查每个对象的组成,搜索可控数据,并查看关键0x68偏移处是否已存在任何有趣的指针。

除了这个动态分析工具,我还使用了一个IDAPython脚本来执行有针对性的静态分析,寻找在通过CopyObjectByObjectID获取对象后写入感兴趣偏移的任何代码路径。这种动态和静态分析的结合对于系统地映射利用面至关重要。

强制堆上的越界读取

借助我的对象转储工具,我决定研究另一条潜在的利用路径。如果我找不到直接将指针写入偏移0x68的方法,也许我可以触发越界读取以达到类似效果。

想法是找到一个小于0x68字节的HALS_Object,在堆上创建它,然后在内存中精心放置第二个攻击者控制的对象紧随其后。如果我在第一个(较小的)对象上触发类型混淆,代码尝试从偏移0x68读取时,将越过对象的边界,读取到第二个对象的受控数据中。

不幸的是,我的对象转储工具和静态分析很快证明这是一条死胡同。在编目了所有对象类型后,很明显不存在小于0x68字节的对象。在我研究期间可用的最新macOS版本(macOS Sequoia 15.0.1)中,最小的对象类型stap是0x70字节。

有趣的是,我查看的早期macOS版本(包括macOS Ventura 13.1)确实包含较小的HALS_Object,这表明软件版本的差异有时会引入新的利用原语。

类型 大小
clnt 0x158
ioct 0xF0
sive 0x78
astr 0xD0/0xD8/0xB8/0x98
stap 0x70
asub 0x80
aplg 0x258/0x248/0x1B0/0xB0/0x88
adev 0x740/0x6E0/0x7A0/0x840
abox 0x198
engn 0x308/0x480
crsd 0xB8

随着越界读取可能性的排除,我的注意力重新回到了堆操作和寻找直接控制对象分配内容的方法上。

一线希望:ngne对象中的未初始化内存

为了寻找其他利用原语,我转向了macOS上一个强大的调试工具:启用了PreScribble选项的Guard Malloc。此功能使用特定的字节模式(0xAA)初始化新分配的内存块,使得很容易发现对象何时未正确清零并可能导致使用未初始化内存。

使用这些设置运行coreaudiod,我发现了一个对象类型ngne,它具有一个奇特的属性:对象内存的一部分是未初始化的。具体来说,正确偏移处的一个指针大小字段的6个高字节在分配时未被清除,保留了来自PreScribble的0xAA模式。

**

这是一个改变游戏规则的因素。未初始化内存漏洞可能提供我需要的原语,以控制易受攻击偏移处的指针。

棘手的限制

你可能会问,为什么只有6个未初始化字节?开发者在定义ngne对象时,可能在偏移0x68处做了类似这样的事情:

class NGNE {
...
  size_t previous_var; // offset 0x60
  short var=0; // offset 0x68
  size_t  next_var; // offset 0x70
...
}

这是因为编译器为了优化,会将8字节变量(如x64上的size_t)对齐到8字节边界。因此,short变量导致next_var被放置在偏移0x70处,而不是紧接在var之后的0x6A处,留下了一个6字节的未初始化间隙。

这个限制会使事情变得有点棘手。即使我们能让受控内存出现在对象内,最后2个字节也会被清零。

新的利用策略

有了这个新知识,我制定了一个新的、更复杂的利用策略:

  1. 分配受控数据:找到一种方法在coreaudiod进程中分配大量我控制的数据。
  2. 创建间接指针:创建指向我受控数据的间接指针。
  3. 释放包含指针的数据。
  4. 重用指针:在分配ngne对象时,诱使程序重用包含指针的内存。

使用属性列表进行堆风水

为了控制大部分内存,我转向了Apple API中的一个常见功能:属性列表。许多API接受用户数据作为序列化的plist文件,然后反序列化,为CoreFoundation对象分配内存。CoreAudio暴露了一个API HALS_Object_SetPropertyData_DPList,它正是这样做的,将其存储在堆上:

plist允许您指定几种类型的嵌套值:

Core Foundation 类型 XML 元素
CFArrayRef
CFDictionaryRef
CFStringRef
CFDataRef
CFDateRef
CFNumberRef (Int)
CFNumberRef (Float)
CFBooleanRef 或

这意味着我可以创建包含大量CFString或CFData对象的plist文件,为我提供强大的原语来大规模分配数据和控制堆布局。此外,我可以添加CFArray或CFDictionary对象以实现漏洞利用所需的间接性,因为这些数据类型包含指向其他用户控制对象的指针。

整体结构如下所示:

但你可能想知道:这难道不会带来与我们尝试分配指向CFString的指针时类似的问题吗?(指针链会尝试解引用CFRuntimeBase头部并失败)。是的!但讽刺的是,偏移0x68处最后2个字节的清除开辟了新的可能性:我们可能在数组中间分配一个对象,覆盖一个CFString指针,在最后2个字节被清除后,该指针指向原始数据。这看起来有点渺茫,但我愿意接受挑战!

释放数据

接下来,我需要释放已分配给我的数据的内存结构。这很容易——我只需要用一个小得多的plist再次调用API。然后,我分配的大型plist结构就被释放了。

在ngne对象中重用已释放的数据

经过一些痛苦的反向工程,我找到了一种按需创建ngne对象的方法,即向audiohald服务发送精心构造的Mach消息。我以为我就要成功了。我的计划是喷洒堆,释放内存,然后立即分配我的ngne对象来回收它。

但我很快遇到了一个根本性的、令人沮丧的障碍:malloc区域。

我可以创建的ngne对象大小为776字节,这使它们正好位于malloc_tiny内存区域。这是一个关键问题,因为作为一项安全缓解措施,macOS的内存分配器在分配时会安全地清零malloc_tiny区域中的任何内存。我精心设计的堆喷洒将在ngne对象放置在其上之前被清除干净。

我的漏洞利用就此搁浅。

新的希望:启动时的ngne对象

这迫使策略转变。如果我想使用未初始化内存,我需要将分配落在不会清零的malloc区域中。我的分析显示,较大的ngne对象——超过1100字节——可以被创建,并将被放置在malloc_small区域,该区域在分配时不会被清零。但问题是?我找不到任何用户可访问的API来触发它们的创建。它们似乎只在coreaudiod在启动时注册音频插件时才被实例化。

所以,我找到了一些适合利用的ngne对象,但它们只在启动时实例化,在我们能够进行堆喷洒之前。这激发了一个想法:如果我执行堆喷洒,然后故意使进程崩溃会怎样?当它重新启动时(所有系统守护进程在macOS上都会自动重启),它能否在我们的喷洒数据上分配一个对象?

在启动时加载到内存中

我必须克服的一个困难是,崩溃后,新生成的coreaudiod将在新的进程空间内分配。这意味着先前分配的堆喷洒将不再起作用。

然而,我发现了一个有助于此的好功能:在执行我们的plist堆喷洒时,CoreAudio将数据序列化到磁盘上的文件/Library/Preferences/Audio/com.apple.audio.DeviceSettings.plist。

然后,在启动时,plist从磁盘获取,用当前运行时信息更新,并保存回磁盘,如下所示。

__int64 __fastcall CASettingsStorage::SetCFTypeValue(
        CFMutableDictionaryRef *this,
        const __CFString *key,
        const void *value)
{
  CASettingsStorage::RefreshSettings((CASettingsStorage *)this);
  CFDictionarySetValue(this[2], key, value);
  return CASettingsStorage::SaveSettings((CASettingsStorage *)this);
}

幸运的是,CASettingsStorage::SaveSettings函数创建了内存中plist的副本,将其写入磁盘,然后释放该副本。 值得庆幸的是,这个过程发生在系统创建ngne对象之前。

void __fastcall CASettingsStorage::SaveSettings(CASettingsStorage *this)
{
  if ( !*((_BYTE *)this + 50) )
  {
    v1 = (const void *)*((_QWORD *)this + 2);
    if ( v1 )
    {
      Data = CFPropertyListCreateData(0LL, v1, *((CFPropertyListFormat *)this + 3), 0LL, 0LL);
      v3 = fopen(*(const char **)this, "w+");

      ----TRUNCATED FILE WRITE OPERATIONS----

      CACFData::~CACFData(Data);
    }
  }
}

这意味着每次进程重新启动时,我们的整个plist结构都会被重新分配然后释放,使我们的数据有机会最终出现在ngne对象的易受攻击偏移处。

更新的利用策略

新的攻击策略如下所示:

  1. 分配受控数据:向coreaudiod发送mach消息以调用HALS_Object_SetPropertyData_DPList消息处理程序。包含一个带有受控数据的大型plist。plist将被存储到磁盘。
  2. 触发类型混淆:触发类型混淆漏洞,只是为了使进程崩溃。
  3. 让魔法发生:等待coreaudiod:
    • 重新启动。
    • 从磁盘加载精心构造的plist。
    • 在内存中创建一个plist。
    • 释放plist.。
    • 在已释放的plist对象上分配一个ngne对象(希望如此)。
  4. 再次触发类型混淆:在随机的ngne对象上触发它,并希望它重用了我们喷洒的数据。
  5. 重复:重复步骤3-4直到成功!

验证方法

为了使漏洞利用生效,很多事情都需要顺利进行。在继续之前,我想确保我的攻击链不是纯粹理论上的——我寻求的指针链实际上可以出现在对象中。

为此,我利用了coreaudiod提供的XSystem_Get_Object_Info消息处理程序。这个API允许我枚举系统上的所有HALS对象,并确定哪些是ngne类型。

然后,我修改了我的对象转储工具,使其仅转储ngne对象,并持续运行直到找到指向喷洒数据的指针链。在精心制作完美的plist进行了大量实验后,我终于让一切完美对齐了!

构建ROP链

一旦我可以将执行重定向到我控制的数据,最后一步就是构建一个面向返回的编程(ROP)链以实现任意代码执行。由于目标是CoreAudio库(它存储在dyld共享缓存中,并且在系统重启前具有恒定地址),因此在权限提升的上下文中不需要击败ASLR。我制作了一个ROP链,用于在通常只有coreaudiod可访问的位置打开和写入文件。由于ROP链编码在其中一个CFString对象中,为了避免无效UTF-8字节的问题,使用了UTF-16字符串编码。

# Beginning of stack after pivot
rop   = bytearray(p64(LOAD_RSP_PLUS_EIGHT)) # lea rax, [rsp + 8] ; ret
rop  += p64(ADD_HEX30_RSP)       # add rsp, 0x30 ; pop rbp ; ret
rop  += INLINE_STRING            # Inline "/Library/Preferences/Audio/malicious.txt"
rop  += b'\x42' * 15             # pop rbp filler and will be moved past
rop  += p64(MOV_RAX_TO_RSI)      # mov rsi, rax ; mov rax, rsi ; pop rbp ; ret
rop  += p64(0x4242424242424242)  # pop rbp filler
rop  += p64(MOV_RSI_TO_RDI)      # mov rdi, rsi ; mov rax, rdi ; mov rdx, rdi ; ret
rop  += p64(POP_RSI_GADGET)      # pop rsi ; ret
rop  += p64(0x201)               # O_CREAT | O_WRONLY
rop  += p64(POP_RDX_GADGET)      # pop rdx ; ret
rop  += p64(0x1A4)               # 0644
rop  += p64(POP_RAX_GADGET)      # pop rax ; ret
rop  += p64(0x2000005)           # syscall number for open()
rop  += p64(SYSCALL)             # syscall
rop += b'\x42' * (1152 - len(rop))

# [rax + 0x168] → pointer to pivot gadget (entrypoint)
rop[0x168:0x170] = p64(STACK_PIVOT_GADGET)  # xchg rsp, rax ; xor edx, edx ; ret

一切就绪后,漏洞利用成功执行了ROP链,使我控制了coreaudiod进程。以下显示了喷洒在内存中的ROP链:

需要注意的是,此漏洞利用是为运行在Intel CPU上的macOS编写的。在Apple Silicon系统上,使用相同技术进行利用将需要能够正确签署构成指针链和ROP小工具的指针。

演示

以下视频演示显示了在macOS Sequoia 15.0.1上运行的PoC漏洞利用:

结论

利用CVE-2024-54529是一段从看似简单的类型混淆到涉及堆喷洒、未初始化内存以及精心策划的一系列崩溃和重启的多阶段漏洞利用的旅程。这项研究突显了沙箱逃逸向量的威力和重要性,并展示了“知识驱动模糊测试”方法如何能够导致发现和利用高影响漏洞。

本研究中使用的所有工具,包括模糊测试工具、自定义插桩和CVE-2024-54529的概念验证,都已开源并可供使用。