【Black Hat Asia 系列分享】清道夫:误用“错误处理代码”导致的 QEMU/KVM 逃逸
作者: 林以、高凝@蚂蚁安全实验室
原文链接:https://mp.weixin.qq.com/s/1KYTZynabBqzNjoJhe1bWw
在今年的Black Hat Asia上,蚂蚁安全实验室共入选了5个议题和3个工具。本期分享的是蚂蚁光年实验室的议题《清道夫:误用“错误处理代码”导致的QEMU/KVM逃逸》。
本研究设计了针对QEMU Hypervisor系统中错误处理代码的导向性模糊测试技术(Directed Fuzzing),利⽤距离引导(Distance-guided)的策略使模糊测试(Fuzzing)遍历所有的错误处理代码。通过Fuzzing发现了⼀个存在于错误处理上下⽂的漏洞,该漏洞的直接后果会导致释放⼀块未初始化的内存,我们将其命名为“清道夫”(scavenger)。
由于该漏洞的类型并不常规,并且当前针对虚拟机逃逸的漏洞案例⾮常少,可供参考的材料⽋缺,导致该漏洞实际利⽤的难度极其⼤,我们很难在QEMU代码本身中找到有效的数据结构来完成漏洞利⽤所需的内存布局。由此,我们提出了⼀种新型的跨虚拟机内存域攻击,这是在本领域⾸次提出这个概念,根据⽤户天然地完全可控客户机内存的特性,利⽤客户机内存来构造任意读写的原语(primitive),从⽽辅助主机进程进⾏内存布局劫持控制流,最终完成漏洞利⽤实现虚拟机逃逸,从客户机端获取主机的完全控制权限。
01 背 景
1.1 QEMU
QEMU是一个通用的、开源的机器仿真器和虚拟机。它支持多种体系结构,如Ia32、x86-64、mips、 sparc、arm、risc-v等。此外,它还包括大量的仿真设备,包括NVMe控制器。同时,QEMU在安全研究中有着广泛的应用,如物联网固件仿真、用于黑盒Fuzzing的afl-QEMU、动态插桩平台等。随之而来的是,QEMU有很多攻击⾯,特别是设备仿真,因为他允许攻击者从客户机向主机写入数据。高质量的漏洞允许攻击者从VM中逃逸出来控制主机。
1.2 NVMe虚拟设备
NVMe⽤于提供虚拟固态硬盘(SSD)服务,为 PCIe SSD的来宾和主机之间的通信定义了⼀个优化的寄存器接⼝、CMD和功能集。NVMe对 SR-IOV等 I/O虚拟化体系结构提供了⾼效的⽀持,这使得它在SSD设备仿真中越来越流⾏。QEMU中也同样⽀持了对 NVMe设备的模拟。
02 针对错误处理代码的导向性Fuzzing
2.1 动机
我们的⼯作受启发于CVE-2020-25084漏洞,其原因是误⽤错误处理代码。该漏洞成因是出现在设置USB数据包时,设备没有检查 usb_packet_map函数的返回状态,⽽在该函数内部存在⼀段释放内存的错误处理代码。
假如未判断usb对象是否被释放,则在后续使⽤对象的过程中会导致uaf漏洞。QEMU官⽅的 patch是通过添加错误检查代码来修复此漏洞,以便在usb_packet_map函数申请失败时停⽌处理usb请求。此漏洞会导致拒绝服务并可能被逃逸利⽤。
2.2 设计
我们经过调查发现QEMU中的错误处理代码可以分为以下⼏类:调试报告和资源释放。其中调试报告代码只执⾏调试报告,不操作运⾏时资源,此类别不会对QEMU造成内存损坏的有害⾏为。⽽资源释放类别可以进⼀步分为释放内存或⽂件处理程序,以及释放锁类别。对于内存释放类别的错误处理代码,如果调⽤者不检查返回值/状态并滥⽤释放的内存,此⾏为可能会导致uaf漏洞。对于锁释放类别的错误处理代码,如果不谨慎使⽤则可能导致竞争条件漏洞。
我们观察到⼤多数的错误处理都是由goto跳转⽽来。因此,我们设计了如下针对错误处理代码的导向性 Fuzzing技术:
1.定位到代码中的goto语句,通过反向切⽚分析,得到goto的调⽤者和goto语句体;
2.我们使⽤AFLGo作为我们的Directed Fuzzing引擎,在步骤1收集的信息被⽤来作为反馈给AFLGo;
3.在预处理阶段,goto语句的代码体是我们模糊处理过程的⽬标点,⽤于计算种⼦距离。在 Fuzzing loop过程中,我们基于距离引导策略来执⾏调度,并考虑到调⽤路径覆盖率,使 Fuzzing过程遍历所有的错误处理代码及其调⽤路径。
03 漏洞分析
通过上述Fuzzing过程我们发现了 Scavenger漏洞,我们利⽤该漏洞在2020天府杯原创漏洞演示赛上完成QEMU⾮默认设备逃逸。该漏洞的基本信息如下:
· 名称:清道夫(Scavenger)
·类型:NVMe设备的未初始化释放(Uninitialized Free)漏洞
· 影响版本:QEMU-5.1.0及以前版本
· 漏洞利⽤环境:主机Ubuntu20.04, 客户机Ubuntu20.04, 保护全开(NX,ASLR, PIE等)
此漏洞位于nvme_map_prp函数中,函数有两种初始化类型,类型1是iovec,类型2是 sglist,但是在错误处理中只针对sglist类型进⾏释放。这种错误处理代码的误使⽤导致 malloc/free对不⼀致。
例如, 该函数预期是初始化类型 2 sglist,如果它进⼊错误状态,它将转到unmap label以释放qsg结构。但是实际上,我们可以控制程序让其⾸先初始化类型1 iovec,然后转到错误处理代码来释放qsg结构。此时qsg是处于未初始化状态,这会导致未初始化Free漏洞。
04 漏洞利用
4.1 思路分析
⾸先,让我们看看这个错误处理函数 qemu_sglist_destroy中发⽣了什么。如上所述,通过不⼀致的malloc/free对,这⾥的参数 qsg并没有初始化过。这个函数存在着⼀个危险的操作 - 释放了这个未初始化结构中的第⼀个元素sg。虽然这是⼀个未初始化的变量,但其实它是可以被初始化的,因为攻击者可以控制执⾏环境并在相应的内存位置放置恶意构造的数据。这意味着,如果攻击者可以控制未初始化的内存,则可以达到⼀个任意地址Free的效果。
我们需要确认这个未初始化结构是否可以被攻击者控制。所以我们想知道qsg是从哪⾥来的?通过审计源代码并搜索找到nvme_map_prp函数的引⽤,我们发现有三个可以触发未初始化 Free漏洞的函数的代码路径。它们分别属于不同的函数调⽤栈:分别由 nvme_dma_read_prp
,nvme_dma_write_prp
以及 nvme_rw调⽤⾄漏洞函数。
在前两条路径中qsg属于栈上的未初始化变量,但是由于NVMe设备的功能相对⽐较简单, 我们在回溯函数调⽤链时,并没有在对应偏移的栈上找到⽤户可控的数据,也就意味着栈上的未初始化在这⾥是不可控的。
我们最终锁定在第三条路径,在该路径上qsg属于堆上的未初始化变量。其在nvme_init_sq中申请了全局堆变量io_req,其中qsg成员并未被初始化。然后在后续⾛到漏洞函数时会 使⽤该全局变量,触发路径为:nvme_process_sq
->nvme_io_cmd
->nvme_rw
->nvme_map_prp
。由于该变量是位于堆上,我们便可以使⽤堆⻛⽔之类的技术控制其内容的。
现在给定了堆未初始化Free漏洞,我们需要确定具体去释放什么对象。⼀个很直观的想法是将未初始化Free转换成UAF。这需要我们⾸先能找到⼀个⽤户控制的结构体。在使⽤之前事先填充好结构 - 将 qsg偏移对应的字段指向在堆中可控的⼀个对象。然后再利⽤漏洞触发未初始化Free,我们就能Free掉⼀个正在使⽤的对象,相当于得到了⼀个UAF。但事实上找到这样的原语并不容易。我们⾸先需要⼀个N*0xa0
⼤⼩的结构,结构体的0x40
偏移处有⼀个指针,同时该指针必须指向⼀个⽤户可控的对象。最重要的是,该对象的分配和使⽤之间应该有⼀个时间窗⼝,这样我们才能再对象被释放后再次去使⽤它。
经过多次尝试,我们并没有找到这样的原语,因为上述的限制太过于严苛。例如,我们发现 NVMe和其他传统设备以及⼀些复杂设备的结 构都不能满⾜这⼀要求。QEMU中的⼤多数结构不是⽤户可控的,可以说 QEMU的原语是相当有限的。
然⽽,我们在 Virtio gpu设备中发现了⼀个有趣的结构。该设备在 virtio_gpu_create_mapping_iov
函数中分配了⼀个地址映射表。这张表是由指针和⻓度组成的序列。该结构的⼤⼩可控,有指针成员,看起来是⼀个不错的结构。但不同的是,这⾥的指针指向guest空间,是由dma_memory_map
映射⽽来的,其代表 的是将guest物理内存映射到host的虚拟地址,通过该地址QEMU可以直接在host进程中去操纵guest内存。
由Virtio-gpu的映射表所启发,或许我们不需要在主机进程上寻找⼀个⽤户可控的读写原语。对于QEMU进程来说,客户机内存和堆内存都是map出来的⼀段地址空间,⼀定程度上来说客户机内存也可以视作是⼀种堆内存。客户机内存既映射到QEMU主机进程,也由客户机VM所控制,我们可以认为它是主机和客户机之间的共享内存。
主机进程对该物理映射区域所做的任何更改都会作⽤于客户机内存中。同时对于客户机来说,其本身是可以在任意时间任意读写⾃身内存的。那么如果我们直接在客户机空间中释放⼀个伪造的堆块 会怎么样呢?
4.2 跨虚拟机内存域攻击
由此,我们提出了跨虚拟机内存域攻击 - 根据⽤户天然地完全可控客户机内存的特性,利⽤客户机内存来构造任意读写的原语(primitive)。具体步骤如下:
1.在本环境中客户机和主机均为Ubuntu,我们便只需在客户机空间正常malloc操作就能得到伪造堆块;
2.预先填充好堆内存,其中指针指向客户机空间的伪造堆块;
3.申请未初始化的req结构体;
4.触发未初始化Free,这样QEMU进程便会把客户机中的伪造堆块添加到主机堆空间的Freelist中了;
5.由于攻击者在客户机空间天然具有读写权限,此时对该伪造堆块操作就能达到UAF的效果。
4.3 完整利用链
现在已经将未初始化Free漏洞利⽤转换成 UAF的漏洞利⽤,剩下的就是常规UAF的利⽤思路了。⾸先,我们需要找到⼀个信息泄漏绕过 ASLR。然后我们需要操纵堆布局来劫持控制流。最后执⾏任意命令,在主机上执⾏代码。
1.堆喷
⾸先,我们需要做⼀些堆喷,以获得稳定的系统堆布局。我们频繁调⽤nvme_init_sq函数来喷 射⼤量的块来清空tcache Freelist。通过这种⽅式,我们可以防⽌接下来释放的块合并到⼤块中,以得到更可靠的利⽤。
2.绕过ASLR
· Guest空间申请⼀个0x290的伪造堆块;
· Host空间申请Virtio-gpu的映射表,其中对应的指针指向伪造堆块;
· 释放映射表,在堆中留下预先设置好的状态;
· 申请io_req结构体,其中未初始化的域正好指向伪造堆块;
· 利⽤漏洞释放掉伪造堆块,主机将把它当作⼀个正常的堆块,并将其加⼊tcache bins中;
· 释放的伪造堆块中将留下主机的堆地址,在客户机中得到堆地址泄漏;
· 在主机中再次申请⼀个0x290的映射表,这块表将会被分配到客户机中(从 tcache bins头部取出);
· 客户机再次读该堆块得到physical map地址泄漏;
· nvme_init_sq中存在分配timer的原语,我们重复上述步骤将QEMU timer分配到客户机空间中;
· 客户机读timer中的cb函数指针得到 QEMU binary地址泄漏。
3.劫持控制流
· 根据QEMU binary地址,计算得system函数偏移;
· 修改timer的cb函数指针为system地址,opaque 参数为“;gnome-calculator”;
· 运⾏定时器,控制RIP。
05 总结
先前的QEMU逃逸相⽐,Scavenger有三点不同之处。在攻击⾯上,先前的逃逸漏洞CVE-2020-14364位于USB模块,CVE-2019-14378和 CVE-2019-14835位于Slirp模块,CVE-2019-5049位于AMD ATIDXX64.DLL driver,⽽ Scavenger位于 NVMe存储设备。
在漏洞类型上,⼏乎所有已知的漏洞是由于缓冲区溢出或者UAF引起的,然⽽Scavenger是错误处理代码中的未初始化Free漏洞,这需要在漏洞利⽤⽅⾯有更多的直觉和技巧。在漏洞利⽤技术上,现有的利⽤技术⼏乎都是试图在主机进程上找到可控数据,并构造攻击原语,如任意读写。相反,我们利⽤客户机内存来辅助主机的内存布局,通过跨 hypervisor域操纵内存,为我们提供了 读/写原语。
跨虚拟机内存域攻击为我们提供了⼀个全新的攻击维度。这种跨虚拟机内存域攻击技术是通⽤的,只要攻击者有漏洞可以任意控制要Free的对象,那么攻击者就可以达到远程代码执⾏(RCE),实现虚拟机逃逸。在本环境中主机和客户机均为 Ubuntu,这为我们伪造堆块带来了便利,因为 Linux中的基本堆块没有加密。但是如果堆头是加密的,攻击难度就会更⼤⼀些,例如在 Windows中。我们相信这种攻击也会影响其他Hypervisor程序,如 VirtualBox、VMware。
以上是 【Black Hat Asia 系列分享】清道夫:误用“错误处理代码”导致的 QEMU/KVM 逃逸 的全部内容, 来源链接: utcz.com/p/199960.html