内核黑客训练与利用CVE-2024-50264的新方法
某些内存破坏漏洞比其他漏洞更难利用。它们可能涉及竞争条件、导致系统崩溃,并带来各种限制,让研究人员的工作变得困难。处理这类脆弱漏洞需要大量时间和精力。Linux内核中的CVE-2024-50264就是这样一个难以利用的漏洞,它获得了2025年Pwnie奖的最佳权限提升漏洞。
漏洞碰撞故事
我早在2021年就在AF_VSOCK中发现了一个漏洞,并发表了文章《四字节的力量:利用Linux内核中的CVE-2021-26708》。2024年4月,我使用定制的syzkaller对这个内核子系统进行模糊测试,在AF_VSOCK中发现了另一个崩溃。我最小化了崩溃重现器并禁用了KASAN,结果导致内核工作线程(kworker)中立即出现空指针解引用,这看起来与安全无关。正确修补它需要对AF_VSOCK子系统进行重大重构。确信前进的道路会很痛苦,我将这个崩溃搁置了。这是一个错误的决定。
后来,在2024年秋季,我决定再次研究这个漏洞,并获得了有希望的结果。然后,在一个平静的夜晚,我意识到我与Hyunwoo Kim(@v4bel)和Wongi Lee(@qwerty)发生了碰撞:他们已经将这个漏洞披露为CVE-2024-50264,并在kernelCTF中使用过。他们的补丁将我的PoC利用变成了空指针解引用。
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,但这会杀死利用进程。我的模糊测试器偶然发现了一个更干净的技巧,让我感到惊讶:
|
|
关于内存破坏
当信号在易受攻击的套接字处于TCP_ESTABLISHED状态时中断connect()系统调用时,竞争条件成功。然后套接字进入TCP_CLOSING状态:
|
|
CVE-2024-50264的限制
正如我之前提到的,这个漏洞在利用方面有很多讨厌的限制:
- 易受攻击的virtio_vsock_sock客户端对象与服务器对象从同一个slab缓存中分配。这干扰了跨缓存攻击。
- 重现这种竞争条件非常不稳定。
- UAF写入在kfree()后的几微秒内发生在kworker中,对于典型的跨缓存攻击来说太快了。
- UAF写入后kworker中出现空指针解引用。这就是我最初搁置这个漏洞的原因。
- 即使避免了内核异常,kworker在VSOCK_CLOSE_TIMEOUT(八秒)后也会发生另一个空指针解引用。
- 如果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中实现了标准的跨缓存攻击。您可以在存储库中查看完整代码,我在这里概述流程。
跨缓存攻击算法:
- 分配objs_per_slab对象以创建新的活动slab。
- 分配objs_per_slab * cpu_partial对象。这将创建cpu_partial数量的完整slab,稍后将在步骤6中填充部分列表。
- 创建包含UAF对象的slab。分配objs_per_slab对象,并保持对该slab中易受攻击对象的悬空引用。
- 再次创建新的活动slab:分配objs_per_slab对象。此步骤对于保持跨缓存攻击的稳定性非常重要。
- 完全释放包含UAF对象的slab。
- 填充部分列表:释放步骤2中保留的每个slab中的一个objs_per_slab对象。
- 为另一个slab缓存回收包含UAF对象的页面:喷洒目标msg_msg对象。
- 利用UAF!覆盖msg_msg.m_ts以越界读取内核内存。
调整跨缓存攻击以适应CVE-2024-50264
正如限制中所述,易受攻击的virtio_vsock_sock客户端对象与服务器对象一起分配(限制#1)。这从两个方面损害了利用:
- 一方面,保持服务器vsock开放会阻止释放包含UAF对象的slab,这会杀死跨缓存攻击。
- 另一方面,关闭服务器vsock会干扰UAF本身。
循环此早期竞争并检查其结果是快速的
一旦早期竞争成功,触发UAF的主要竞争变得更加稳定;我现在可以每秒触发一次UAF,而不是每几分钟一次,解决了限制#2中提到的稳定性问题。我的竞争条件"速通"也缓解了限制#5:我设法在kworker在VSOCK_CLOSE_TIMEOUT(8秒)后遇到空指针解引用之前进行了大约五次UAF写入。
哦,太慢了!跨缓存攻击姗姗来迟
正如限制#3所述,kworker中的UAF写入仅在virtio_vsock_sock的kfree()后几微秒触发。跨缓存攻击需要更多时间,因此UAF写入会落在原始的virtio_vsock_sock上,永远不会到达msg_msg。
我不知道如何使跨缓存过程更快,但我知道如何减慢受攻击的内核代码。这种方法在Jann Horn的文章《与时钟赛跑》中有所描述。它使我的kworker比缓慢的跨缓存攻击更慢。
整理战利品
损坏的msg_msg允许我从内核空间读取8 KiB的数据。我整理了这个战利品,并发现了一个有希望的信息泄漏:内核地址0xffffffff8233cfa0 [1]。这个信息泄漏非常稳定,并且以高概率工作,因此我决定在不进行任何额外堆风水的情况下调查它。
寻找任意地址写入原语
研究中最有趣和最困难的部分从这里开始。我正在寻找UAF写入的目标内核对象,它可以提供任意地址写入利用原语。搜索是 exhausting 的。我做了以下工作:
- 查阅了数十个内核对象,
- 阅读了许多内核利用文章,
- 尝试了Eduardo Vela和KernelCTF团队的内核利用仪表板。
实验脏管道攻击
首先,我在kernel-hack-drill中构建了一个脏管道原型;PoC利用drill_uaf_w_pipe_buffer.c在存储库中。它:
- 执行跨缓存攻击,并将包含drill_item_t对象的slab回收为pipe_buffer对象的slab
- 利用对drill_item_t的UAF写入;写入drill_item_t偏移量24的攻击者控制字节修改pipe_buffer.flags
- 实现脏管道攻击,在一次射击中实现LPE,无需信息泄漏,很酷!
管道缓冲区娱乐
所以原始的脏管道技术不适合我的CVE-2024-50264 PoC利用。但突然一个想法击中了我:
如果我创建一个容量为PAGE_SIZE * 4的管道,强制内核在kmalloc-192中分配四个pipe_buffer对象会怎样?
在这种情况下,内存对象重叠看起来像这样:一个kmalloc-192 slab中的四个pipe_buffer对象被分配在两个kmalloc-96 slab中的两个virtio_vsock_sock对象的位置。
AARW和KASLR的最后复仇
在pipe_buffer中,页面指针保存虚拟内存映射(vmemmap)内struct page的地址。vmemmap是这些结构的数组,允许内核高效地寻址物理内存。
因此,当我成功将受控数据的UAF写入到pipe_buffer.page指针时,我通过管道获得了任意地址读取和写入(AARW)。然而,我无法多次更改AARW目标地址,如限制#5所述,因此我必须仔细选择vmemmap中的目标。
结论
漏洞碰撞是痛苦的。无论如何完成研究是有回报的。让我引用我的好朋友的话:
研究这个具有多个限制的困难竞争条件让我发现了新的利用技术,并使用和改进了我宠物项目kernel-hack-drill,它为Linux内核安全研究人员提供了测试环境。欢迎您尝试并贡献。
感谢阅读!