macOS上的NULL指针解引用历史
[正文内容]
macOS上的NULL指针解引用历史
2025年3月10日
Karol Mazurek
Blog
在模糊测试中发现崩溃仅仅是漏洞研究的开始。在发现崩溃之后,漏洞利用开发通常是一段漫长的旅程。并非每个漏洞都是可利用的,我之前的文章(案例研究:分析macOS IONVMeFamily驱动拒绝服务问题)就是一个很好的证明。有时,我确信某些东西可以被利用,但随后我面临着操作系统实施的一系列缓解措施。
在我最近的一次模糊测试会话中,我发现了一个错误,经过一些阅读、试错和逆向工程,确定由于多年来在macOS中实施的各种缓解措施而无法利用。其中一项缓解措施专门针对NULL指针解引用。
我遇到了许多关于这个主题的宝贵资源,但找到它们花费了相当长的时间。因此,我决定撰写一篇概述文章,总结关键点,并包含指向这些参考资料的链接,供其他寻求特定问题答案的人:
为什么自arm64以来macOS上的NULL指针解引用不再可利用?享受阅读!
简介:Apple Silicon上的NULL指针解引用
当软件尝试通过设置为NULL的指针访问地址0(NULL地址)的内存时,会发生NULL指针解引用。在ARM64(如Apple Silicon)上,地址0通常不像其他架构那样映射在虚拟内存中。因此,解引用NULL指针通常会触发内存访问错误。在内核上下文中,此错误会导致立即panic,而不是读取或写入有效数据。后果是拒绝服务(崩溃),因为内核停止执行以保护自身。
NULL指针解引用是一种编程错误,不会执行攻击者控制的代码——它只是导致错误。下面是一个简单的用户态示例,您可以编译并运行以亲自查看:
//clang -o null_dereference null_dereference.c
#include <stdio.h>
int main() {
int *ptr = NULL; // 初始化一个指向NULL的指针
int value = *ptr; // 尝试解引用NULL指针
printf("Value: %d\n", value); // 如果上面的解引用导致崩溃,则永远不会到达此行
return 0;
}
从历史上看,攻击者已经找到了在某些条件下利用内核NULL解引用的方法。
我们如何利用它?
经典的利用场景包括通过将受控内存页映射到该地址来欺骗内核_不_在地址零上出错。如果成功,内核可能不会崩溃,而是读取或执行地址0处的攻击者提供的数据,从而可能导致内核模式下的代码执行。
在基于ARM64的系统上,Apple设置了内存保护,以便在正常操作期间零地址保持未映射。任何使用NULL指针的尝试都会导致数据中止(内存访问异常)。因此,内核中的NULL解引用通常会停止系统以确保安全。
只有当攻击者能够以某种方式将受控数据放置在地址零并让内核使用它时,它才会成为可利用的条件。正如我们将看到的,现代macOS使得这非常困难。
macOS中NULL解引用的历史利用
在早期macOS版本(在Intel架构上)中,通常的方法是在用户空间中将伪造对象映射到NULL地址,以便当内核错误地解引用NULL指针时,它将访问攻击者的伪造数据而不是出错。这需要绕过操作系统对低地址内存映射的正常预防。
在少数情况下,这是可能的。
__PAGEZERO
在macOS上,默认情况下,64位进程无法在地址0处映射内存——系统保留一个大的未映射区域以捕获空指针错误。在每个Mach-o中,我们可以找到__PAGEZERO
段:
来源:Snake&Apple I — Mach-O 和 macOS and iOS Security Internals Advent Calendar – Day 5
实际上,对于64位Mach-O二进制文件,地址空间的前4GB是保留的且不可访问的。
使用32位二进制文件的NULL页面映射技术
但是,在旧版本的macOS中,当支持32位应用程序时,我们可以使用链接器标志编译一个32位二进制文件(-m 32
)以禁用page-zero保留(-pagezero_size 0x0
)。
// clang -o poc poc.c -framework IOKit -m32 -pagezero_size 0x0
这种遗留的怪癖允许32位进程在地址0处成功分配内存。攻击者使用它将shellcode或伪造对象放置在NULL,然后触发内核漏洞。
// 1. 释放NULL页面区域:
err = vm_deallocate(mach_task_self(), 0x0, 0x1000);
// 2. 在地址0处分配内存:
vm_address_t addr = 0;
err = vm_allocate(mach_task_self(), &addr, 0x1000, 0);
// 3. 用受控数据填充NULL页面:
char* np = 0;
for (int i = 0; i < 0x1000; i++){
np[i] = '\x41';
}
例如,来自Cisco Talos的Piotr Bania在2016年通过利用Intel图形驱动程序错误演示了这一点。他编译了一个没有pagezero
的32位payload,在0
处映射了一个页面,并使内核从该页面调用一个函数指针,从而实现内核代码执行,甚至绕过了KASLR。
值得注意的是,这需要禁用SMEP保护,这将在后面讨论。
TPWN 0-day (OS X Yosemite, 2015)
但在此之前,一个众所周知的例子是Luca Todesco的2015年针对OS X Yosemite的漏洞,他结合了IOKit中的NULL指针解引用和信息泄露来获得root权限。该漏洞被称为“tpwn
”,并且公开可用。以下是简化的利用流程:
[初始用户访问]
↓
[信息泄露漏洞]
↓
[获取kalloc.1024 zone指针]
↓
[绕过内核ASLR]
↓
[内存损坏原语]
↓
[IOKit中的NULL指针解引用]
↓
[在内核内存中的任何位置OR 0x10]
↓
[损坏vtable指针]
↓
[触发IOServiceRelease]
↓
[在vtable+0x20处执行代码]
↓
[栈旋转 (RSP = RAX)]
↓
[执行ROP链]
↓
[将UID设置为0(root)]
↓
[清理内存损坏]
↓
[调整任务计数]
↓
[解锁IOAudioEngine锁]
↓
[获得root权限]
Todesco还开发了一种名为NULLGuard的保护措施,以防御NULL指针解引用错误,这些错误后来在OS X 10.11 (El Capitan)中得到缓解,该版本引入了System Integrity Protection (SIP)和其他强化措施。
当时,如果满足其他条件,NULL解引用可能是升级的垫脚石。
macOS OS X 10.11 (El Capitan) – IOKit Driver Exploits Race (2015–2016)
在此期间,在macOS内核驱动程序中发现了许多漏洞,其中NULL指针用作对象引用。通过在恶意进程中映射NULL页面,然后竞争或强制指针变为NULL,研究人员可以实现对指令指针的控制。
- 例如,Ian Beer在NVIDIA GeForce驱动程序中发现的CVE-2016-1846允许用户映射地址零并在那里放置一个伪造的C++对象。然后,驱动程序在NULL对象上调用一个虚拟函数,从伪造的Virtual Table跳转到攻击者控制的函数指针。
┌────────────────────────────────────────────────────────────────┐
│ Process Virtual Memory │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ NULL Page │ ◄── 1. vm_deallocate(0x0, 0x1000) │
│ │ (0x0-0xFFF) │ ◄── 2. vm_allocate(&addr=0, 0x1000, 0) │
│ │ │ ◄── 3. Fill with 0x41 bytes │
│ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Fake VTable │ │
│ │ 0x41414141 │ ◄── Function pointers in VTable │
│ │ 0x41414141 │ (will be called by kernel) │
│ │ ... │ │
│ └─────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ Kernel Execution Flow │
├────────────────────────────────────────────────────────────────┤
│ │
│ Thread 1 Thread 2 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Open driver │ │ Wait for │ │
│ │ connection │──┐ ┌──│ spinlock │ │
│ └─────────────┘ │ │ └─────────────┘ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Release │ │ │ │ Acquire │ │
│ │ spinlock │──┼────────┼─▶│ spinlock │ │
│ └─────────────┘ │ │ └─────────────┘ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ┌─────────────┐ │ │ ┌─────────────┐ │
│ │ Close driver│ │ │ │ Call IOKit │ │
│ │ connection │◄─┘ └─▶│ method │ │
│ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ nvCommandQueue:: │ │
│ │ GetHandleIndex accesses │ │
│ │ NULL pointer at +0x5b8 │ │
│ └─────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────┐ │
│ │ Virtual method call on │ │
│ │ NULL object jumps to │ │
│ │ address 0x4141414141414141| │
│ └───────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘
- 同样,IOAudioEngine中的一个错误(CVE-2016-1821)将对象设置为NULL,但后来使用了它,从而允许攻击者在地址零处提供伪造的
reserved->streams
数组并重定向执行。
在这些情况下,映射null页面为攻击者提供了对内核RIP的简单控制。
Supervisor Mode Execution Prevention (SMEP)
在一些较旧的Mac上,缺少或禁用了防止在内核中执行用户内存的硬件保护。在Talos示例中,研究人员指出,现代CPU具有SMEP,这将阻止内核执行放置在用户页面中的代码(如NULL页面)。如果在系统上未启用SMEP,则该漏洞可以直接从映射的NULL页面执行shellcode。
在较新的硬件上,Apple已开始使用称为SMEP的CPU功能来禁止在内核模式下执行用户空间内存。它于2016年末在OS X 10.11 (El Capitan)中引入。
macOS上的Intel缓解措施
这些历史漏洞表明,当攻击者可以映射地址零时,如果内核随后将该内存用作指针或代码,则NULL解引用错误可能会导致任意代码执行。
Apple在2016年末通过以下方式关闭了这些途径。Apple的安全更新将此类错误重新分类为拒绝服务问题,并通过添加NULL检查或更好的状态处理来修复它们:
Apple完全删除了macOS 10.15中对32位进程的支持,并启用了SMEP,实际上完全消除了了在基于Intel的机器上映射NULL页面的主要技巧。
macOS Apple Silicon上的现代缓解措施
随着向Apple Silicon(ARM64架构)和现代macOS版本(11.0 Big Sur及更高版本)的过渡,Apple引入了强大的缓解措施,使得NULL指针解引用不可利用。
严格的NULL页面映射保护
Apple Silicon上的所有用户进程都是64位的,并且内核不会在用户空间中将任何内存映射到地址0。如__PAGEZERO
中所述,Mach-O格式保留了低地址区域(4GB)。
即使没有此保留,macOS内核也会强制执行用户代码无法分配的最小地址(如Linux中的mmap_min_addr[](https://afine.com/history-of-null-pointer-dereferences-on-macos/<https:/wiki.debian.org/mmap_min_addr>)
)。简而言之,我们无法将数据放置在NULL地址附近或NULL地址处。
这意味着任何内核对NULL指针的解引用都会击中未映射的区域并立即触发错误,而不是访问攻击者控制的内存。
硬件执行和访问控制 (PXN/PAN)
Apple的Silicon利用了类似于Intel上的SMEP/SMAP的ARMv8功能。ARM64具有Privileged Execute Never (PXN),它确保内核无法执行标记为用户空间的页面中的代码。
相比之下,**Privileged Access Never (PAN)**阻止内核在没有显式覆盖的情况下意外读取/写入用户空间内存。内核将所有用户内存标记为PXN,因此即使漏洞以某种方式将执行重定向到用户页面(例如,地址0),处理器也会拒绝执行它。
同样,PAN会阻止内核读取放置在用户控制的NULL页面中的恶意数据(它需要一个特殊的指令序列才能有意访问用户内存)。
1. 攻击者在用户空间NULL (0x0)处映射shellcode。
2. 漏洞劫持内核控制流到0x0。
3. 硬件检查:
├─ PXN=1 → 指令获取错误(执行被拒绝)
└─ PAN=1 → 数据访问错误(如果内核读取0x0,则访问被拒绝)。
4. 系统通过错误停止漏洞。
这些硬件保护使得从NULL页面执行shellcode的经典场景不可行——该尝试只会导致另一个错误。
指针认证代码 (PAC)
Apple的ARM64e架构的一个主要增强功能是指针认证,它在A12/M1芯片(ARMv8.3-A)上引入。指针认证代码向指针值(例如返回地址和函数指针)添加一个加密签名,该签名在使用时进行验证。
来源:PAC it up: Towards Pointer Integrity using ARM Pointer Authentication∗
macOS在内核中广泛使用PAC来防止指针损坏。如果通过操纵内核指针来利用NULL解引用错误,PAC很可能会检测到篡改。
常规指针:0x00007FFFFFFFFFF8 (64位地址)
└────────────────┘
地址
PAC指针: 0xA23F7FFFFFFFFFF8
└────┘└──────────┘
│ │
│ └─ 原始地址
└─────────── 加密签名 (PAC)
内核的函数指针使用密钥签名。攻击者伪造的指针将没有有效的签名。当内核尝试在使用前验证伪造指针时,检查将失败。
实际上,PAC阻止了伪造控制流指针的尝试。这与NULL解引用漏洞高度相关,NULL解引用漏洞历史上依赖于使内核跳转到攻击者的伪造函数指针。
删除旧漏洞利用向量
如前所述,macOS不再支持32位可执行文件(自macOS 10.15 Catalina以来),从而关闭了禁用__PAGEZERO
区域的漏洞。此外,System Integrity Protection (SIP)和Kernel Integrity机制甚至可以阻止root用户修改某些内存或加载未签名的内核代码,从而间接使设置NULL页面攻击变得更加困难。
攻击者不能简单地禁用这些保护措施,而无需先拥有内核权限。
改进的内核内存管理和检查
Apple对XNU内核的指针处理进行了重大改进。许多内核接口现在对指针执行更严格的验证。在2015年至2016年之间发生高调错误之后,Apple审核了驱动程序以引入NULL检查,并确保仅在释放对象后安全时才将指针设置为NULL。在检查各种驱动程序的外部方法的反编译代码时,发现其中许多方法的名称都带有前缀,这可能意味着它们是“经过清理”或“安全”的。这些函数充当原始函数的包装器。
例如:IOMobileFramebufferUserClient::s_set_matrix 包装 IOMobileFramebufferUserClient::set_matrix
尽管如此,即使在审核之后,这些错误仍然存在,但是它们仍然危险吗?
Apple Silicon上的NULL指针解引用是否仍然可利用?
在Apple Silicon上的现代macOS中,内核中的NULL指针解引用几乎可以肯定无法用于代码执行。它将导致内核panic(拒绝服务),但正如我们所看到的,多层防御阻止了利用它进行权限提升。
总结为什么在Apple Silicon macOS上有效阻止了利用:
- 无法控制NULL处的内存: 与十年前不同,攻击者无法在相关地址空间中映射或将受控数据放置在地址0处。操作系统和硬件确保NULL页面未映射且用户程序无法访问。
- 内核不会执行用户内存: 即使内核以某种方式跳转到地址0(或任何用户地址),它也会遇到PXN强制执行并拒绝在那里执行代码,从而导致错误。
- 内核不会读取用户内存: 内核无法从地址0(或任何用户地址)读取内存。它会遇到PAN强制执行并拒绝从那里加载代码,从而导致错误。
- 指针真实性检查: 指针认证会阻止伪造指针来欺骗内核(例如,作为payload的伪造函数指针)。攻击者将需要绕过PAC,这并非易事,并且通常需要单独的侧信道或信息泄露攻击(例如,PACMAN漏洞)。内核将检测到无效指针并安全崩溃而无需绕过PAC。
这些缓解措施使得漏洞利用链难以完成,但是让我们“理论化”它可能看起来的样子。
理论利用链
至少,我们需要五个单独的漏洞:
- 页表修改: 允许修改内核页表以映射具有可写权限的NULL页面的漏洞。仅此一项就需要专门针对页表条目的复杂内核内存损坏原语。我从未见过。
- PAC绕过: 这需要以下任一条件:
- 泄露密钥PAC密钥(因为它们存储在系统寄存器中,因此很复杂)
- 找到可以生成有效签名的oracle
- 利用侧信道攻击,如PACMAN
- 在内核实现中找到PAC绕过漏洞
- 写入原语:用于将payload放置在地址0处的漏洞。
- PXN绕过: 用于操作页表属性以将NULL页面标记为内核可访问和可执行的漏洞(理论上,用于重新映射的第一个原语可以做到这一点)。
- NULL指针解引用:最后,我们的错误是解引用NULL并执行我们的指令。
这种复杂性使得NULL指针解引用主要仅用作现代Apple Silicon系统中的拒绝服务漏洞。
Apple是否仍然对其进行修补?
是的,Apple仍然在修复此类问题并接受此类报告,但并非总是将其归类为安全问题。我不知道他们为什么这样做,有时不这样做,但我将在下一篇文章中进一步讨论。我们可以在最近的macOS安全更新中看到,Apple将NULL指针解引用修复归类为拒绝服务修复,而不是“任意代码执行”问题:
自macOS Big Sur以来,没有关于利用NULL解引用进行升级的公开报告,这与以下预期相符:这些漏洞不再是Apple Silicon上可行的攻击媒介。
Apple的整体安全架构已经结束了此漏洞利用历史的一章。
可利用性清单 – 报告NULL指针解引用之前
并非每个NULL指针解引用都是相同的——有时可以利用该错误的根本原因将指针重定向到攻击者控制的内核内存。在报告之前,请验证以下内容:
- 检查指针是否来自堆分配,以及其来源是否可以操纵
- 测试是否可以喷射或整理堆以影响所讨论的内存位置
- 确定指针是否是使用可能由用户控制的索引或偏移量计算的
- 查找释放后使用条件,其中可以重新分配释放的内存并带有受控数据
- 评估缺失或不足的空值检查是否允许意外的指针值
- 探索可能绕过安全检查并导致指针损坏的替代控制流
简而言之,验证任何用户控制的输入是否会影响解引用中使用的指针值。勾选上面列表中的每个点可确保NULL指针解引用确实为NULL。
最后的话
尽管NULL指针解引用不再是漏洞利用开发的可行选择,但我希望您喜欢关于创建针对其的缓解措施的历史以及为什么今天这是一个不可利用的问题。
如果您觉得这篇博文有趣并想了解更多关于网络安全的知识,我鼓励您定期访问我们的AFINE博客以获取新的见解。如果您对macOS特别感兴趣,请收藏Snake_Apple存储库,您将在其中找到我所有关于macOS的文章。
我希望您在这里学到了一些新东西!
Karol Mazurek 研究负责人