内核黑客训练场与利用CVE-2024-50264的新方法
漏洞碰撞故事
我早在2021年就在AF_VSOCK中发现了一个漏洞,并发表了文章《四字节的力量:利用Linux内核中的CVE-2021-26708》。2024年4月,我使用定制的syzkaller对这个内核子系统进行模糊测试,在AF_VSOCK中发现了另一个崩溃。我最小化了崩溃重现器并禁用了KASAN,这导致内核工作线程(kworker)中立即出现null-ptr-deref,看起来与安全无关。正确修补它需要对AF_VSOCK子系统进行重大重构。由于确信前进的道路会很痛苦,我将这个崩溃搁置了。这是一个错误的决定。
后来,在2024年秋季,我决定再次研究这个漏洞,并取得了有希望的结果。然后,在一个平静的夜晚,我意识到我与Hyunwoo Kim(@v4bel)和Wongi Lee(@qwerty)发生了漏洞碰撞:他们已经将这个漏洞披露为CVE-2024-50264,并在kernelCTF中使用过。他们的补丁将我的PoC利用变成了null-ptr-deref。
CVE-2024-50264分析
CVE-2024-50264漏洞于2016年8月由Linux v4.8中的commit 06a8fc78367d引入。这是AF_VSOCK套接字中的一个竞争条件,发生在connect()系统调用和POSIX信号之间,导致释放后使用(UAF)。非特权用户可以在没有用户命名空间的情况下触发此漏洞,这使其更加危险。
内核使用已释放的virtio_vsock_sock对象,其大小为80字节,适合kmalloc-96 slab缓存。内存损坏是由内核工作线程执行的UAF写入。
使用"不朽信号"重现漏洞
首先,攻击者应创建一个监听虚拟套接字(服务器vsock):
|
|
然后攻击者应尝试从客户端vsock打开连接:
|
|
为了触发漏洞,攻击者应使用POSIX信号中断此connect()系统调用。@v4bel和@qwerty使用了SIGKILL,但这会杀死利用进程。我的模糊测试器偶然发现了一个更干净的技巧:
|
|
我的模糊测试器发现计时器可以触发信号33并中断connect()。信号33很特殊,原生POSIX线程库(NPTL)将其保留用于内部工作,操作系统会悄悄保护应用程序免受其影响。
关于内存损坏
当信号在易受攻击的套接字处于TCP_ESTABLISHED状态时中断connect()系统调用时,竞争条件成功。然后套接字进入TCP_CLOSING状态:
|
|
第二次尝试使用不同的svm_cid(VMADDR_CID_HYPERVISOR)将易受攻击的vsock连接到服务器vsock会引发内存损坏:
|
|
在底层,connect()系统调用执行vsock_assign_transport()。此函数将虚拟套接字切换到新的svm_cid传输,并释放与先前vsock传输绑定的资源:
|
|
此过程在virtio_transport_close()中关闭旧的vsock传输,并在virtio_transport_destruct()中释放virtio_vsock_sock对象。然而,由于套接字的错误TCP_CLOSING状态,virtio_transport_close()启动了进一步的通信。为了处理该活动,内核调度了一个kworker,最终调用virtio_transport_space_update(),该函数在已释放的结构上操作:
|
|
CVE-2024-50264的限制
正如我之前提到的,这个漏洞在利用方面有很多讨厌的限制:
- 易受攻击的virtio_vsock_sock客户端对象与服务器对象从同一slab缓存分配,这干扰了跨缓存攻击
- 重现此竞争条件非常不稳定
- UAF写入在kfree()后几微秒内在kworker中发生,对于典型的跨缓存攻击来说太快了
- UAF写入后kworker中会出现null-ptr-deref
- 即使在避免内核oops的情况下,另一个null-ptr-deref也会在VSOCK_CLOSE_TIMEOUT(八秒)后在kworker中发生
- 如果virtio_vsock_sock.tx_lock不为零,kworker会在spin_lock_bh()中挂起
内核黑客训练场
早在2017年,我为学生创建了一个名为kernel-hack-drill的宠物项目。它为学习和实验Linux内核漏洞利用提供了测试环境。我想起了它,并决定使用kernel-hack-drill为CVE-2024-50264开发漏洞利用原语。
kernel-hack-drill是一个在GPL-3.0许可证下发布的开源项目,包含以下部分:
- drill_mod.c是一个小型Linux内核模块,提供/proc/drill_act文件作为与用户空间的简单接口。此模块包含您可以控制和实验的漏洞
- drill.h是描述drill_mod.ko接口的头文件
- drill_test.c是drill_mod.ko的用户空间测试,提供使用/proc/drill_act的示例
使用内核黑客训练场实验跨缓存攻击
我的第一步是了解跨缓存攻击在启用slab分配器加固的最新Ubuntu内核上的行为。
我在drill_uaf_w_msg_msg.c中实现了标准的跨缓存攻击。要观察其运行并调试它,请在内核源中进行以下调整:
|
|
将跨缓存攻击适配到CVE-2024-50264
正如限制中所述,易受攻击的virtio_vsock_sock客户端对象与服务器对象一起分配(限制#1)。这对利用有两个不利影响:
一方面,保持服务器vsock打开会阻止释放包含UAF对象的slab,这会杀死跨缓存攻击。 另一方面,关闭服务器vsock会干扰UAF本身。
@v4bel和@qwerty使用SLUBStick时序侧信道来发现分配器何时切换到新的活动slab。我走了另一条路:如果我在connect()系统调用后几乎立即用信号击中它会怎样?
简而言之,我使用了另一个竞争条件来利用主要的竞争条件 - 它奏效了:
我在10000 ns超时后向易受攻击的connect()系统调用发送"不朽"信号33,远早于触发UAF所需的延迟。 然后我验证了早期竞争条件:
- connect()系统调用必须返回"Interrupted system call"
- 另一个测试客户端vsock应该仍然可以毫无问题地连接到服务器vsock
我发现当两个检查都通过时,只创建了客户端vsock的单个易受攻击的virtio_vsock_sock。中断信号在内核能够为服务器vsock创建第二个virtio_vsock_sock之前到达。这绕过了限制#1(配对对象创建)。之后,我再次发送信号33 - 这次是在正常超时之后 - 第二次中断易受攻击的connect()并引发UAF。针对virtio_vsock_sock的跨缓存攻击被解锁了!
管道缓冲区娱乐
当使用pipe()系统调用创建管道时,内核分配一个pipe_buffer结构数组。此数组中的每个pipe_buffer项对应一个内存页面,用于保存写入管道的数据。
这个对象看起来是一个很好的UAF目标。我可以通过更改管道的容量来使pipe_buffer数组与virtio_vsock_sock大小相同:fcntl(pipe_fd[1], F_SETPIPE_SZ, PAGE_SIZE * 2)。内核将数组大小更改为2 * sizeof(struct pipe_buffer) = 80字节,与virtio_vsock_sock大小完全匹配。
此外,vsock UAF写入在偏移量24处的4个攻击者控制字节可以翻转pipe_buffer.flags,就像在Max Kellermann的原始Dirty Pipe攻击中一样。
AARW和KASLR的最后报复
在pipe_buffer中,页面指针保存虚拟内存映射(vmemmap)内的struct page地址。vmemmap是这些结构的数组,允许内核高效地寻址物理内存。
当我成功将受控数据的UAF写入pipe_buffer.page指针时,我通过管道获得了任意地址读写(AARW)。但是,正如我在限制#5中提到的,我无法多次更改AARW目标地址,因此我必须在vmemmap中仔细选择目标。
我决定使用管道AARW攻击内核堆中的struct cred。正如我之前描述的,我使用msg_msg越界读取泄漏了cred的虚拟地址。这个虚拟地址看起来像0xffff888003b7ad00,我理解它来自所有物理内存的直接映射。因此,我使用以下公式计算vmemmap中相应struct page的偏移量:
|
|
背后的想法很简单:
- addr & 0x3ffffffflu给出struct cred从page_offset_base的偏移量
- 右移12给出包含struct cred的内存页数
- 最后,乘以64(struct page的大小)给出vmemmap中相应struct page的偏移量
结论
漏洞碰撞是痛苦的。无论如何完成研究都是有回报的。研究这个具有多个限制的困难竞争条件使我能够发现新的利用技术,并使用和改进我的宠物项目kernel-hack-drill,它为Linux内核安全研究人员提供了一个测试环境。欢迎您尝试并做出贡献。