四字节的力量:深入剖析Linux内核CVE-2021-26708漏洞利用

本文详细分析了Linux内核虚拟套接字实现中的五个竞争条件漏洞CVE-2021-26708,展示了如何利用这些漏洞在Fedora 33 Server上实现本地权限提升,绕过SMEP和SMAP保护机制。

四字节的力量:利用Linux内核中的CVE-2021-26708

Alexander Popov

2021年2月9日

CVE-2021-26708被分配给Linux内核虚拟套接字实现中的五个竞争条件漏洞。我在2021年1月发现并修复了这些漏洞。在本文中,我将描述如何在x86_64架构的Fedora 33 Server上利用这些漏洞实现本地权限提升,并绕过SMEP和SMAP保护。

今天我在Zer0Con 2021上就此主题进行了演讲(幻灯片)。

我喜欢这个漏洞利用。竞争条件可以被用来进行非常有限的内存破坏,我逐渐将其转化为对内核内存的任意读写,最终完全控制系统。这就是我将本文标题为"四字节的力量"的原因。

现在来看PoC演示视频:

漏洞

这些漏洞是由net/vmw_vsock/af_vsock.c中的错误锁定引起的竞争条件。这些竞争条件在2019年11月添加VSOCK多传输支持的提交中被隐式引入。这些提交被合并到Linux内核版本5.5-rc1中。

在所有主要的GNU/Linux发行版中,CONFIG_VSOCKETS和CONFIG_VIRTIO_VSOCKETS作为内核模块提供。当你为AF_VSOCK域创建套接字时,易受攻击的模块会自动加载:

1
vsock = socket(AF_VSOCK, SOCK_STREAM, 0);

AF_VSOCK套接字创建对非特权用户可用,不需要用户命名空间。很巧妙,对吧?

漏洞和修复

我使用经过自定义修改的syzkaller模糊测试器。在1月11日,我看到它在virtio_transport_notify_buffer_size()中得到了一个可疑的内核崩溃。然而,模糊测试器无法重现这个崩溃,所以我开始手动检查源代码并开发重现器。

几天后,我在vsock_stream_setsockopt()中发现了一个令人困惑的漏洞,看起来是故意的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct sock *sk;
struct vsock_sock *vsk;
const struct vsock_transport *transport;

/* ... */

sk = sock->sk;
vsk = vsock_sk(sk);
transport = vsk->transport;

lock_sock(sk);

这很奇怪。虚拟套接字传输的指针在lock_sock()调用之前被复制到局部变量中。但是当套接字锁未被获取时,vsk->transport值可能会改变!这是一个明显的竞争条件漏洞。我检查了整个af_vsock.c文件,发现了另外四个类似的问题。

搜索git历史有助于理解原因。最初,虚拟套接字的传输不能更改,因此将vsk->transport的值复制到局部变量是安全的。后来,这些漏洞被提交c0cfa2d8a788fcf4(vsock:添加多传输支持)和提交6a2c0962105ae8ce(vsock:防止传输模块卸载)隐式引入。

修复这个漏洞很简单:

1
2
3
4
5
6
7
sk = sock->sk;
vsk = vsock_sk(sk);
-	transport = vsk->transport;
 
lock_sock(sk);
 
+	transport = vsk->transport;

有点奇怪的漏洞披露

在1月30日完成PoC漏洞利用后,我创建了修复补丁,并向security@kernel.org进行了负责任的披露。我得到了Linus和Greg非常迅速的回复,我们确定了以下程序:

  1. 将我的补丁公开发送到Linux内核邮件列表(LKML)
  2. 将其合并到上游并回溯到受影响的稳定树
  3. 通过linux-distros邮件列表通知发行版此问题的安全相关性
  4. 当发行版允许时,通过oss-security@lists.openwall.com进行披露

第一步是有问题的。Linus决定立即合并我的补丁,没有任何披露禁运,因为补丁"看起来与我们每天做的补丁没有什么不同"。我遵守了,并提议将其公开发送到LKML。这样做很重要,因为任何人都可以通过过滤未出现在邮件列表上的内核提交来找到内核漏洞修复。

在2月2日,我的补丁的第二版本被合并到netdev/net.git,然后进入Linus的树。在2月4日,Greg将其应用到受影响的稳定树。然后我立即通知linux-distros@vs.openwall.org,修复的漏洞是可利用的,并询问Linux发行版在我进行公开披露之前需要多少时间。

但我得到了以下回复:

1
2
3
如果补丁已提交到上游,那么问题就是公开的。

请立即发送到oss-security。

有点奇怪。无论如何,我随后在https://cve.mitre.org/cve/request_id.html请求了一个CVE ID,并在oss-security@lists.openwall.com上发布了公告。

这提出了一个问题:这种"尽快合并"程序是否与linux-distros邮件列表兼容?

作为一个反例,当我向security@kernel.org报告CVE-2017-2636时,Kees Cook和Greg通过linux-distros邮件组织了一周的披露禁运。这使得Linux发行版能够不慌不忙地将我的修复集成到他们的安全更新中,并同时发布。

内存破坏

现在让我们专注于利用CVE-2021-26708。我利用了vsock_stream_setsockopt()中的竞争条件。重现它需要两个线程。第一个调用setsockopt():

1
2
setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
			&size, sizeof(unsigned long));

第二个线程应该在vsock_stream_setsockopt()尝试获取套接字锁时更改虚拟套接字传输。这是通过重新连接到虚拟套接字来执行的:

1
2
3
4
5
6
7
8
9
struct sockaddr_vm addr = {
    .svm_family = AF_VSOCK,
};

addr.svm_cid = VMADDR_CID_LOCAL;
connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

addr.svm_cid = VMADDR_CID_HYPERVISOR;
connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));

为了处理虚拟套接字的connect(),内核执行vsock_stream_connect(),它调用vsock_assign_transport()。这个函数有一些我们感兴趣的代码:

 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()必须与套接字锁一起调用。
     * 此路径只能在vsock_stream_connect()期间采取,
     * 在那里我们已经持有套接字锁。
     * 在其他情况下,此函数在一个新的套接字上调用
     * 该套接字未分配给任何传输。
     */
    vsk->transport->release(vsk);
    vsock_deassign_transport(vsk);
}

注意vsock_stream_connect()持有套接字锁。同时,vsock_stream_setsockopt()在一个并行线程中尝试获取它。很好。这就是我们触发竞争条件所需要的。

因此,在具有不同svm_cid的第二个connect()上,调用vsock_deassign_transport()函数。该函数执行传输析构函数virtio_transport_destruct(),从而释放vsock_sock.trans。此时,您可能猜到use-after-free是这一切的走向:) vsk->transport被设置为NULL。

当vsock_stream_connect()释放套接字锁时,vsock_stream_setsockopt()可以继续执行。它调用vsock_update_buffer_size(),随后调用transport->notify_buffer_size()。这里transport具有来自局部变量的过时值,与vsk->transport(为NULL)不匹配。

内核执行virtio_transport_notify_buffer_size(),破坏内核内存:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void virtio_transport_notify_buffer_size(struct vsock_sock *vsk, u64 *val)
{
    struct virtio_vsock_sock *vvs = vsk->trans;

    if (*val > VIRTIO_VSOCK_MAX_BUF_SIZE)
        *val = VIRTIO_VSOCK_MAX_BUF_SIZE;

    vvs->buf_alloc = *val;

    virtio_transport_send_credit_update(vsk, VIRTIO_VSOCK_TYPE_STREAM, NULL);
}

这里vvs是指向已在virtio_transport_destruct()中释放的内核内存的指针。struct virtio_vsock_sock的大小是64字节;此对象位于kmalloc-64 slab缓存中。buf_alloc字段的类型为u32,位于偏移量40处。VIRTIO_VSOCK_MAX_BUF_SIZE是0xFFFFFFFFUL。值*val由攻击者控制,其四个最低有效字节被写入已释放的内存。

“模糊测试奇迹”

正如我提到的,syzkaller无法重现此崩溃,我不得不手动开发重现器。但为什么模糊测试器会失败?查看vsock_update_buffer_size()给出了答案:

1
2
3
4
5
if (val != vsk->buffer_size &&
  transport && transport->notify_buffer_size)
    transport->notify_buffer_size(vsk, &val);

vsk->buffer_size = val;

仅当val与当前buffer_size不同时,才调用notify_buffer_size()处理程序。换句话说,执行SO_VM_SOCKETS_BUFFER_SIZE的setsockopt()应每次使用不同的size参数调用。我使用了这个有趣的技巧来在我的第一个重现器中触发内存破坏(源代码):

1
2
3
4
5
6
7
struct timespec tp;
unsigned long size = 0;

clock_gettime(CLOCK_MONOTONIC, &tp);
size = tp.tv_nsec;
setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
			&size, sizeof(unsigned long));

这里,size值取自clock_gettime()返回的纳秒计数,并且很可能在每个竞争轮次中都不同。没有修改的上游syzkaller不会做这样的事情。syscall参数的值在syzkaller生成模糊测试输入时选择。当模糊测试器在目标上执行它时,它们不会改变。

无论如何,我仍然不完全理解syzkaller如何设法触发此崩溃¯_(ツ)_/¯看起来模糊测试器使用SO_VM_SOCKETS_BUFFER_MAX_SIZE和SO_VM_SOCKETS_BUFFER_MIN_SIZE做了一些幸运的多线程魔法,但随后无法重现它。

想法!也许添加在运行时随机化一些syscall参数的能力将使syzkaller能够发现更多像CVE-2021-26708这样的漏洞。另一方面,这样做也可能使崩溃重现不太稳定。

四字节的力量

这次我选择Fedora 33 Server作为利用目标,内核版本为5.10.11-200.fc33.x86_64。从一开始,我就决心绕过SMEP和SMAP。

总结一下,这种竞争条件可能导致将4字节受控值写后释放到偏移量40处的64字节内核对象。这是相当有限的内存破坏。我很难将其转化为真正的武器。我将根据其开发时间线描述漏洞利用。

这些照片来自俄罗斯国家冬宫博物馆收藏的文物。我喜欢这个美妙的博物馆!

作为第一步,我开始研究稳定的堆喷洒。漏洞利用应执行一些用户空间活动,使内核在释放的virtio_vsock_sock位置分配另一个64字节对象。这样,4字节写后释放应该破坏喷洒的对象(而不是未使用的空闲内核内存)。

我使用add_key syscall设置了一些快速的实验性喷洒。在第二次连接到虚拟套接字后,我立即调用了几次,而并行线程完成了易受攻击的vsock_stream_setsockopt()。使用ftrace跟踪内核分配器可以确认释放的virtio_vsock_sock被覆盖。换句话说,我看到成功的堆喷洒是可能的。

我利用策略的下一步是找到一个64字节的内核对象,当它在偏移量40处有四个损坏的字节时,可以提供更强的利用原语。嗯…不那么容易!

我的第一个想法是采用Maddie Stone和Jann Horn的Bad Binder漏洞利用中的iovec技术。其本质是使用精心破坏的iovec对象对内核内存进行任意读写。然而,这个想法我遇到了三重失败:

  1. 64字节iovec在内核堆栈上分配,而不是堆上。
  2. 偏移量40处的四个字节覆盖iovec.iov_len(而不是iovec.iov_base),因此原始方法无法工作。
  3. 自Linux内核版本4.13以来,这种iovec利用技巧已经失效。出色的Al Viro在2017年6月用提交09fc68dc66f7597b杀死了它:
1
我们*没有*最近进行access_ok();我们依赖于iovec数组在创建时通过了健全性检查,并且自那以后没有任何东西破坏它。然而,这是非常非本地的,所以我们最好重新检查。

在用一些其他适合堆喷洒的内核对象进行详尽实验后,我找到了msgsnd() syscall。它在内核空间创建struct msg_msg,参见pahole输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct msg_msg {
	struct list_head           m_list;               /*     0    16 */
	long int                   m_type;               /*    16     8 */
	size_t                     m_ts;                 /*    24     8 */
	struct msg_msgseg *        next;                 /*    32     8 */
	void *                     security;             /*    40     8 */

	/* size: 48, cachelines: 1, members: 5 */
	/* last cacheline: 48 bytes */
};

这是消息头,后面是消息数据。如果用户空间中的struct msgbuf有16字节的mtext,则相应的msg_msg在kmalloc-64 slab缓存中创建,就像struct virtio_vsock_sock一样。4字节写后释放可以破坏偏移量40处的void *security指针。使用security字段破坏Linux安全性:讽刺本身!

msg_msg.security字段指向由lsm_msg_msg_alloc()分配的内核数据,在Fedora的情况下由SELinux使用。当msg_msg被接收时,它由security_msg_msg_free()释放。因此,破坏安全指针的前半部分(在小端x86_64上的最低有效字节)提供了任意释放,这是一个更强的利用原语。

内核信息泄漏作为奖励

在实现任意释放后,我开始思考将其瞄准何处——我可以释放什么?在这里,我使用了与在CVE-2019-18683漏洞利用中相同的技巧。正如我之前提到的,第二次连接到虚拟套接字调用vsock_deassign_transport(),它将vsk->transport设置为NULL。这使得易受攻击的vsock_stream_setsockopt()在内存破坏后调用virtio_transport_send_pkt_info()时显示内核警告:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
WARNING: CPU: 1 PID: 6739 at net/vmw_vsock/virtio_transport_common.c:34
...
CPU: 1 PID: 6739 Comm: racer Tainted: G        W         5.10.11-200.fc33.x86_64 #1
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.13.0-2.fc32 04/01/2014
RIP: 0010:virtio_transport_send_pkt_info+0x14d/0x180 [vmw_vsock_virtio_transport_common]
...
RSP: 0018:ffffc90000d07e10 EFLAGS: 00010246
RAX: 0000000000000000 RBX: ffff888103416ac0 RCX: ffff88811e845b80
RDX: 00000000ffffffff RSI: ffffc90000d07e58 RDI: ffff888103416ac0
RBP: 0000000000000000 R08: 00000000052008af R09: 0000000000000000
R10: 0000000000000126 R11: 0000000000000000 R12: 0000000000000008
R13: ffffc90000d07e58 R14: 0000000000000000 R15: ffff888103416ac0
FS:  00007f2f123d5640(0000) GS:ffff88817bd00000(0000) knlGS:0000000000000000
CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 00007f81ffc2a000 CR3: 000000011db96004 CR4: 0000000000370ee0
Call Trace:
  virtio_transport_notify_buffer_size+0x60/0x70 [vmw_vsock_virtio_transport_common]
  vsock_update_buffer_size+0x5f/0x70 [vsock]
  vsock_stream_setsockopt+0x128/0x270 [vsock]
...

使用gdb进行快速调试会话显示,RCX寄存器包含释放的virtio_vsock_sock的内核地址,RBX寄存器包含vsock_sock的内核地址。太好了!在Fedora上,我可以打开和解析/dev/kmsg:如果内核日志中再出现一个警告,那么漏洞利用就赢得了又一次竞争,并且可以从寄存器中提取相应的内核地址。

从任意释放到use-after-free

我的利用计划是使用任意释放进行use-after-free:

  1. 释放在内核警告中泄漏的内核地址处的对象。
  2. 执行堆喷洒以用受控数据覆盖该对象。
  3. 使用损坏的对象进行权限提升。

起初,我想利用任意释放针对vsock_sock地址(来自RBX),因为这是一个大结构,包含许多有趣的东西。但这没有奏效,因为它位于一个专用的slab缓存中,我无法在那里执行堆喷洒。所以我不知道在vsock_sock上进行use-after-free利用是否可能。

另一个选项是释放来自RCX的地址。我开始搜索一个对use-after-free有趣的内核对象(例如包含内核指针)。此外,用户空间中的漏洞利用应该以某种方式使内核将

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