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.