
CVE-2025-21756 漏洞复现及利用

CVE-2025-21756
https://nvd.nist.gov/vuln/detail/CVE-2025-21756 为漏洞相关描述信息,该漏洞位于 vsock 模块下,原因为transport
(实际执行数据传输逻辑的后端实现接口集)在重新分配时解除绑定导致 UAF。本篇文章重点在于详细分析漏洞触发原因,漏洞利用部分参考exploit ,同时也会说明一下笔者在实际利用测试过程中遇到的一些问题。
Vsock
VSOCK(Virtual Socket) 是一种专为虚拟机(VM)与宿主机之间高效通信设计的套接字协议,由 VMware 最初提出并集成到 Linux 内核中。它允许虚拟机内的应用程序直接与宿主机或其他虚拟机通信,无需经过传统网络协议栈(如 TCP/IP),从而提供更低延迟和更高吞吐量。
VSOCK 的典型应用场景
(1) 宿主机-虚拟机通信
- 示例:宿主机上的监控工具直接读取虚拟机内的日志。
- 优势:无需配置虚拟网络(如桥接、NAT),避免网络带宽竞争。
(2) 虚拟机间通信
- 示例:同一宿主机上的两个 Kubernetes Pod(运行在不同 VM 中)通过 VSOCK 交换数据。
- 优势:比 overlay 网络(如 Flannel、Calico)更高效。
(3) 嵌套虚拟化
- 在嵌套虚拟化环境中(如 VM 内再运行 VM),VSOCK 可跨多层虚拟化通信。
Patch
Linux kernel 对应的 commit 3f43540166128951cc1be7ab1ce6b7f05c670d8b
新增条件判断 SOCK_DEAD
只有套接字标记为
SOCK_DEAD
(表示已关闭或不可用)时,才从绑定表(vsock_bound_sockets
)移除。避免在传输层切换(
transport reassignment
)时误删绑定。
保留 vsock_remove_connected(vsk)
- 连接表(
vsock_connected_sockets
)的移除逻辑不变,因为连接状态与传输层无关。
将sock_orphan
提前,目的是提前将sk
状态设置为SOCK_DEAD
Basics
Struct info
1 | /* Address structure for vSockets. The address family should be set to |
Introduction
关于 vsock 的相关操作介绍内容如下
bind()
显式绑定:用户调用bind()
时,套接字会被加入vsock_bound_sockets
列表。connect()
隐式绑定(autobind):如果未显式bind()
,connect()
会自动绑定一个随机端口。
1. vsock_remove_bound(vsk)
作用
- 移除套接字的绑定信息(从
vsock_bound_sockets
列表中删除)。 - 通常在以下情况调用:
- 套接字显式调用
bind()
后又被关闭(close()
)。 - 套接字隐式绑定(
autobind
)后被释放。
- 套接字显式调用
1 | void vsock_remove_bound(struct vsock_sock *vsk) |
list_del_init(&vsk->bound_table)
如果vsk
在vsock_bound_sockets
列表(即已绑定),则移除它。sock_put(&vsk->sk)
减少套接字的引用计数(refcnt
),如果refcnt=0
,则释放套接字。
使用场景
- 当
vsock
套接字关闭(release
)或重新绑定(rebind
)时调用。 - 问题修复前:
如果transport
重新赋值(如connect()
时切换传输层),错误调用vsock_remove_bound()
可能导致 UAF(因为vsk
可能未真正绑定)。
2. vsock_remove_connected(vsk)
- 移除套接字的连接信息(从
vsock_connected_sockets
列表中删除)。 - 通常在以下情况调用:
- 套接字已建立连接(
connect()
/accept()
)后被关闭。 - 传输层(
transport
)释放时(如virtio-vsock
断开连接)。
- 套接字已建立连接(
1 | void vsock_remove_connected(struct vsock_sock *vsk) |
list_del_init(&vsk->connected_table)
如果vsk
在vsock_connected_sockets
列表(即已连接),则移除它。sock_put(&vsk->sk)
减少引用计数,可能触发套接字释放。
这里具体说一下 vsk->bound_table
在实际进行绑定操作时,需要先通过vsk->local_addr
计算出哈希值,然后存储到对应hash table
的bucket
当中。
1 | __vsock_remove_bound(vsk); |
相关宏定义如下,同时注释信息也详细解释了相关实现逻辑,不再过多说明
1 | /* Each bound VSocket is stored in the bind hash table and each connected |
Func
这一部分主要包括一些漏洞相关的函数源码,供读者参考,大致浏览即可(后面遇到理解问题时可以在尝试仔细阅读)
vsock_create
1 | static int vsock_create(struct net *net, struct socket *sock, |
__vsock_create
完成当前 sk 的初始化
1 | static struct sock *__vsock_create(struct net *net, |
vsock_bind
1 | static int __vsock_bind(struct sock *sk, struct sockaddr_vm *addr) |
这个函数是 VSOCK (虚拟套接字) 的绑定操作实现,负责将一个 VSOCK 套接字绑定到指定的地址
__vsock_bind_connectible
1 | static int __vsock_bind_connectible(struct vsock_sock *vsk, |
vsk->transport
VSOCK 是一个统一的地址族(AF_VSOCK),用于虚拟机(Guest)与宿主机(Host)之间通信,但不同虚拟化平台提供的数据通道是不同的:
平台 | 底层机制 | 内核 transport 实现 |
---|---|---|
VMware | VMCI(虚拟机通信接口) | vmci_transport |
KVM / QEMU | Virtio | virtio_transport |
Hyper-V | VMBus | hv_transport |
为了支持这些不同平台而不让上层逻辑乱套,VSOCK 设计了一个 vsock_transport
接口结构体来“封装差异”。
vsk->transport
是 vsock_sock
结构体中的一个指针,指向 VSOCK 协议中用于实际执行数据传输逻辑的后端实现接口集,是 VSOCK 子系统中的一个关键抽象
1 | struct vsock_transport { |
sock & vsock
(1) struct sock
(通用套接字层)
- 定义位置:
include/net/sock.h
- 作用:表示一个通用的网络套接字,是所有协议族(如 INET、UNIX、VSOCK 等)套接字的基类。
- 包含字段:
- 协议无关的通用状态(如引用计数
sk_refcnt
) - 套接字队列(接收/发送缓冲区)
- 操作函数表(
struct proto_ops *sk_prot_ops
) - 网络命名空间指针等
- 协议无关的通用状态(如引用计数
(2) struct vsock_sock
(VSOCK专用层)
定义位置:
net/vmw_vsock/af_vsock.h
作用:VSOCK 协议族的扩展数据结构,继承自
sock
并添加 VSOCK 特有的属性和方法。关键字段:
1
2
3
4
5
6
7
8
9
10struct vsock_sock {
struct sock sk; // 内嵌的通用sock结构
struct sockaddr_vm local_addr; // 本地CID+端口
struct sockaddr_vm remote_addr; // 远程CID+端口
// VSOCK特有状态(如传输层接口、流控制等)
const struct vsock_transport *transport;
u32 buf_size;
u32 buf_alloc;
// ...
};
vsock close(s)
call chain
在对 vsock
套接字调用 close(s)
时,相关触发逻辑如下:
1 | __vsock_release(struct sock *sk, int level) |
POC
1 |
|
Vulnerability
Setup ENV
1 | s = vsock_bind(VMADDR_CID_LOCAL, VMADDR_PORT_ANY, SOCK_SEQPACKET); |
这一步主要用于初始化漏洞利用环境(可以参考前面部分__vsock_bind_connectible
的相关实现逻辑),主要是先通过vsock_bind(VMADDR_CID_LOCAL, VMADDR_PORT_ANY, SOCK_SEQPACKET);
创建并绑定一个vsock
套接字到s
,这一步实际是用来得到addr.svm_port
,在后续for
循环中将会在该端口值的基础上递增,用来将最大允许的 MAX_PORT_RETRIES
端口数量消耗完,这一步将会导致后续绑定一个新的vsock
套接字后,调用 connect
找不到可用端口,返回错误,从而触发漏洞相关逻辑。
**STEP.1 **
vsock_create() (refcnt=1) calls vsock_insert_unbound() (refcnt=2)
1 | // vsock_bind wrapped by func `socket` & `bind` in userspace |
STEP.2
transport->release() calls vsock_remove_bound() without checking if sk was bound and moved to bound list (refcnt=1)
1 | /*---------First connect----------*/ |
第一次 connect 触发执行路径如下所示:
此时,由于vsock_auto_bind
返回失败,vsk仍然还在unbound list
当中,需要强调的是vsock_auto_bind
会失败是因为前面一开始,就通过耗尽VMADDR_CID_ANY
对应cid
下所有的有效端口数量,从而使得返回-EADDRNOTAVAIL
,相关代码逻辑如下:
内核调试验证如下所示:
关于这一步connect
的作用在后续分析第二次connect
时会有提到。
第二次 connect 触发执行路径如下所示:
再来关注下vsock_assign_transport
这个函数,该函数实际就是触发 UAF 的所在函数,内容如下:
在执行
transport->release
之后,vsock_deassign_transport
将会把transport
置为空
为方便理解,这里用实际调试时的场景进行解释:
在第一次 connect
时,由于此时 vsk->transport
还未初始化,因此该值为 0,如上图所示,但在函数后面会将vsk->transport
初始化为 new_transport
,即loopback_transport
,因为第一次执行connect
时addr.svm_cid
为 VMADDR_CID_LOCAL
(对应内核中的remote_cid
),此时transport
将会被设置为transport_local
(对应为loopback_transport
),这也是为什么前面需要一次connect
操作。
在执行第二次connect
时,此时 vsk->transport
和new_transport
内容分别如上图所示,此时会调用 vsk->transport->release(vsk)
,而该函数指针实际调用为virtio_transport_release
在该函数内部会调用virtio_transport_remove_sock
,内容如下:
因而使得这里的有效引用位减一。
STEP.3
注意这里的 vsock_auto_bind 还会进行一次释放
最终会触发执行
相较于第一次connect
失败,第二次connect
可以正常进行 bind 操作,是因为第二次connect
时,通过 Step.2 可知transport->release
会将vsk
状态重置,如下图所示,此时在函数__vsock_bind_connectible
调用__vsock_find_bound_socket
会返回 NULL(VSOCK地址未被vsk
占用),会正常完成绑定操作。
内核调试验证如下,此时由于refcnt = 0
会进入到__sk_free(sk)
,会使得该堆块被释放,从而在进入到__vsock_insert_bound
时会触发 UAF。
Crash 如下:
Exploit
这一部分原文 说的也比较详细,这里主要说一些笔者角度的理解。
Step.1 Spray vsock heap object and alloc target vsock for uaf
1 | puts("[+] pre alloc sockets"); |
因为在实际测试过程中发现这里 vsock
套接字的分配来自独立缓存,如下图所示,所以需要借助 Cross Cache Attack
完成利用,这一步主要就是堆喷相关结构体,然后触发目标vuln_vsk
UAF。
Step.2 Release target slab back to buddy system
1 | puts("[+] uaf finished!.."); |
这一步主要是将vuln_vsk
所在的vuln_slab
释放回伙伴系统,用于后面PageHijack
(通过pipe_buffer
来进行实现)。
Step.3 Hijack vuln_slab and detect whether hits target slab by brute force
1 | int pipes[NUM_PIPES][2]; |
可以看到这里采用了一个稍微比较巧妙的方式来判断vuln_vsk
是否被损坏,即通过每次写write
8字节的方式,然后调用query_vsock_diag
是否执行成功判断是否命中成功。query_vsock_diag
最终会调用到vsock_diag_dump
函数中,该函数中有两个检查,具体如下:
其中第二个检查比较好绕过,设置对应的sk->sk_state
为2即可,sock_net
对应sk
偏移为0x30的位置,因此如果该位置出现损坏,将会读取失败(最终反应在 len
字段上面),从下图可以看到比较的对象是init_net
,该值为0xffffffff84bb1f80
,因此为了绕过这个判断,我们需要对init_net
的地址进行爆破(Step.4)。
Step.4 Brute force the address of init_net
1 | long base = 0xffffffff84bb1000; // probably need to change for aslr |
这里先是估计了一个init_net
的基地址0xffffffff84bb1000
,然后从这个基地址开始爆破,爆破的思路是只要发现在写入新的init_net
值后,不能通过vsock_diag_dump
的判断就重新释放vuln_slab
,然后重新获取该slab
在尝试进行写。
在实际利用过程中,这种重复释放page并进行回收写,每次都成功命中的概率是很低的。但是由于测试的文件系统环境比较干净,使得该 POC 在这样的情况下利用成功率还不错,这一点是需要注意的。
同时,这里猜的地址实际上是没有开随机化时的值,所以实际情况下要爆破出该地址更是难上加难。
Step.5 Hijack control flow and Construct ROP
由于我们已经劫持了vuln_vsk
所在的 page,所以vuln_vsk
结构体的内容由我们可控,与其他劫持控制流的思路一样,vsk
中也有一些函数指针的调用,这里劫持的思路是通过vsock_release
实现控制流劫持。
sk->sk_prot->close
是一个二级虚表函数指针调用,这意味着可以通过伪造sk_prot
指向一个拥有函数指针的结构体实现任意函数调用,这里选取的对象是raw_prot
,该对象存在一个函数指针调用为raw_abort
,函数内容如下:
如下图所示,因此可以通过伪造sk->sk_err_report
字段来实现控制流劫持的目的。
由于 sk
结构体内容可控,所以控制流劫持第一步当然是栈迁移,经过调试后发现在执行到sk_err_report
时,寄存器rbx
和rdi
都指向vuln_vsk
的堆地址,所以只要找类似push rbx ; pop rsp ; ret
或者push rdi ; pop rsp ; ret
类似的 gadget 就可以了,但是实际上内核中品相这么好用的 gadget 很少(几乎没有),通过ropper
和ROPgadget
都没有找到。
这里提一下笔者找栈迁移 gadget 的思路,由于前面两种常见找gadget方式都没找到,所以采用了一种比较原始的方式,即扫描内核kernel code
段匹配对应push rbx ; pop rsp ;
的字节码 0x535C ,然后在找到的结果(总共68处)中在人工筛选满足条件的能够完成实际栈迁移的gadget
。
最终锁定如下图所示的一处 gadget
,由于中间有个jb
跳转,所以在实际测试过程中可能因为满足跳转条件而不走后面 ret
,怀着忐忑的心情在测试后,发现可以成功执行到ret
完成栈迁移,至此就可以布置 ROP 链完成提权。
详细 ROP 链如下;
1 | // create the rop chain! |
Final
最终利用效果如下图所示
exp
1 |
|
Notion
关于漏洞利用部分有一些需要注意的点
- POC 成功需要大量的页释放和回收命中操作,使得成功率较低
- 同时原文中所提出的爆破地址,个人看来也仅局限于不开随机化的情况,因为在开启随机化的同时也意味着爆破的次数增加,这使得导致对页命中的要求更高了,从而提前导致内核崩溃
- Title: CVE-2025-21756 漏洞复现及利用
- Author: henry
- Created at : 2025-06-13 11:46:57
- Updated at : 2025-06-13 12:00:04
- Link: https://henrymartin262.github.io/2025/06/13/CVE-2025-21756/
- License: This work is licensed under CC BY-NC-SA 4.0.