QEMU-KVM虚拟化平台下虚机逃逸漏洞修复方案
Posted 苏研大云人
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了QEMU-KVM虚拟化平台下虚机逃逸漏洞修复方案相关的知识,希望对你有一定的参考价值。
友情提示:全文6200多文字,预计阅读时间16分钟
摘要
在公有云环境中, 用户不再拥有基础设施的硬件资源,软件都运行在云中,业务数据也存储在云中,因此安全问题是用户决定是否上云的主要顾虑之一。
公有云的安全涉及虚拟化安全、管理安全、数据安全等多个方面,其中关键核心与安全前提是虚拟化安全,因为只有先解决了这一问题,公有云自身的环境特有的安全问题才能够逐步地、完整地得以解决,最终为用户提供一个安全有保障的云上环境。
因此,云服务提供商的安全预防、响应、问题处理能力尤为重要。
一、漏洞简介
近期,社区在QEMU-KVM虚拟化平台的vhost内核模块中发现了一个缓冲区溢出漏洞(CVE-2019-14835),可能在虚拟机热迁移过程中被滥用,拥有特权的Guest用户可以将长度无效的描述符传递给宿主机,并升级其在宿主机上的特权,进而通过此漏洞实现虚拟机逃逸攻击,操纵虚拟机使宿主机内核崩溃或在物理机内核中执行恶意代码,控制宿主机。
二、virtio 网络简介
virtio是为虚拟机开发的标准化开放接口,用于访问简化的设备,如块设备和网络适配器。virtio-net是一个虚拟以太网卡,是virtio迄今为止支持的最复杂的设备。
图 1 vhost-net / virtio-net架构
virtio网络有2层(如图1所示):
1、Control plane:用于主机和客户之间建立和终止Data plane的能力交换协商
2、Data plane:用于在主机和客户端之间传输实际数据
其中,Control plane基于virtio规范,定义如何在Guest和Host之间创建控制平面和数据平面,为了性能Data plane并没有采用virtio规范,而是使用了vhost协议来进行数据传输。
由图1中可以看出,virtio中位于Guest的组件为virtio-net,作为前端运行在Guest内核空间,位于Host的组件为vhost-net,作为后端运行在Host内核空间。
图 2 virtio 网络数据传输
在实际的Data plane通信过程中,发送和接收是通过专用的队列完成的(如图2所示),对于每个虚拟cpu都会创建一个发送和一个接收队列。
以上描述了Guest如何使用virtio网络将包传递给主机内核。为了将这些包转发给在同一主机上或在主机(如internet)之外运行的其他客户机,需要使用OVS。
OVS是一个软件交换机,它允许在内核中转发数据包。它由用户空间部分和内核部分组成(如图3所示):
用户空间:包括用于管理和控制交换机的数据库(ovsdb-server)和OVS守护进程(OVS -vswitchd)
内核空间:包括负责数据路径或转发平面的ovs内核模块
图 3 OVS连接virtio模型
从图3中可以看出,OVS控制器与ovsdb-server和内核层的forwarding plane通信,使用2个端口将数据包推入和推出OVS。如图3所示,有一个端口将OVS内核forwarding plane连接到物理NIC,而另一个端口连接到vhost-net后端。
三、攻击分析
该漏洞存在于宿主机的vhost/vhost_net内核模块中,vhost/vhost_net是一个virtio网络后端。bug发生在热迁移流程中,在迁移时,QEMU需要知道脏页面,vhost/vhost_net使用内核缓冲区来记录脏日志,但不检查日志缓冲区的边界。
因此,我们可以在Guest中伪造desc表,等待迁移或做一些事情(比如增加工作负载)来触发云供应商迁移这个Guest。当Guest迁移时,它将使主机内核日志缓冲区溢出。
漏洞调用路径为:
handle_rx(drivers/vhost/net.c) -> get_rx_bufs -> vhost_get_vq_desc -> get_indirect(drivers/vhost/vhost.c)
在VM Guest中,攻击可以在VM驱动程序中创建一个间接的desc表,让vhost在热迁移VM时进入上面的调用路径,最后进入函数get_indirect。
漏洞的详细触发逻辑如下(以BC-Linux7.5系统bek内核4.14.78-300.el7.bclinux为例,省略了前后调用函数信息):
1. static int get_indirect(struct vhost_virtqueue *vq,
2. struct iovec iov[], unsigned int iov_size,
3. unsigned int *out_num, unsigned int *in_num,
4. struct vhost_log *log, unsigned int *log_num,
5. struct vring_desc *indirect)
6. {
7. struct vring_desc desc;
8. unsigned int i = 0, count, found = 0;
9. #len 可以由VM Guest控制传入
10. u32 len = vhost32_to_cpu(vq, indirect->len);
11. struct iov_iter from;
12. int ret, access;
13.
14. …省略…
15.
16. #所以count也能被VM Guest控制
17. count = len / sizeof desc;
18. /* Buffers are chained via a 16 bit next field, so
19. * we can have at most 2^16 of these. */
20. #count最大能到USHRT_MAX + 1即2^16 + 1
21. if (unlikely(count > USHRT_MAX + 1)) {
22. vq_err(vq, "Indirect buffer length too big: %d\n",
23. indirect->len);
24. return -E2BIG;
25. }
26.
27. do {
28. unsigned iov_count = *in_num + *out_num;
29. #所以这个循环能执行USHRT_MAX+1 次
30. if (unlikely(++found > count)) {
31. vq_err(vq, "Loop detected: last one at %u "
32. "indirect size %u\n",
33. i, count);
34. return -EINVAL;
35. }
36. #导致此处每个 desc 都能被控制
37. if (unlikely(!copy_from_iter_full(&desc, sizeof(desc), &from))) {
38. vq_err(vq, "Failed indirect descriptor: idx %d, %zx\n",
39. i, (size_t)vhost64_to_cpu(vq, indirect->addr) + i * sizeof desc);
40. return -EINVAL;
41. }
42. …省略…
43. if (desc.flags & cpu_to_vhost16(vq, VRING_DESC_F_WRITE))
44. access = VHOST_ACCESS_WO;
45. else
46. access = VHOST_ACCESS_RO;
47.
48. #如果设置 desc.len为0 , translate_desc 不会返回错误并且 ret == 0
49. ret = translate_desc(vq, vhost64_to_cpu(vq, desc.addr),
50. vhost32_to_cpu(vq, desc.len), iov + iov_count,
51. iov_size - iov_count, access);
52. …省略…
53. /* If this is an input descriptor, increment that count. */
54. if (access == VHOST_ACCESS_WO) {
55. #因为ret == 0, in_num 的值不会改变 (如果in_num 的值大于ov_size会导致translate_desc返回错误
56. *in_num += ret;
57. #当执行热迁移时,log 的buffer不会为NULL
58. if (unlikely(log)) {
59. #因为 log_num 能够为 USHRT_MAX大小,但log的buffer大小远远小于 USHRT_MAX, 从而导致了log的buffer溢出
60. log[*log_num].addr = vhost64_to_cpu(vq, desc.addr);
61. log[*log_num].len = vhost32_to_cpu(vq, desc.len);
62. ++*log_num; #不停地循环计数
63. }
64. } else {
65. …省略…
66. }
67. } while ((i = next_desc(vq, &desc)) != -1);
68. return 0;
通过上述代码可以看出,在每次循环时,只要将in_num加上ret的计数值,并且将log_num加1,理论上就可以保证log_num < in_num这个条件在任何时候都成立。但是,代码并没有考虑到在一些特殊情况下in_num不增加的情况,一旦出现in_num不增加,而log_num增加,就会触发宿主机log数组越界访问。
也就是说,如果虚机恶意的创建了desc.len = 0的vring desc,那么就出现了in_num不增加,而log_num增加的情况,从而构造了此bug触发的部分条件,再辅以虚机热迁移(热迁移期间一定条件下会调用到相关的代码部分),最终会操纵虚拟机使宿主机内核崩溃或在物理机内核中执行恶意代码,控制宿主机。
同时还需说明的一点是,类似的这个问题在vhost内核模块的另一个函数vhost_get_vq_desc中也存在。
四、修复方案
基于上述的分析,本问题的解决方法就是当desc.len为0时(也就是ret参数值为0时)不记录log,在这种情况下log_num的值也就不会增加,既可确保log_num < in_num这个条件在任何时候都成立。
详细补丁改动如下:
1. diff --git a/drivers/vhost/vhost.c b/drivers/vhost/vhost.cindex 34ea219..acabf20 100644--- a/drivers/vhost/vhost.c+++ b/drivers/vhost/vhost.c
2. @@ -2180,7 +2180,7 @@ static int get_indirect(struct vhost_virtqueue *vq,
3. /* If this is an input descriptor, increment that count. */
4. if (access == VHOST_ACCESS_WO) {
5. *in_num += ret;
6. - if (unlikely(log)) {
7. + if (unlikely(log && ret)) {
8. log[*log_num].addr = vhost64_to_cpu(vq, desc.addr);
9. log[*log_num].len = vhost32_to_cpu(vq, desc.len);
10. ++*log_num;
11. @@ -2321,7 +2321,7 @@ int vhost_get_vq_desc(struct vhost_virtqueue *vq,
12. /* If this is an input descriptor,
13. * increment that count. */
14. *in_num += ret;
15. - if (unlikely(log)) {
16. + if (unlikely(log && ret)) {
17. log[*log_num].addr = vhost64_to_cpu(vq, desc.addr);
18. log[*log_num].len = vhost32_to_cpu(vq, desc.len);
19. ++*log_num;
4.1 升级内容
操作系统团队在第一时间跟进了此漏洞信息,并发布了安全更新公告,详细的安全更新内容如下:
系统版本 |
漏洞修复版本 |
公告详情 |
BC-Linux7.2 |
kernel-3.10.0-327.82.1.el7 |
BLSA-2019:0050 - [重要] kernel 安全更新公告 |
BC-Linux7.3 |
kernel-3.10.0-514.69.1.el7 |
BLSA-2019:0051 - [重要] kernel 安全更新公告 |
BC-Linux7.4 |
kernel-3.10.0-693.59.1.el7 |
BLSA-2019:0048 - [重要] kernel 安全更新公告 |
BC-Linux7.5 |
kernel-3.10.0-862.41.2.el7 |
BLSA-2019:0047 - [重要] kernel 安全更新公告 |
BC-Linux7.6 |
kernel-3.10.0-957.35.2.el7 |
BLSA-2019:0046 - [重要] kernel 安全更新公告 |
对于使用了BC-Linux系统的用户,可以参考上述的公告信息中的步骤,通过yum命令自动对系统内核进行升级,并重启系统生效补丁功能;
4.2 内核热补丁
升级内核需重启,一般线上生产环境并不会采用此方式,因为系统重启即意味着业务的中断。
我们针对线上运行的系统,推出了热补丁的修复方案,可以在不重启系统的前提下修复此漏洞,保障了用户的业务不中断。
以BC-Linux7.3系统3.10.0-514.1.el7内核为例,热补丁使用方式如下:
1、安装kpatch用户态工具和补丁包
下载并安装BC-Linux团队提供的BC-Linux7.3系统下的热补丁管理工具:
bclinux-kpatch-0.6.0-2.el7_3.bclinux.x86_64.rpm
下载并安装BC-Linux团队提供的BC-Linux7.3系统下的热补丁文件:
bclinux-kpatch-patch-7.3-5.el7_3.bclinux.x86_64.rpm
2、为系统打上热补丁
首先先检查一下当前系统的热补丁信息:
1. [root@bclinux-vm1 ~]# kpatch list
2. Loaded patch modules:
3.
4. Installed patch modules:
5. vhost_make_sure_log_num_CVE_2019_14835 (3.10.0-514.1.el7.x86_64)
启动kpatch服务来加载所有已安装的kpatch热补丁模块,同时配置默认系统启动时开启kpatch服务,保证系统重启后热补丁依然生效:
1. [root@bclinux-vm1 ~]# systemctl start kpatch
2. [root@bclinux-vm1 ~]# systemctl enable kpatch
3、验证热补丁是否生效
1. [root@bclinux-vm1 ~]# kpatch list
2. Loaded patch modules:
3. vhost_make_sure_log_num_CVE_2019_14835 [enabled]
4.
5. Installed patch modules:
6. vhost_make_sure_log_num_CVE_2019_14835 (3.10.0-514.1.el7.x86_64)
通过上述热补丁方案,即修复了问题,同时我们也采用社区公布的POC方案进行了验证,确保了热补丁的有效性。
本次发现的CVE-2019-14835这个QEMU-KVM漏洞存在于虚拟化宿主机的内核层面,它突破了虚机与宿主机、虚机与虚机之间的安全边界(以往我们发现的一些漏洞更多是存在于用户态层面),操作系统团队及时响应了相关问题,并针对线上环境出具了热补丁方案,目前在按计划对移动云等线上环境使用热补丁方案修复,整个过程用户无任何感知,保证了用户业务的持续稳定运行。
参考
1、Introduction to virtio-networking and vhost-net
2、VHOST-NET GUEST TO HOST ESCAPE - Kernel vulnerability - CVE-2019-14835
3、V-gHost : QEMU-KVM VM Escape in vhost/vhost-net
4、https://www.openwall.com/lists/oss-security/2019/09/17/1
5、https://www.openwall.com/lists/oss-security/2019/09/24/1
6、patch: vhost: make sure log_num < in_num
END
往期精选
1、
2、
3、
以上是关于QEMU-KVM虚拟化平台下虚机逃逸漏洞修复方案的主要内容,如果未能解决你的问题,请参考以下文章
主流虚拟化平台 QEMU-KVM 被曝存在漏洞,可完全控制宿主机及其虚拟机 | 每日安全资讯
GitHub现VMware虚拟机逃逸EXP,利用三月曝光的CVE-2017-4901漏洞