Kernel-hack-drill和利用Linux内核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信号之间,导致use-after-free(UAF)。非特权用户可以在没有用户命名空间的情况下触发此漏洞,这使其更加危险。
内核使用已释放的virtio_vsock_sock对象。其大小为80字节,适合kmalloc-96 slab缓存。内存破坏是由内核工作线程执行的UAF写入。
使用"不朽信号"重现漏洞
首先,攻击者应创建一个监听虚拟套接字(服务器vsock):
|
|
然后攻击者应尝试从客户端vsock打开连接:
|
|
为了触发漏洞,攻击者应该用POSIX信号中断这个connect()系统调用。我的模糊测试器发现了一个更干净的技巧:
|
|
关于内存破坏
当信号在易受攻击的套接字处于TCP_ESTABLISHED状态时中断connect()系统调用时,竞争条件成功。然后套接字进入TCP_CLOSING状态:
|
|
第二次尝试使用不同的svm_cid(VMADDR_CID_HYPERVISOR)将易受攻击的vsock连接到服务器vsock会引发内存破坏:
|
|
CVE-2024-50264的限制
这个漏洞在利用方面有很多讨厌的限制:
- 易受攻击的virtio_vsock_sock客户端对象与服务器对象从相同的slab缓存分配,这会干扰跨缓存攻击
- 重现这个竞争条件非常不稳定
- UAF写入在kfree()后几微秒内在kworker中发生,对于典型的跨缓存攻击来说太快了
- kworker中的null-ptr-deref跟随UAF写入
- 即使在避免内核oops后,另一个null-ptr-deref在VSOCK_CLOSE_TIMEOUT(八秒)后在kworker中发生
- 如果virtio_vsock_sock.tx_lock不为零,kworker会在spin_lock_bh()中挂起
内核黑客训练工具(Kernel Hack Drill)
我在2017年创建了一个名为kernel-hack-drill的宠物项目,为我的学生提供学习和实验Linux内核漏洞利用的测试环境。kernel-hack-drill是一个在GPL-3.0许可证下发布的开源项目,包含以下部分:
drill_mod.c
:一个小的Linux内核模块,提供/proc/drill_act
文件作为与用户空间的简单接口drill.h
:描述drill_mod.ko接口的头文件drill_test.c
:drill_mod.ko的用户空间测试
使用kernel-hack-drill实验跨缓存攻击
我的第一步是学习跨缓存攻击在启用了slab分配器加固的最新Ubuntu内核上的行为。
跨缓存攻击算法:
- 分配objs_per_slab对象以创建新的活动slab
- 分配objs_per_slab * cpu_partial对象
- 创建包含UAF对象的slab
- 再次创建新的活动slab
- 完全释放包含UAF对象的slab
- 填充部分列表
- 为另一个slab缓存回收包含UAF对象的页面
- 利用UAF!
将跨缓存攻击适配到CVE-2024-50264
正如限制中所述,易受攻击的virtio_vsock_sock客户端对象与服务器对象一起分配(限制#1)。这对利用有两个不利影响:
- 一方面,保持服务器vsock打开会阻止释放包含UAF对象的slab,这会杀死跨缓存攻击
- 另一方面,关闭服务器vsock会干扰UAF本身
我使用了另一个竞争条件来利用主要的竞争条件 - 它起作用了:
- 我在10000 ns超时后向易受攻击的connect()系统调用发送"不朽"信号33,远早于触发UAF所需的延迟
- 然后我验证了早期竞争条件
实验Dirty Pipe攻击
首先,我在kernel-hack-drill中构建了一个Dirty Pipe原型;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
- 实现Dirty Pipe攻击,一次命中即可获得LPE,无需信息泄露
AARW和KASLR的最后报复
在pipe_buffer中,page指针保存虚拟内存映射(vmemmap)中struct page的地址。当我成功将受控数据的UAF写入pipe_buffer.page指针时,我通过管道获得了任意地址读写(AARW)能力。
我决定使用管道AARW攻击内核堆中的struct cred。如我之前所述,我使用msg_msg越界读取泄漏了cred的虚拟地址。这个虚拟地址看起来像0xffff888003b7ad00,我理解它来自所有物理内存的直接映射。
结论
漏洞碰撞是痛苦的。无论如何完成研究都是有回报的。研究这个具有多个限制的困难竞争条件使我能够发现新的利用技术,并使用和改进我的宠物项目kernel-hack-drill,它为Linux内核安全研究人员提供了一个测试环境。欢迎你尝试并贡献。