1 Intro
如今,虚拟机已大量部署以供个人使用或在企业细分市场中使用。 网络安全供应商使用不同的VM在受控和受限的环境中分析恶意软件。 一个自然的问题出现了:恶意软件能否从虚拟机中逃脱并在主机上执行代码?
2015年,来自CrowdStrike的Jason Geffner报告了QEMU中的一个严重错误(CVE-2015-3456),该错误影响了虚拟软盘驱动器代码,这可能使攻击者从VM逃脱到主机。 此漏洞在netsec社区中引起了极大的关注,可能是因为它有一个专用名(VENOM),这并不是第一个此类漏洞。
2011年,Nelson Elhage在Blackhat 报告并成功利用了QEMU模拟PCI设备热插拔中的漏洞。
2016年,来自奇虎360的刘旭和王胜平在HITB 2016上展示了对KVM / QEMU的成功利用。 他们利用了两个不同的网卡设备仿真器模型RTL8139和PCNET中存在的两个漏洞(CVE-2015-5165和CVE-2015-7504)。 在他们的演讲中,他们概述了在主机上执行代码的主要步骤,但没有提供任何利用,也没有提供再现它的技术细节。
在本文中,我们提供了对CVE-2015-5165(一个内存泄漏漏洞)和CVE-2015-7504(一个基于堆的溢出漏洞)的深入分析,以及可利用的漏洞。 这两个漏洞的结合可让您从VM突围并在目标主机上执行代码。
我们讨论了技术细节,以利用QEMU的网卡设备仿真中的漏洞,并提供可以重新使用以利用QEMU未来错误的通用技术。 例如,利用共享内存区域和共享代码的交互式绑定外壳。
2 KVM/QEMU Overview
KVM(Kernal-based Virtual Machine,基于内核的虚拟机)是一个内核模块,可为用户空间程序提供完整的虚拟化基础架构。 它允许一个人运行多个运行未修改的Linux或Windows映像的虚拟机。
KVM的用户空间组件包含在主线QEMU(快速仿真器)中,该QEMU特别处理设备仿真。
2.1 Workspace Environment
为了使那些想使用本文中给出的示例代码的人更轻松,我们在此处提供了重现我们的开发环境的主要步骤。
由于我们定位的漏洞已经修复,因此我们需要签出QEMU存储库的源,并切换到这些漏洞的修复之前的提交。 然后,我们仅为目标x86_64配置QEMU并启用调试,在我们的测试环境中,我们使用Gcc的4.9.2版构建QEMU:
1 | $ git clone git://git.qemu-project.org/qemu.git |
使用qemu-img来生成一个qcow2系统文件
1 | **`$`**` ./qemu-img create -f qcow2 ubuntu.qcow2 20G` |
之后首先通过qemu-system-x86_64完成对qcow2系统文件中系统的安装,需要用-cdrom对iso镜像文件进行加载
1 | $ ./x86_64-softmmu/qemu-system-x86_64 -enable-kvm -m 2048 -hda ./ubuntu.qcow2 -cdrom\ |
安装完成后就获得了一个有系统的qcow2文件,我们分配2GB的内存并创建两个网络接口卡:RTL8139和PCNET,同时创建tap接口连接虚拟机和主机:
1 | ✗ sudo tunctl -t tap0 -u `whoami` |
使用vncviewer连接qemu
1 | `apt-get install xvnc4viewer` |
2.2 QEMU Memory Layout
分配给guest虚拟机的物理内存实际上是QEMU虚拟地址空间中mmapp专用的区域。 重要的是要注意,分配guest的物理内存时未启用PROT_EXEC标志。
下图说明了来宾的内存和主机的内存如何共存。
1 | Guest' processes |
此外,QEMU为BIOS和ROM保留了一个内存区域。 这些映射在QEMU映射文件中可用:
1 | ✗ cat /proc/36220/maps |
有关虚拟化环境中内存管理的更详细说明,请参见:http://lettieri.iet.unipi.it/virtualization/2014/Vtx.pdf
2.3 Address Translation
在QEMU中存在两个翻译层:Guest Virtual Address → Guest Physical Address → Host Virtual Address
- 从Guest虚拟地址到Guest物理地址。 在我们的利用中,我们需要配置需要DMA访问的网卡设备。 例如,我们需要提供Tx / Rx缓冲区的物理地址以正确配置网卡设备。
- 从Guest物理地址到QEMU的虚拟地址空间。 在我们的攻击中,我们需要注入伪造的结构,并在QEMU的虚拟地址空间中获得其精确地址。
在x64系统上,虚拟地址由页偏移量(位0-11)和页码组成。 在linux系统上,具有CAP_SYS_ADMIN特权的用户空间进程能够使用页面映射文件(pagemap )找出虚拟地址和物理地址的映射。 页面映射文件为每个虚拟页面存储一个64位值,其中physical_address = PFN * page_size + offset
1 | **- Bits 0-54 : physical frame number if present.** |
将虚拟地址(Guest Virtual Address)转换为物理地址(Guest Physical Address)的过程包括
64wei每个页面的大小为
4096
字节,即1 << 12
;基于
/proc/pid/pagemap
可以查看进程任意 Virtual Page 的状态,包括是否被映射到物理内存以及在物理内存中的 Page Frame Number(PFN)等;pagemap
文件为每个 Virtual Page 存储64
位(即8
字节)的信息,数据格式如上。
对任意的虚拟地址
address
,基于address/4096
可以计算出该虚拟地址在pagemap
文件中的索引值,address/4096 * 8
即对应的文件偏移值,在该位置能够获取PFN信息;页内偏移对任意的虚拟地址
address
,address%4096
即虚拟地址在对应的内存页中的偏移值;根据物理内存的 PFN (physical frame number)以及页内偏移,就可以计算出对应的物理地址;
1 | `physical_address = PFN * page_size + offset |
我们依靠Nelson Elhage的代码。 下面的程序分配一个缓冲区,并用字符串“Where am I?”填充它。 并打印其物理地址:
1 | ---[ mmu.c ]--- |
静态编译好程序之后将其上传到 QEMU 虚拟机中以 root
身份执行,打印出物理地址为 0x73b17b20
在主机将gdb附加到QEMU进程,我们可以看到缓冲区位于为guest虚拟机分配的物理地址空间内。 更准确地说,输出的guest物理地址地址实际上是与guest物理内存基址的偏移量。
1 | ✗ sudo gdb qemu-system-x86_64 38140 |
3 Memory Leak Exploitation
接下来,我们将利用CVE-2015-5165(一个会影响RTL8139网卡设备仿真器的内存泄漏漏洞)来重建QEMU的内存布局。 更准确地说,我们需要泄漏
- .text段的基地址,以构建我们的shellcode
- 为Guest分配的物理内存的基地址,以便能够获得 一些虚拟结构的地址
3.1 The vulnerable Code
REALTEK网卡支持两种 接收/发送 操作模式:C模式和C +模式。 当将网卡设置为使用C +时,网卡设备仿真器会错误地计算IP数据包数据的长度,最终发送的数据量会超出数据包中实际可用的数据量。
该漏洞存在于hw/net/rtl8139.c的 rtl8139_cplus_transmit_one 函数中:
1 | /* ip packet header */ |
IP头包含两个字段hlen和ip-> ip_len,分别表示IP头的长度(考虑到不带选项的数据包,为20字节)和包括ip头的数据包的总长度。 如下面给出的代码片段末尾所示,在计算IP数据长度(ip_data_len)时,没有检查以确保 ip→ip_len >= hlen 。 由于ip_data_len字段被编码为unsigned short int,因此导致发送的数据多于发送缓冲区中实际可用的数据。
更精确地讲,ip_data_len稍后用于计算TCP数据的长度,如果该数据超过MTU的大小,则将其逐块复制到一个malloc缓冲区中:
1 | int **tcp_data_len** **= ip_data_len - tcp_hlen;** |
因此,如果我们伪造了长度损坏的畸形数据包(例如ip→ip_len = hlen-1),则可能会从QEMU的堆内存中泄漏大约64 KB。网卡设备仿真器将通过发送43个分段的数据包结束, 而不是发送单个数据包。
3.2 Setting up the Card
为了发送格式错误的数据包并读取泄漏的数据,我们需要在卡上配置Rx和Tx描述符缓冲区,并设置一些标志,以使我们的数据包流经易受攻击的代码路径。
下图显示了RTL8139寄存器。 我们将不详述所有这些内容,而是仅详述与我们的利用相关的那些内容:
1 | +---------------------------+----------------------------+ |
- TxConfig: 启用/禁用Tx标志,例如TxLoopBack(启用回送测试模式),TxCRC(不将CRC附加到Tx数据包)等。
- RxConfig: 启用/禁用Rx标志,例如AcceptBroadcast(接受广播数据包),AcceptMulticast(接受组播数据包)等。
- CpCmd: C+命令寄存器,用于启用某些功能,例如CplusRxEnd(启用接收),CplusTxEnd(启用发送)等。
- TxAddr0: Tx描述符表的物理内存地址。
- RxRingAddrLO: Rx描述符表的低32位物理内存地址。
- RxRingAddrHI: Rx描述符表的高32位物理内存地址。
- TxPoll:告诉网卡检查Tx描述符。
Rx/Tx描述符 由以下结构定义,其中buf_lo和buf_hi分别是Tx/Rx缓冲区的低32位和高32位物理存储地址。 这些地址指向保存要发送/接收的数据包的缓冲区,并且必须在页面大小边界上对齐。 变量dw0对缓冲区的大小以及其他标志(例如所有权标志)进行编码,以表示缓冲区是由网卡还是由驱动程序拥有。
1 | struct rtl8139_desc { |
网卡通过in() out()原语(来自sys/io.h)进行配置。 为此,我们需要具有CAP_SYS_RAWIO特权。 以下代码段配置了网卡并设置了一个Tx描述符。
1 | #define RTL8139_PORT 0xc000 |
3.3 Exploit
phrack随附的源代码中提供了完整的利用(cve-2015-5165.c)。( uuencode用于将二进制文件编码为纯ASCII文本,以便可以通过电子邮件发送它们。)
cve-2015-5165.c依赖qemu.h头文件中的函数偏移地址,因此首先需要通过build-exploit.sh来进行计算。
1 | ./build-exploit.sh '/home/han/VMescape/qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64' |
该漏洞利用程序在网卡上配置所需的寄存器,并设置Tx和Rx缓冲区描述符。 然后,它伪造了格式错误的IP数据包,该IP数据包的目的地址和源地址为网卡的MAC地址。 这使我们能够通过访问已配置的Rx缓冲区来读取泄漏的数据。
通过对qemu运行程序下断点,可用看到漏洞触发的过程,由于ip_len小于伪造的hlen,导致最后tcp_data_len比实际的 tcp 数据大, 多余的内存区会被拷贝到包里发送出去(网卡需要配置为loopback 口)
1 | (gdb) b rtl8139.c:2173 |
虚拟机内部的用户进程通过读取收包队列的数据包就可以知道被泄露的那块 qemu 内存区的内容。在分析泄漏的数据时,我们观察到存在多个函数指针。经过调试,发现这些函数指针都是struct ObjectProperty这个 qemu 内部结构体的数据。struct ObjectProperty 包含 11 个指针, 这里边有 4 个函数指针 get/set/resolve/release
1 | typedef struct ObjectProperty |
QEMU遵循对象模型来管理设备,内存区域等。启动时,QEMU创建多个对象并为其分配属性。 例如,以下的函数将“may-overlap”属性添加给一个内存区域对象。 此属性具有getter方法,可以检索此boolean属性的值:
1 | object_property_add_bool(OBJECT(mr), "may-overlap", |
RTL8139网卡设备仿真器在堆上保留了64 KB的空间以重组数据包。 该分配的缓冲区很可能把释放掉的object properties的内存占位了。
在我们的漏洞利用中,我们在泄漏的内存中搜索已知的对象属性。更准确地说,我们正在寻找80个字节的内存块(块大小为已释放的ObjectProperty结构),其中至少设置了一个函数指针(get, set, resolve or release)。
即使这些地址受ASLR约束,我们仍然可以猜测.text节的基地址。
0) 从 qemu-system-x86_64 二进制文件里搜索上述 4 类符号的所有静态地址, 如 property_get_bool 等符号的地址
1) 在读回来的 IP 包的数据里搜索值等于 0x60 的内存 ptr, 如果匹配到, 认为 (u64*)ptr+1 的地方就是一个潜在的 struct ObjectProperty 对象, 对应的函数是 qemu_get_leaked_chunk
2) 在 1 搜索到的内存上匹配 0 收集到的 get/set/resolve/release 这几种符号的静态地址, 匹配方式为页内偏移相等, 如果匹配到, 认为就是 struct ObjectProperty 对象, 对应的函数是 qemu_get_leaked_object_property
3) 在 2 搜索的基础上, 用 object->get/set/resolve/release 的实际地址减去静态编译里算出来的 offset, 得到 .text 加载的地址
实际上,它们的页面偏移是固定的(12个最低有效位或虚拟地址不是随机的)。 我们可以通过一些算法来获取QEMU一些有用函数的地址。 我们还可以从它们的PLT条目中导出某些LibC函数的地址,例如mprotect() 和system()。
我们还注意到,地址PHY_MEM + 0x78泄漏了几次,其中PHY_MEM是分配给该Guest的物理内存的起始地址。
总结:当前漏洞利用程序搜索泄漏的内存,并尝试解析(i).text段的基地址和(ii)物理内存的基地址。
3.4 遇到的几个问题
- phrack提供的build-exploit.sh, 它是一个工具脚本,用来获取一些符号的(相对)地址。原始的 build-exploit.sh 获取 plt 段是通过下面的命令行:
plt**=**$(readelf -S $binary | grep plt | tail -n 1 | awk '{print $2}')
这样获取到的是 .plt.got 段,在我的环境里, mprotect 等系统函数符号没有在 .plt.got 这个段,而是在 .plt 这个段。因此替换如下:
1 | #plt=$(readelf -S $binary | grep plt | tail -n 1 | awk '{print $2}') |
- Phrack 文章提供的 Exploit 代码中搜索的地址是PHY_MEM + 0x78,但实际上并不固定为0x78,更通用的做法是统计泄露的数据中出现的
uint64_t
类型的数据0x00007FXXYYZZZZZZ
,其中7FXXYY
出现次数最多的数据,就是 QEMU 虚拟机物理内存的结束地址;修改之后成功获得物理地址
通过 gdb 调试验证结果正确性:
ref
http://jiayy.me/2019/04/15/CVE-2015-5165-7504/
http://jiayy.me/2019/04/15/CVE-2015-5165-7504/#cve-2015-5165-exp
https://programlife.net/2020/06/30/cve-2015-5165-qemu-rtl8139-vulnerability-analysis/