*本文作者:Murkfox,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。
前言
硬件虚拟化为广大从事IT行业的朋友提供了极大的便利。同时也是“云”这一概念的重要支持技术之一。微软在前有VM,QEMU等产品的广泛应用下毅然决然的发布了微软编写的虚拟化产品Hyper-V,并运用到了Microsoft Azure 云计算平台中。今年的 Blackhat 大会中也有议题对 Hyper-V 的安全问题展开了讨论。今天我们将对其从安全角度上进行深度的分析。
0x01 工作原理
先上一张Hyper-V结构的布局图:
当用户开启虚拟化(Intel VT)支持后,物理机会在进入Windows 内核前启动 hvix64.exe/hvax64.exe 初始化 Hypervisor 层再运行 Windows 内核程序完成系统加载。
当 Hypervisor 层被启用后,宿主机在操作硬件时也会通过 Hyperisor 再传至物理设备,因此 Hyperisor 层的分级则为 Ring -1 (最新版的 Hyper-V 则会支持部分操作不进入Ring -1层直接传至物理设备进行操作)
宿主机内核与虚拟机内核都是Ring 0级操作,只不过宿主机可以控制虚拟机,所以,如果虚拟机发生了故障,不会影响到宿主机;宿主机发生了非致命故障,例如并非蓝屏等Ring 0下的故障,也不会影响虚拟机的正常运行。
宿主机与虚拟机之间的数据传输通过VMBus(虚拟数据总线)实现,VMBus是微软开发的虚拟数据传输总线,用来宿主机和虚拟机之间交换数据和信息。使用VMBus的优点就是能进行快速、高效、大数据量的数据传输,极大的提升了虚拟机的性能。VMBus的原理和QEMU中的VirtIO设备类似,通过共享环形内存,每当要传输的数据写满整个环形内存时,就把数据发送到宿主机中。可以说,微软成功的借鉴了VirtIO设备的优点,取其优点用之,才会使得Hyper-V虚拟机性能大幅提升。
和QEMU模拟设备端口读写的方法不同的是,Hyper-V全部使用了虚拟设备,而不是模拟硬件端口。Hyper-V不再使用传统的读写模拟硬件端口的方式进行硬件设备的虚拟化,而是使用了虚拟设备作为设备模拟。例如,在虚拟机中网卡,硬盘等设备的驱动都是由微软提供,如果没有这些驱动程序,虚拟机则无法使用虚拟设备,就像是QEMU中的VIRTIO设备一样,需要加载特殊驱动,而不是使用和真实硬件一样的驱动。
下图表示了虚拟机如何通过宿主机访问网络:
其中虚拟机与物理机的数据交互如下图:
虚拟机与物理直接的数据交互通过环形内存的读写来完成。对环形内存的读写操作由VMBus来完成
下面我将通过这个例子来阐述虚拟机是如何与物理机进行交互的:
虚拟机用户通过 foo.exe 发起一个 TCP 请求;
请求传递到虚拟机的网卡驱动;
虚拟机网卡驱动会通过 VMBus 将请求封装成一个数据包传递到环形内存中;
在虚拟机写满一个环形内存或将数据成功写入环形内存后;
虚拟机使用Hypercall指令通知宿主机,数据已经写完,并产生一个VM-Exit事件陷入 Hyperisor 层执行代码;
这时 Hyperisor 层就会出发注册在 WIndows 内核中的中断例程(IDT);
Windows 内核接收到消息后会根据虚拟设备类型调用相对应的函数读取环形内存的数据,并将数据分发到相对应的虚拟设备驱动;
这样数据就由虚拟机成功的转移到物理机,相关的虚拟驱动会继续解析数据;
由物理机传入数据至虚拟机道理相同;
更细致的分析可以参考文章。
0x02 测试
根据 Joe Bialek 和 Nicolas Joly 在大会中所讲,对于 Hyper V 的攻击主要在于对宿主机内核态的攻击,大部分情况是会造成宿主机的崩溃。
有关用户态的攻击通常无法造成实质的效果,一般情况会直接被异常接管,漏洞利用及其复杂。
以下是宿主机内核态组件:
VMSwitch.sys:提供半虚拟化网络
StorVSP.sys:提供半虚拟化存储
VID.sys:虚拟化设施驱动
WinHVr.sys:内核到hypervisor的接口(hypercall)
VMBusR.sys:VMBUS,负责guest与host的通信
vPCI.sys:半虚拟化PCI
根据看雪ID: ifyou 的介绍,在用户态中的 vmuidevices.dll 组件也非常容易受到攻击,这是一个负责图形显示的组件,由于图形显示的程序编写及其复杂,所以这里出问题的概率也会大很多。
在 Blackhats 大会中 Joe Bialek 者阐述了一个通过VMSwitch.sys组件实施逃逸的案例。这个时候我们要详细的讲解一下有关于虚拟机与宿主机的数据交互细节。 在原理中我们讲到,宿主机与虚拟机的数据交互通过VMBus控制,但数据并不直接由VMBus进行传输,而是缓存到两者之间的环形内存中,环形内存至少存在两个 一个请求内存,一个应答内存。
下面是虚拟机与宿主机通过环形内存进行数据交互的过程:
虚拟机发送的数据通过VMBus传入请求内存。
随后通过Hypercall的方式通知宿主机提取数据,产生VM-EXIT事件陷入Hyperisor层 当宿主机收到消息后会触发中断,随后读取请求内存的数据,分发给相关的虚拟驱动程序处理。
数据处理完成后向虚拟机发送处理成功的消息,并将应答给虚拟机的数据,写入应答内存。 虚拟机读取应答内存的数据并回应一个消息告诉宿主机,应答数据有有效。
宿主机才会释放线程进入下一个数据包的处理。 当然,环形内存是会被分割成数个子区。环形内存的描述(GPADL)由虚拟机提供,环形内存中的子区大小及其数量由宿主机自适应。
举个例子:一个应答环形内存的地址以及大小由虚拟机使用GPADL通知宿主机,宿主机会根据GPADL;里提供的指针索引到应答环形内存的地址,然后根据GPADL中提供的内存大小去调整这一段环形内存中子区的大小并生成适配这一段内存的子区分配表。
Blackhats 大会中 Joe Bialek 利用的漏洞便是针对于数据交互中,更新内存指针与更新和生成这一内存中的子区大小及其数量,并非是原子操作(中间存在时间差),提出了竞态攻击。
攻击设想:
我们可以控制宿主机接收的数据;
我们传入的数据可以通过竟态完成越界;
越界之后的数据可以执行可控的攻击。
作者通过RNDIS control message responses来控制写入的内容。这是虚拟机要发送的网络数据经过虚拟机内核态中的虚拟网络组件构成的 RNDIS 协议数据。
第二点实际上是要求我们控制数据包的延迟,已达到,应答内存的指针已经更新到新的内存地址,但宿主机上依然保留着旧内存地址的子区分配表,这样就会存在,子区分配表中所描述的总内存大小与新内存的大小不匹配,这会给我们的越界操作提供越界内存。
首先虚拟机发送GPADl通知内存地址指针,宿主机收到通知后更新内存指针,指到新的内存地址,但依旧保留这旧的子区分配表:
然后宿主机根据GPADL中通知的此段内存的大小分配子区大小及其数量,生成新的子区分配表:
生成子区分配表后,执行更新动作,将适配与原先内存的子区分配表量更新到新的子区分配表:
如图所示,在更新内存指针之后,宿主机还没有更新子区分配表,子区内存的总大小是和新内存不适配。这样就存在越界内存 ,给我们提供了存放攻击载荷的空间。
针对于 RNNIS 协议的数据,宿主机在处理完虚拟机发送的数据后会将应答给虚拟机的数据封装成 cmplt 数据包,宿主机处理完数据后发送处理成功的消息,并将 cmplt 包 写入应答内存,等待虚拟机的回应。如果虚拟机没有回应,则负责处理该数据的线程会一直处于等待状态,不会处理下一个数据。
当然,并不是每一个 cmplt 包都会被虚拟机回应,比如畸形的 cmplt 数据包。
所以我们可以通过虚拟机传递给宿主机的 RNDIS 协议数据 使宿主机在处理数据后生成畸形的 cmplt 数据包。达到阻塞宿主机数据处理线程,造成延迟。 而后在N个畸形数据包后附加一个有效的并带有攻击载荷的cmplt数据包,使其在宿主机更新到新的应答内存过程中,指针已经偏移但子区分配表未更新时,写入越界内存。
当然,最重要的,是我们在成功越界写入之后,所造成的效果是否是我们可控的,否则将无法造成有效攻击。
Jordan Rabet 在大会中做出进一步分析。MDL(表明虚拟内存缓冲区的物理页面布局)映射到物理内存,而这些MDL会被映射到SystemPTE区域。这个区域里通常存放的是其他的MDL以及内核栈。
毫无疑问,我们将内核栈作为我们攻击的目标,Win的内核栈一共有7页,6个页面作为栈空间,而最后一个页面在底部作为guard page。
那么问题来了,我们怎样将内核栈放到我们可以操控的越界空间中,并且我们怎样开启一个线程去执行这一步操作。
先来看一下 SystemPTE 分配内存的流程:
它基于一个可以进行扩展的Bitmap 分配以及检索内存空间;
每个位代表一个页面的状态 位0表示空闲页,1表示已分配使用 内存分配基元 进行内存分配;
从内存分配基元 的位置开始扫描 Bitmap;
如果需要改变某一个内存空间的状态,则包装此空间并改变此空间的位;
内存分配基元 会放在成功分配的空间的尾部;
如果找不到空闲内存,则展开Bitmap 搜索新的可用内存。
下面是它分配页面的一个示例:
由此发现,我们需要一个可控的内存分配基元帮助我们在 SystemPTE 区域中布局内存。 但其实,回顾环形内存的特性,虚拟机可以任意构造大小和数量不限的MDL(他们存在上限,但是非常高,以至于我们不用考虑大小的问题)。
由于宿主机应答给虚拟机的数据(NVSP_MSG1_TYPE_REVOKE_RECY_BUF和NVSP_MSG1_TYPE_REVOKE_SEND_BUF)是可以被撤回的,当然这是一个bug,当多个撤销消息被处理时,除了最后一个工作线程,其它的工作线程都会被永久死锁,但我们依然有方法通过这样的机制去撤回内存的使用。
因此我们就有了内存分配和释放的基元来帮助我们操控这个区域。但我们还需要生成新的内核栈,以便我们越界后对其进行操控。所以我们还需要堆栈分配基元。
vmswitch依赖系统工作线程执行异步任务,这些线程被放在内核维护的线程池中,于是我们可以通过向里边添加线程的方式获取这一内核栈的权限。只有所有的进程都在忙的时候,我们才可以向里边加入新的线程。所以我们要做到:
多次快速的触发异步任务。如果产生的任务足够快,那么就会有新的线程加入。有几类vmswitch消息依赖于系统工作线程,例如我们使用的NVSP_MSG2_TYPE _SEND_NDIS_CONFIG。 漏洞发掘者 在实验的过程中利用这样的方法创建了5个线程。
如果无法创建更多的线程,说明线程池中已经有了足够多的线程。这个时候我们就要通过上述的撤回bug,锁死其他线程,造成一个受限的线程栈喷射。(在shellcode的前面加上大量的slide code(滑板指令),组成一个注入代码段。然后向系统申请大量内存,并且反复用注入代码段来填充。这样就使得进程的地址空间被大量的注入代码所占据。)
这样我们便可以生成一个靠近应答内存的内核栈:
内核的保护机制,能使任何堆缓冲区溢出到内存区域的末尾。我们需要多次申请内存以最大限度的接触到 SystemPTE 的末尾,如图中所示,实验过程中,可替换的应答数据与内核栈距离已经很接近了。
最后的最后我们需要绕过空间地址随机化防御Bypassing KASLR。
这里用到一个信息泄露的漏洞,造成信息泄露漏洞的结构体是nvsp_message,它在栈上进行分配,它只初始化了前8个字节,却返回了sizeof(nvsp_message)大小,因此32字节未被初始化的栈内存会被发回给guest,造成信息泄露。通过泄露的信息,我们能够获得vmswitch的返回地址,进而构造rop链。
综上,整个利用过程便是:
使用infoleak定位vmswitch的返回地址;
使用信息建 ROP 链,但由于我们并不知道我们破坏的是那个堆,所以我们要构建一个ROP NOP-sled 这意味着我们要连续执行一串RET 指令;
通过 SystemPTE massaging生成可控内核;
使用竞争条件覆盖宿主机原 内核线程堆栈与ROP链;
在宿主机上执行ROP。
防御手段:
上述漏洞只出现在Windows Server 2012 R2 中 Win 10并没有这类漏洞。
虚拟机管理程序强制执行的代码完整性(HVCI);
攻击者无法将任意代码注入主机内核;
内核模式控制流保护(KCFG);
攻击者无法通过劫持函数指针来实现内核ROP;
这里作者采用的防御手段是将内核栈迁移到单独的区域,实现隔离。
后记
搭建 Windbg 双机调试时,网络双机调试失败,依然是通过串口进行双机调试(延迟略高,调试了一天。。)
Vmware 14.0 可以加载Windows Server 12 虚拟机镜像(VM12 不支持),如果采用虚拟机嵌套,内存制少分配8G
测试 Joe Bialek 在大会上演示的逃逸漏洞,需要多次调试,本人在测试中多次崩溃(操控SysPTE内存。。。不要灰心,你总能成功!
*本文作者:Murkfox,本文属 FreeBuf 原创奖励计划,未经许可禁止转载。