内核漏洞利用新突破:利用CVE-2024-50264实现Linux权限提升

本文深入分析了Linux内核中的高危漏洞CVE-2024-50264,这是一个涉及AF_VSOCK的竞争条件漏洞,可导致权限提升。作者介绍了创新的利用技术,包括使用"不朽信号"、跨缓存攻击和管道缓冲区操作,并展示了如何通过内核黑客训练项目kernel-hack-drill来开发稳定的利用原语。

内核黑客训练与利用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):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int ret = -1;
int vsock1 = 0;

vsock1 = socket(AF_VSOCK, SOCK_STREAM, 0);
if (vsock1 < 0)
    err_exit("[-] creating vsock");

ret = bind(vsock1, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));
if (ret != 0)
    err_exit("[-] binding vsock");

ret = listen(vsock1, 0); /* backlog = 0 */
if (ret != 0)
    err_exit("[-] listening vsock");

然后攻击者应尝试从客户端vsock打开连接:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#define UAF_PORT 0x2712

int vsock2 = 0;
struct sockaddr_vm addr = {
    .svm_family = AF_VSOCK,
    .svm_port = UAF_PORT,
    .svm_cid = VMADDR_CID_LOCAL
};

vsock2 = socket(AF_VSOCK, SOCK_STREAM, 0);
if (vsock2 < 0)
    err_exit("[-] creating vsock");

ret = connect(vsock2, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

为了触发漏洞,攻击者应该用POSIX信号中断这个connect()系统调用。@v4bel和@qwerty使用了SIGKILL,但这会杀死利用进程。我的模糊测试器偶然发现了一个更干净的技巧,让我感到惊讶:

1
2
3
4
5
6
struct sigevent sev = {};
timer_t race_timer = 0;

sev.sigev_notify = SIGEV_SIGNAL;
sev.sigev_signo = 33;
ret = timer_create(CLOCK_MONOTONIC, &sev, &race_timer);

关于内存破坏

当信号在易受攻击的套接字处于TCP_ESTABLISHED状态时中断connect()系统调用时,竞争条件成功。然后套接字进入TCP_CLOSING状态:

1
2
3
4
5
6
7
8
if (signal_pending(current)) {
    err = sock_intr_errno(timeout);
    sk->sk_state = sk->sk_state == TCP_ESTABLISHED ? TCP_CLOSING : TCP_CLOSE;
    sock->state = SS_UNCONNECTED;
    vsock_transport_cancel_pkt(vsk);
    vsock_remove_connected(vsk);
    goto out_wait;
}

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中实现了标准的跨缓存攻击。您可以在存储库中查看完整代码,我在这里概述流程。

跨缓存攻击算法:

  1. 分配objs_per_slab对象以创建新的活动slab。
  2. 分配objs_per_slab * cpu_partial对象。这将创建cpu_partial数量的完整slab,稍后将在步骤6中填充部分列表。
  3. 创建包含UAF对象的slab。分配objs_per_slab对象,并保持对该slab中易受攻击对象的悬空引用。
  4. 再次创建新的活动slab:分配objs_per_slab对象。此步骤对于保持跨缓存攻击的稳定性非常重要。
  5. 完全释放包含UAF对象的slab。
  6. 填充部分列表:释放步骤2中保留的每个slab中的一个objs_per_slab对象。
  7. 为另一个slab缓存回收包含UAF对象的页面:喷洒目标msg_msg对象。
  8. 利用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内核安全研究人员提供了测试环境。欢迎您尝试并贡献。

感谢阅读!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计