Linux内核漏洞CVE-2024-50264利用新方法及内核黑客训练工具

本文深入分析了Linux内核中的竞争条件漏洞CVE-2024-50264,介绍了利用该漏洞的新方法,包括使用"不朽信号"、跨缓存攻击和内核黑客训练工具kernel-hack-drill,最终实现了权限提升。

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):

 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()系统调用。我的模糊测试器发现了一个更干净的技巧:

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;
}

第二次尝试使用不同的svm_cid(VMADDR_CID_HYPERVISOR)将易受攻击的vsock连接到服务器vsock会引发内存破坏:

1
2
3
4
5
6
7
8
struct sockaddr_vm addr = {
    .svm_family = AF_VSOCK,
    .svm_port = UAF_PORT,
    .svm_cid = VMADDR_CID_HYPERVISOR
};

/* this connect will schedule the kernel worker performing UAF */
ret = connect(vsock2, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

CVE-2024-50264的限制

这个漏洞在利用方面有很多讨厌的限制:

  1. 易受攻击的virtio_vsock_sock客户端对象与服务器对象从相同的slab缓存分配,这会干扰跨缓存攻击
  2. 重现这个竞争条件非常不稳定
  3. UAF写入在kfree()后几微秒内在kworker中发生,对于典型的跨缓存攻击来说太快了
  4. kworker中的null-ptr-deref跟随UAF写入
  5. 即使在避免内核oops后,另一个null-ptr-deref在VSOCK_CLOSE_TIMEOUT(八秒)后在kworker中发生
  6. 如果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内核上的行为。

跨缓存攻击算法:

  1. 分配objs_per_slab对象以创建新的活动slab
  2. 分配objs_per_slab * cpu_partial对象
  3. 创建包含UAF对象的slab
  4. 再次创建新的活动slab
  5. 完全释放包含UAF对象的slab
  6. 填充部分列表
  7. 为另一个slab缓存回收包含UAF对象的页面
  8. 利用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内核安全研究人员提供了一个测试环境。欢迎你尝试并贡献。

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