在这部分,我们合并了这上述两个漏洞,用来实现虚拟机逃逸并获得QEMU的特权实现在宿主机上运行代码。
首先,我们利用 CVE-2015-5165 来重建 QEMU 的结构。更确切的说,利用该漏洞获得以下地址来实现ASLR的绕道:
用户物理内存基地址。在我们的漏洞利用中,需要定位用户并获得其在 QEMU 虚拟地址空间中的准确地址。
.text 字段的基地址。这是为了获得qemu_set_irq()函数的地址。
.plt 字段的基地址。这是为了确定一些函数的地址,比如生成我们恶意代码的fork() 和 execv() 函数。还需要 mprotect() 函数来修改用户物理地址。记得用户的物理地址是不可执行的。
如第 4 部分所示,我们已经控制了 %rip 寄存器。为了让 QEMU 在特定地址处泄露,我们需要用一个指向假 IRQState 结构体的地址指针来泄露 PCNET 缓冲区,该结构调用了一个我们选择的函数。
首先,可以尝试构建一个假IRQState结构体调用system()。然而,有时调用会失败,因为一些QEMU内存映射不通过fork()调用。更确切的说即映射的物理内存有 MADV_DONTFORK参数。
调用execv()也没用,因为我们失去了对用户机器的控制。
还要注意可以连接一些泄露的IRQState来构造恶意代码调用许多函数,因为qemu_set_irq()被PCNET设备仿真器多次调用。然而,我们发现更方便实用的方法是,设置恶意代码所在页内存的PROT_EXEC参数之后执行恶意代码。
我们的方法是构造两个假IRQState结构体。第一个调用mprotect()。第二个调用恶意代码,首先取消MADV_DONTFORK参数,然后在用户和主机之间互动。
如之前所述,当调用qemu_set_irq()时,输入两个参数:irq(IRQstate结构体的指针)和level(IRQ level),然后调用如下处理程序:
如之前所述,我们只能控制前两个参数。所以怎样调用mprotect()这样有三个参数的函数呢?
为了解决这个问题,我们让qemu_set_irq()先调用自己的两个参数:
- irq:指向假IRQState结构体的指针,设置处理程序指针指向mprotect()函数。
- level:设置mprotect参数为PROT_READ | PROT_WRITE | PROT_EXEC
这通过构造两个假IRQState结构体来实现,代码段如下:
当溢出发生后,qemu_set_irq()被一个假的处理程序调用,该处理程序在调整level参数为7(要求mprotect参数)后调用mprotect。
现在内存可以执行了,我们可以通过将第一个IRQState的处理程序重写为我们的恶意代码地址,把控制权转交给交互式shell。
好了,我们可以简单的写一个基本的shellcode,构建一个远程控制端口的shell并从一个单独的机器上连接这个shell。这是个好的方案,但是我们可以做的更好来逃过防火墙限制。我们利用在用户和主机之间的共享内存来构建一个bindshell。
利用QEMU的漏洞是很巧妙的,因为我们在用户内存上写的代码已经在QEMU的进程内存中可用。所以不需要再注入shellcode。更好的,我们可以共享代码并让它运行在用户机和连接的主机上。
我们构造了两个循环缓冲区(输入和输出)并给这些共享内存空间提供有自旋锁的读/写方法。在主机上,我们运行一个shellcode,在一个单独的进程中复制了其stdin和stdout文件后执行/bin/sh的shell。我们还创建爱你了两个线程。第一个线程从共享内存中读命令并通过一个管道传递给shel。第二个线程读取shell的输出(通过另一个管道)然后写入共享内存。
这两个线程还分别实例化了用户机,一个写入用户输入的命令到专用的共享内存,一个输出从第二个循环缓冲读取的结果到stdout。
注意在我们的漏洞利用中,有第三个线程(和一个专用共享区域)来处理stderr输出。
这部分我们列出了虚拟机逃逸(vm-escape.c)用到的主要结构体和函数。
注入的payload结构体定义如下:
fake_irq是与假IRQState结构体一对的,负责调用mprotect()并改变配置所在页的页保护。
shared_data结构体用来传递数据到主shellcode。
got 结构体是一个全局变量偏移表。它包括shellcode运行需要的主要函数的地址。这些函数的地址从内存泄露中得到。
主 shellcode 定义为如下函数:
shellcode首先检查shared_data->done参数以避免多次执行(记住用来转移控制权给shellcode的qemu_set_irq参数会被 QEMU 代码多次调用)。
shellcode通过将shared_data->addr指向物理内存来调用 madvise()。必须取消MADV_DONTFORK标记来通过调用fork()保护内存映射。
shellcode创建一个子进程负责启动一个shell(“/bin/sh”)。父进程启动一个线程使用共享内存区域来从用户机传递Shell命令到连接的主机,然后将这些命令的结果写会到用户机,父线程和子进程之间通过管道相互通信。
如下所示,共享的内存区域包括一个循环缓冲区,该缓冲区通过sm_read()和sm_write()原语进行访问:
这两个原语被以下线程函数使用。第一个函数从共享内存区域读取数据并写入文件寄存器。第二个函数从文件寄存器中读取数据并写入共享内存区。
注意这些函数的代码共享于主机和用户机。这些线程还在用户机上实例化来读取用户输入的命令并复制到专用共享内存区域(在内存中),并将这些命令运行的结果写入对应的共享内存区域(输出和err 共享内存)。
之前部分讨论过的计算过程表明,共享内存和进程/线程会在用户机和主机上启动。
该逃逸过程在QEMU的一个脆弱版本4.9.2上实现,通过Gcc的4.9.2版本构建。为了在特定的QEMU版本上也能实现,我们提供一个Shell script(build-exploit.sh)将会输出一个需要的偏移地址的C头文件:
注意,该逃逸过程有时是不能实现的。在我们的测试环境中(Debian 7,3.16内核,x_86_64),我们有十分之一的失败率。大多数的失败,都因无用的泄露的数据而无法重建QEMU的内存泄露。
该逃逸过程不能在没有CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE标记的linux内核上实现。在这种情况下,QEMU二进制(由 -fPIE默认编译)映射到一个如下所示的单独的地址空间:
结论是,我们的4字节溢出不适合irq指针的间接引用(最初位于堆的0x55xxxxxxxxxx处),所以它会指向我们的假IRQState结构体(注入到0x7fxxxxxxxxxx处)。
本文中,我们阐述了QEMU网卡设备模拟器上的两种漏洞利用。结合使用这两种漏洞可以实现虚拟机逃逸并在宿主机上执行代码。
在研究过程中,我们测试虚拟机超过1000次。用一个复杂的shellcode给每个进程产生大量的线程,实验不成功是让人沮丧的。所以,我们希望,已经提供了足够的技术细节和通用技术,可以用在将来的QEMU开发中。
我们要感谢 Pierre-Sylvain Desse 的精辟建议。感谢 coldshell 和 Kevin Schouteeten帮助我们在多种环境中测试。
还要感谢Nelson Elhage在虚拟机逃逸方面所做的工作。
感谢Phrack Staff的对修改文章和代码的建议。
[1] http://venom.crowdstrike.com
[2] media.blackhat.com/bh-us-11/Elhage/BH_US_11_Elhage_Virtunoid_WP.pdf
[3] https://github.com/nelhage/virtunoid/blob/master/virtunoid.c
[4] http://lettieri.iet.unipi.it/virtualization/2014/Vtx.pdf
[5] https://www.kernel.org/doc/Documentation/vm/pagemap.txt
[6] https://blog.affien.com/archives/2005/07/15/reversing-crc/
本文由 看雪翻译小组 Green奇 编译,来源 Mehdi Talbi和Paul Fariello@phrack
本系列到此结束了,如果你喜欢的话,不要忘记点个赞哦!
热门阅读文章:
更多优秀文章,长按下方二维码,“关注看雪学院公众号”查看!
看雪论坛:http://bbs.pediy.com/
微信公众号 ID:ikanxue
微博:看雪安全
投稿、合作:www.kanxue.com