【漏洞分析】前往黑暗之门!Debugee in QEMU

http://p1.qhimg.com/t01a86dbbe57655ab47.jpg

作者:k0shl

预估稿费:600RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿

 

0x00 前言

好久没给安全客投稿了,最近刚刚接触漏洞挖掘,一直在读一些经典的fuzzer源码,同时也开始接触虚拟化逃逸这块的内容,在这个时候正巧碰到了两个非常经典的漏洞利用,相信很多小伙伴也已经看过了,phrack前段时间刚刚更新了关于这个漏洞的详细利用过程。

我后来对这个漏洞进行了动态调试,并且通过phrack的paper恶补了一些关于虚拟机的工作原理,Guest OS和Host OS之间的一些知识。

http://p0.qhimg.com/t014528f8cdab6f770e.png

在调试的过程中,我愈发觉得这两个漏洞作为前往黑暗之门入门再合适不过,通过对两个漏洞的分析和利用的调试,可以熟悉这类虚拟化漏洞的调试原理。今天,我将和大家分享QEMU虚拟化逃逸的调试环境搭建,关于CVE-2015-5165和CVE-2015-7504漏洞动态调试分析,以及补丁对比。

在此之前,我默认阅读此文的小伙伴们已经看过了phrack.org关于VM Escape Case Study的文章,并且已经了解虚拟机工作的基本原理,包括但不限于内存管理机制,REALTEK网卡、PCNET网卡的数据包结构,Tx、Rx缓冲区等等。关于phrack.org的文章以及看雪翻译版分析文章的链接我将在文末给出。下面我们一起出发前往黑暗之门吧!

http://p7.qhimg.com/t01c22f7fd9d1301ab3.png

 

0x01 QEMU环境搭建

在调试QEMU虚拟化逃逸漏洞之前,我们需要搭建虚拟化逃逸的环境,首先通过git clone下载QEMU,并且通过git check设定分支(如果要调试以前版本的话)。

 

$ git clone git://git.qemu-project.org/qemu.git
$ cd qemu
$ mkdir -p bin/debug/native
$ cd bin/debug/native
$ ../../../configure --target-list=x86_64-softmmu --enable-debug --disable-werror
$ make

在make的时候,Host OS会需要一些库的安装,可以通过apt-get来下载安装,比如zlib,glib-2.22等(其中glib-2.22也需要一些依赖,同时需要去网站下载,网站地址:http://ftp.gnome.org/pub/gnome/sources/glib/2.22/ )。

安装完毕后,会在/path/to/qemu/bin/debug/native/下生成一个x86_64-softmmu目录,在此之前,需要安装一个qcow2的系统文件,所以需要通过qemu-img来生成一个qcow2系统文件。

$ qemu-img create -f qcow2 ubuntu.qcow2 20G

之后首先通过qemu-system-x86_64完成对qcow2系统文件中系统的安装,需要用-cdrom对iso镜像文件进行加载。同时,需要安装vncviewer,这样可以通过vncviewer对qemu启动的vnc端口进行连接。

 

$ qemu-system-x86_64 -enable-kvm -m 2048 -hda /path/to/ubuntu.qcow2 -cdrom /path/to/ubuntu.iso
$ apt-get install xvnc4viewer

通过vnc连接qemu之后,根据镜像文件提示进行安装,这里推荐还是用server.iso,因为安装比较快,用desktop的话可能会稍微卡顿一些,安装完成后就获得了一个有系统的qcow2文件,之后就可以用包含漏洞的rlt8139和pcnet网卡硬件启动了。

$ ./qemu-system-x86_64 -enable-kvm -m 2048 -display vnc=:89 -netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 -netdev user,id=t1, -device pcnet,netdev=t1,id=nic1 -drive  file=/path/to/ubuntu.qcow2,format=qcow2,if=ide,cache=writeback

启动之后,这里我为了省事,直接用NAT的方法共享宿主机网络,然后在本地通过SimpleHTTPServer建立一个简单的HTTP Server,通过wget方法获得两个漏洞的PoC,这两个漏洞PoC可以通过gcc -static的方法在本地编译后直接上传,然后运行即可。

之后在宿主机通过ps -ef|grep qemu找到qemu的启动进程,通过gdb attach pid的方法附加,按c继续运行就可以了,可以通过b function的方法下断点,方便跟踪调试。

http://p4.qhimg.com/t0109633df0ef38510d.jpg

 

0x02 CVE-2015-5165漏洞分析

CVE-2015-5165是一个内存泄露漏洞,由于对于ip->ip_len和hlen长度大小没有进行控制,导致两者相减计算为负时,由于ip_data_len变量定义是unsigned类型,导致这个值会非常大,从而产生内存泄露。漏洞文件在/path/to/qemu/hw/net/rtl8139.c。

首先根据漏洞描述,漏洞发生在rtl8139_cplus_transmit_one函数中,通过b rtl8139_cplus_transmit_one的方法在该函数下断点,之后运行PoC,命中函数后,首先函数会传入一个RTL8139State结构体变量。继续单步跟踪,会执行到一处if语句,这里会比较当前数据包头部是否是IPV4的头部。

 

gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x4 
[-------------------------------------code-------------------------------------]
   0x55b25db58480 <rtl8139_cplus_transmit_one+1854>:shr    al,0x4
   0x55b25db58483 <rtl8139_cplus_transmit_one+1857>:movzx  eax,al
   0x55b25db58486 <rtl8139_cplus_transmit_one+1860>:and    eax,0xf
=> 0x55b25db58489 <rtl8139_cplus_transmit_one+1863>:cmp    eax,0x4
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db584892173                if (IP_HEADER_VERSION(ip) != IP_HEADER_VERSION_4) {

可见此时确实是IPv4的结构,随后进入if语句的代码逻辑,在其中会调用be16_to_cpu对ip->ip_len进行转换,ip->ip_len的长度为0x1300,转换后长度为0x13。

 

[----------------------------------registers-----------------------------------]
RAX: 0x1300 
RDI: 0x1300 //ip->ip_len
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55b25db584f7 <rtl8139_cplus_transmit_one+1973>:
    movzx  eax,WORD PTR [rax+0x2]
   0x55b25db584fb <rtl8139_cplus_transmit_one+1977>:movzx  eax,ax
   0x55b25db584fe <rtl8139_cplus_transmit_one+1980>:mov    edi,eax
=> 0x55b25db58500 <rtl8139_cplus_transmit_one+1982>:
    call   0x55b25db54a37 <be16_to_cpu>
   0x55b25db58505 <rtl8139_cplus_transmit_one+1987>:mov    edx,eax
Guessed arguments:
arg[0]: 0x1300 //ip->ip_len=0x1300
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db585002181                    ip_data_len = be16_to_cpu(ip->ip_len) - hlen;
      
gdb-peda$ ni
[----------------------------------registers-----------------------------------]
RAX: 0x13 //经过be16_to_cpu()之后返回值为0x13
[-------------------------------------code-------------------------------------]
   0x55b25db584fb <rtl8139_cplus_transmit_one+1977>:movzx  eax,ax
   0x55b25db584fe <rtl8139_cplus_transmit_one+1980>:mov    edi,eax
   0x55b25db58500 <rtl8139_cplus_transmit_one+1982>:
    call   0x55b25db54a37 <be16_to_cpu>
=> 0x55b25db58505 <rtl8139_cplus_transmit_one+1987>:mov    edx,eax
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db585052181                    ip_data_len = be16_to_cpu(ip->ip_len) - hlen;

转换后,会将转换后的值和hlen相减。

 

gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x14 //hlen=0x14
RDX: 0x13 //be16_to_cpu(ip->ip_len)=0x13
[-------------------------------------code-------------------------------------]
   0x55b25db58500 <rtl8139_cplus_transmit_one+1982>:
    call   0x55b25db54a37 <be16_to_cpu>
   0x55b25db58505 <rtl8139_cplus_transmit_one+1987>:mov    edx,eax
   0x55b25db58507 <rtl8139_cplus_transmit_one+1989>:
    mov    eax,DWORD PTR [rbp-0x13c]
=> 0x55b25db5850d <rtl8139_cplus_transmit_one+1995>:sub    edx,eax
   0x55b25db5850f <rtl8139_cplus_transmit_one+1997>:mov    eax,edx
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db5850d2181                    ip_data_len = be16_to_cpu(ip->ip_len) - hlen;
gdb-peda$ si
[----------------------------------registers-----------------------------------]
RDX: 0xffffffff //相减之后为0xffffffff,这个变量是一个unsigned类型,此值极大
[-------------------------------------code-------------------------------------]
   0x55b25db5850d <rtl8139_cplus_transmit_one+1995>:sub    edx,eax
=> 0x55b25db5850f <rtl8139_cplus_transmit_one+1997>:mov    eax,edx
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db5850f2181                    ip_data_len = be16_to_cpu(ip->ip_len) - hlen;

相减后,这个值为0xffffffff,而这个值是一个16位无符号数,也就是是一个极大值0xffff,我们可以通过源码看到关于这个变量的定义。

 

uint16_t ip_data_len = 0;
……
ip_data_len = be16_to_cpu(ip->ip_len) - hlen;

接下来继续单步跟踪,会发现ip_data_len这个极大值会被用来计算tcp_data_len,也就是tcp数据的长度,随后还有一个tcp_chunk_size,这个chunk_size限制了一个数据包的最大值,当tcp数据的长度超过chunk_size的时候,则会分批发送。

 

//计算tcp_data_len
gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0xffff //ip_data_len
[-------------------------------------code-------------------------------------]
=> 0x55b25db586c2 <rtl8139_cplus_transmit_one+2432>:
    sub    eax,DWORD PTR [rbp-0x10c]//hlen的大小是0x14
   0x55b25db586c8 <rtl8139_cplus_transmit_one+2438>:
    mov    DWORD PTR [rbp-0x108],eax
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db586c22231                    int tcp_data_len = ip_data_len - tcp_hlen;
gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0xffeb //相减后tcp_data_len长度是0xffeb
[-------------------------------------code-------------------------------------]
   0x55b25db586c2 <rtl8139_cplus_transmit_one+2432>:
    sub    eax,DWORD PTR [rbp-0x10c]
=> 0x55b25db586c8 <rtl8139_cplus_transmit_one+2438>:
    mov    DWORD PTR [rbp-0x108],eax
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db586c82231                    int tcp_data_len = ip_data_len - tcp_hlen;
//计算chunk_size = 0x5b4      
gdb-peda$ ni
[----------------------------------registers-----------------------------------]
RAX: 0x5b4 
[-------------------------------------code-------------------------------------]
   0x55b25db586ce <rtl8139_cplus_transmit_one+2444>:mov    eax,0x5dc
   0x55b25db586d3 <rtl8139_cplus_transmit_one+2449>:
    sub    eax,DWORD PTR [rbp-0x13c]//ETH_MTU-hlen
   0x55b25db586d9 <rtl8139_cplus_transmit_one+2455>:
    sub    eax,DWORD PTR [rbp-0x10c]//-tcp_hlen
=> 0x55b25db586df <rtl8139_cplus_transmit_one+2461>:
    mov    DWORD PTR [rbp-0x104],eax
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db586df2232                    int tcp_chunk_size = ETH_MTU - hlen - tcp_hlen;

随后rtl8139_cplus_transmit_one函数会进入一个for循环处理,这个for循环会计算每一个chunk_size是达到整个tcp_data_len的末尾,如果没有则处理整个chunk_size并发送。

 

int tcp_data_len = ip_data_len - tcp_hlen;//tcp_data_len = 0xffff-0x14=0xffeb
int tcp_chunk_size = ETH_MTU - hlen - tcp_hlen;//chunk_size = 0x5b4
for (tcp_send_offset = 0; tcp_send_offset < tcp_data_len; tcp_send_offset += tcp_chunk_size)//0xffeb/0x5b4 = 43
{
                        uint16_t chunk_size = tcp_chunk_size;//0x5b4
                        /* check if this is the last frame */
                        if (tcp_send_offset + tcp_chunk_size >= tcp_data_len)//if packet length > tcp data length
                        {
                            is_last_frame = 1;
                            chunk_size = tcp_data_len - tcp_send_offset;
                        }
                        DPRINTF("+++ C+ mode TSO TCP seqno %08xn",
                            be32_to_cpu(p_tcp_hdr->th_seq));
                        /* add 4 TCP pseudoheader fields */
                        /* copy IP source and destination fields */
                        memcpy(data_to_checksum, saved_ip_header + 12, 8);
                        DPRINTF("+++ C+ mode TSO calculating TCP checksum for "
                            "packet with %d bytes datan", tcp_hlen +
                            chunk_size);
                        if (tcp_send_offset)
                        {
                            memcpy((uint8_t*)p_tcp_hdr + tcp_hlen, (uint8_t*)p_tcp_hdr + tcp_hlen + tcp_send_offset, chunk_size);//disclouse key!!!p_tcp_hdr = ip_header   p_tcp_hdr+tcp_hlen = data section
                            //
                        }
                        /* keep PUSH and FIN flags only for the last frame */
                        if (!is_last_frame)
                        {
                            TCP_HEADER_CLEAR_FLAGS(p_tcp_hdr, TCP_FLAG_PUSH|TCP_FLAG_FIN);
                        }
                        /* recalculate TCP checksum */
                        ip_pseudo_header *p_tcpip_hdr = (ip_pseudo_header *)data_to_checksum;
                        p_tcpip_hdr->zeros      = 0;
                        p_tcpip_hdr->ip_proto   = IP_PROTO_TCP;
                        p_tcpip_hdr->ip_payload = cpu_to_be16(tcp_hlen + chunk_size);
                        p_tcp_hdr->th_sum = 0;
                        int tcp_checksum = ip_checksum(data_to_checksum, tcp_hlen + chunk_size + 12);
                        DPRINTF("+++ C+ mode TSO TCP checksum %04xn",
                            tcp_checksum);
                        p_tcp_hdr->th_sum = tcp_checksum;
                        /* restore IP header */
                        memcpy(eth_payload_data, saved_ip_header, hlen);
                        /* set IP data length and recalculate IP checksum */
                        ip->ip_len = cpu_to_be16(hlen + tcp_hlen + chunk_size);
                        /* increment IP id for subsequent frames */
                        ip->ip_id = cpu_to_be16(tcp_send_offset/tcp_chunk_size + be16_to_cpu(ip->ip_id));
                        ip->ip_sum = 0;
                        ip->ip_sum = ip_checksum(eth_payload_data, hlen);
                        DPRINTF("+++ C+ mode TSO IP header len=%d "
                            "checksum=%04xn", hlen, ip->ip_sum);
                        int tso_send_size = ETH_HLEN + hlen + tcp_hlen + chunk_size;
                        DPRINTF("+++ C+ mode TSO transferring packet size "
                            "%dn", tso_send_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;
                    }

在for循环中,会有一处if语句判断tcp_send_offset是否为0,当tcp_send_offset不为0时,会执行memcpy操作,拷贝目标缓冲区进入待发送的tcp_buffer中,这个memcpy拷贝的就是buffer,而每轮都会拷贝一个chunk_size,之后再加一个chunk_size,这样就会超过原本buffer的大小,而考到缓冲区外的空间,造成内存泄露。首先来看memcpy第一回合。

 

gdb-peda$ si
[----------------------------------registers-----------------------------------]
RDX: 0x5b4 //size
RSI: 0x7f49f003adfa --> 0x9000000000000000//src 
RDI: 0x7f49f003a846 --> 0x0 //dst
[-------------------------------------code-------------------------------------]
   0x55b25db5880e <rtl8139_cplus_transmit_one+2764>:add    rcx,rdx
   0x55b25db58811 <rtl8139_cplus_transmit_one+2767>:mov    rdx,rax
   0x55b25db58814 <rtl8139_cplus_transmit_one+2770>:mov    rdi,rcx
=> 0x55b25db58817 <rtl8139_cplus_transmit_one+2773>:call   0x55b25d9361a8//memcpy
Guessed arguments:
arg[0]: 0x7f49f003a846 --> 0x0 
arg[1]: 0x7f49f003adfa --> 0x9000000000000000 
arg[2]: 0x5b4 
arg[3]: 0x7f49f003a846 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 3 "qemu-system-x86" hit Breakpoint 4, 0x000055b25db58817 in rtl8139_cplus_transmit_one (s=0x55b26083d430)
    at /home/sh1/Desktop/qemu/hw/net/rtl8139.c:2267
2267                            memcpy((uint8_t*)p_tcp_hdr + tcp_hlen, (uint8_t*)p_tcp_hdr + tcp_hlen + tcp_send_offset, chunk_size);
gdb-peda$ x/50x 0x7f49f003adfa//src
0x7f49f003adfa:0x90000000000000000x100000007f49e7c7
0x7f49f003ae0a:0xc0000000000000000x100000007f49e456
0x7f49f003ae1a:0x80000000000000000x100000007f49ef8b
gdb-peda$ ni
[----------------------------------registers-----------------------------------]
RAX: 0x7f49f003a846 --> 0x9000000000000000 
[-------------------------------------code-------------------------------------]
   0x55b25db58811 <rtl8139_cplus_transmit_one+2767>:mov    rdx,rax
   0x55b25db58814 <rtl8139_cplus_transmit_one+2770>:mov    rdi,rcx
   0x55b25db58817 <rtl8139_cplus_transmit_one+2773>:call   0x55b25d9361a8
=> 0x55b25db5881c <rtl8139_cplus_transmit_one+2778>:
    cmp    DWORD PTR [rbp-0x130],0x0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
2271                        if (!is_last_frame)
gdb-peda$ x/50x 0x7f49f003a846//target dst
0x7f49f003a846:0x90000000000000000x100000007f49e7c7
0x7f49f003a856:0xc0000000000000000x100000007f49e456
0x7f49f003a866:0x80000000000000000x100000007f49ef8b
//memcpy(0x7f49f003a846,0x7f49f003adfa,lenth)

这里注意一下目前我们拷贝的缓冲区起始地址是:0x7f49f003adfa,拷贝到目标缓冲区后,单步跟踪,会发现for循环中会调用rtl8139_tansfer_frame函数将saved_buffer送回缓冲区。而saved_buffer的内容就包含了我们拷贝的内容。

 

gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x55b26083d430 --> 0x55b25f178400 --> 0x55b25f15eda0 --> 0x55b25f15ef20 --> 0x393331386c7472 ('rtl8139')
RBX: 0x1 
RCX: 0x0 
RDX: 0x5ea 
RSI: 0x7f49f003a810 --> 0x5452563412005452 
RDI: 0x55b26083d430 --> 0x55b25f178400 --> 0x55b25f15eda0 --> 0x55b25f15ef20 --> 0x393331386c7472 ('rtl8139')
[-------------------------------------code-------------------------------------]
   0x55b25db58a3a <rtl8139_cplus_transmit_one+3320>:mov    r8,rcx
   0x55b25db58a3d <rtl8139_cplus_transmit_one+3323>:mov    ecx,0x0
   0x55b25db58a42 <rtl8139_cplus_transmit_one+3328>:mov    rdi,rax
=> 0x55b25db58a45 <rtl8139_cplus_transmit_one+3331>:
    call   0x55b25db5776d <rtl8139_transfer_frame>
Guessed arguments:
arg[0]: 0x55b26083d430 --> 0x55b25f178400 --> 0x55b25f15eda0 --> 0x55b25f15ef20 --> 0x393331386c7472 ('rtl8139')
arg[1]: 0x7f49f003a810 --> 0x5452563412005452 
arg[2]: 0x5ea 
arg[3]: 0x0 
arg[4]: 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db58a452307                        rtl8139_transfer_frame(s, saved_buffer, tso_send_size,
gdb-peda$ x/50x  0x7f49f003a810//save_buffer
0x7f49f003a810:0x54525634120054520x0045000856341200
0x7f49f003a820:0x06400040aededc050xa8c0010108c0b9d3
0x7f49f003a830:0xfecaefbeadde02010x1050bebafeca72c0
0x7f49f003a840:0x00000000f015adde0xe7c7900000000000
0x7f49f003a850:0x0000100000007f490xe456c00000000000
0x7f49f003a860:0x0000100000007f490xef8b800000000000
0x7f49f003a870:0x0000100000007f490xe43ce00000000000
0x7f49f003a880:0x0000100000007f490xe369c00000000000

随后我们第二轮再次命中memcpy函数,注意一下源缓冲区的值。

 

gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x5b4 
RBX: 0x5b4 
RCX: 0x7f49f003a846 --> 0x9000000000000000 
RDX: 0x5b4 
RSI: 0x7f49f003b3ae --> 0x7f49cc0de0000000 
RDI: 0x7f49f003a846 --> 0x9000000000000000 
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55b25db5880e <rtl8139_cplus_transmit_one+2764>:add    rcx,rdx
   0x55b25db58811 <rtl8139_cplus_transmit_one+2767>:mov    rdx,rax
   0x55b25db58814 <rtl8139_cplus_transmit_one+2770>:mov    rdi,rcx
=> 0x55b25db58817 <rtl8139_cplus_transmit_one+2773>:call   0x55b25d9361a8//memcpy
Guessed arguments:
arg[0]: 0x7f49f003a846 --> 0x9000000000000000 
arg[1]: 0x7f49f003b3ae --> 0x7f49cc0de0000000 
arg[2]: 0x5b4 
arg[3]: 0x7f49f003a846 --> 0x9000000000000000 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 3 "qemu-system-x86" hit Breakpoint 4, 0x000055b25db58817 in rtl8139_cplus_transmit_one (s=0x55b26083d430)
    at /home/sh1/Desktop/qemu/hw/net/rtl8139.c:2267
2267                            memcpy((uint8_t*)p_tcp_hdr + tcp_hlen, (uint8_t*)p_tcp_hdr + tcp_hlen + tcp_send_offset, chunk_size);

这一次是 0x7f49f003b3ae – 0x7f49f003adfa = 0x5b4 确实是一个chunk的大小,如此一来,每一轮memcpy都会加上一个chunk_size,当超出了buffer,就造成了信息泄露,可以拷贝当前buffer之外的内容。而我们只需要从Rx Buffer中读取,这样就会造成信息泄露了。

http://p5.qhimg.com/t01b9404a08dc6b68e3.png

 

0x03 CVE-2015-7504漏洞分析

CVE-2015-7504是一个堆溢出漏洞,这个漏洞形成的原因涉及到一个PCNetState_st结构体,这个结构体中有一个buffer变量长度大小定义为4096,然而在PCNET网卡的pcnet_receive函数中处理buffer时会在结尾增加一个4字节的CRC校验,这时当我们对传入buffer长度控制为4096的话,4字节的CRC校验会覆盖超出4096长度的4字节位置,而这4字节正好是PCNetState_st结构体中的一个irq关键结构,进一步我们可以利用irq结构控制rip,漏洞文件在/path/to/qemu/hw/net/pcnet.c。

接下来我们在pcnet_receive函数入口下断点,函数入口处会传入PCNetState_st结构体对象,这里我筛选部分跟此漏洞有关的结构体变量。

 

gdb-peda$ p *(struct PCNetState_st*)0x55b25f34a1a0
$1 = {
  ……
  buffer = "RT002264VRT002264Vb", '00', 
  irq = 0x55b2603bc910, 
  ……
  looptest = 0x1
}

随后单步跟踪,这里首先会获取s->buffer的值。

 

//store s->buffer to src
[----------------------------------registers-----------------------------------]
RAX: 0x55b25f34a1a0 --> 0x55b2603bca00 --> 0x55b2603bca20 --> 0x55b25e13d940 --> 0x1 
[-------------------------------------code-------------------------------------]
   0x55b25db4e537 <pcnet_receive+952>:mov    WORD PTR [rax+0x212c],dx
   0x55b25db4e53e <pcnet_receive+959>:
    jmp    0x55b25db4effb <pcnet_receive+3708>
   0x55b25db4e543 <pcnet_receive+964>:mov    rax,QWORD PTR [rbp-0xa8]
=> 0x55b25db4e54a <pcnet_receive+971>:add    rax,0x2290//offset
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db4e54a1062            uint8_t *src = s->buffer;

随后会到达一处if语句判断,这里会判断looptest的值,当此值为非0值时,会进入else语句处理。

 

//looptest
[----------------------------------registers-----------------------------------]
RAX: 0x1 //s->looptest
[-------------------------------------code-------------------------------------]
   0x55b25db4e587 <pcnet_receive+1032>:mov    DWORD PTR [rbp-0xd8],0x0
   0x55b25db4e591 <pcnet_receive+1042>:mov    rax,QWORD PTR [rbp-0xa8]
   0x55b25db4e598 <pcnet_receive+1049>:mov    eax,DWORD PTR [rax+0x32b4]
=> 0x55b25db4e59e <pcnet_receive+1055>:test   eax,eax
   0x55b25db4e5a0 <pcnet_receive+1057>:
    jne    0x55b25db4e635 <pcnet_receive+1206>
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db4e59e1067            if (!s->looptest) {
//s->looptest = PCNET_LOOPTEST_CRC
[----------------------------------registers-----------------------------------]
RAX: 0x1 [-------------------------------------code-------------------------------------]
=> 0x55b25db4e645 <pcnet_receive+1222>:
    je     0x55b25db4e66c <pcnet_receive+1261>
   0x55b25db4e647 <pcnet_receive+1224>:mov    rax,QWORD PTR [rbp-0xa8]
   0x55b25db4e64e <pcnet_receive+1231>:movzx  eax,WORD PTR [rax+0x206a]
   0x55b25db4e655 <pcnet_receive+1238>:movzx  eax,ax
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db4e6451075            } else if (s->looptest == PCNET_LOOPTEST_CRC ||

随后会进入else语句处理,在else语句处理中会有一处while循环进行CRC校验。

 

 else if (s->looptest == PCNET_LOOPTEST_CRC ||
                       !CSR_DXMTFCS(s) || size < MIN_BUF_SIZE+4) {
                uint32_t fcs = ~0;
                uint8_t *p = src;
                while (p != &src[size])
                    CRC(fcs, *p++);
                *(uint32_t *)p = htonl(fcs);
                size += 4;
            }

这处循环CRC校验会处理4096大小的数据。

 

[----------------------------------registers-----------------------------------]
RAX: 0x55b25f34c430 --> 0x5452563412005452 //buffer
RBX: 0x1000//大小 
[-------------------------------------code-------------------------------------]
   0x55b25db4e66c <pcnet_receive+1261>:mov    DWORD PTR [rbp-0xd4],0xffffffff
   0x55b25db4e676 <pcnet_receive+1271>:mov    rax,QWORD PTR [rbp-0x98]
   0x55b25db4e67d <pcnet_receive+1278>:mov    QWORD PTR [rbp-0xb8],rax
=> 0x55b25db4e684 <pcnet_receive+1285>:
    jmp    0x55b25db4e6ce <pcnet_receive+1359>
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Thread 3 "qemu-system-x86" hit Breakpoint 10, pcnet_receive (
    nc=0x55b2603bca20, buf=0x55b25f34c430 "RT", size_=0x1000)
    at /home/sh1/Desktop/qemu/hw/net/pcnet.c:1080
1080                while (p != &src[size])

每一轮循环p都会自加1,循环结束后p会到0x1000的位置,随后会进行一处赋值,赋值的内容是htonl(fcs),长度是4字节,而这4字节的内容会超过s->buffer的大小,可以回头看一下之前我发的关于PCNetState_st结构体的值,在s->buffer之后跟的是irq结构。

根据之前我们跟踪对*src = s->buffer的汇编代码,我们可以看到buffer的偏移是0x2290,而buffer的长度是0x1000,buffer 的下一个变量是irq结构,buffer是0x2290 + 0x1000 = 0x3290 + 0x55b25f34a1a0 = 0x55b25f34d430

 

gdb-peda$ x/10x 0x55B25F34D400
0x55b25f34d400:0x00000000000000000x0000000000000000
0x55b25f34d410:0x00000000000000000x0000000000000000
0x55b25f34d420:0x00000000000000000xfe7193d400000000
0x55b25f34d430:0x000055b2603bc910

可以看到0x55b25f34d430位置存放的正是irq的指针(结合我之前发的结构体中irq变量的值),接下来我们来看p=htonl(fcs)赋值操作。这里fcs是可控的,我们把它的值设置为0xdeadbeef,因为是PoC仅用于验证,而真实利用,请参考phrack文中的利用方法。

 

[----------------------------------registers-----------------------------------]
RAX: 0xdeadbeef //eax的值是deadbeef
RBX: 0x1000 
[-------------------------------------code-------------------------------------]
   0x55b25db4e6f2 <pcnet_receive+1395>:call   0x55b25d936078
=> 0x55b25db4e6f7 <pcnet_receive+1400>:mov    edx,eax
   0x55b25db4e6f9 <pcnet_receive+1402>:mov    rax,QWORD PTR [rbp-0xb8]
 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db4e6f71082                *(uint32_t *)p = htonl(fcs);
[----------------------------------registers-----------------------------------]
RAX: 0x55b25f34d430 --> 0x55b2603bc910 --> 0x55b25f18a3f0 --> 0x55b25f1564a0 --> 0x55b25f156620 --> 0x717269 ('irq')//目标地址
RDX: 0xdeadbeef //拷贝内容
[-------------------------------------code-------------------------------------]
=> 0x55b25db4e700 <pcnet_receive+1409>:mov    DWORD PTR [rax],edx
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055b25db4e7001082                *(uint32_t *)p = htonl(fcs);
gdb-peda$ x/10x 0x55b25f34d430//拷贝前
0x55b25f34d430:0x000055b2603bc9100x000055b25db4be11
0x55b25f34d440:0x000055b25db4bdd90x000055b25f349920
0x55b25f34d450:0x00000001000000010x000055b25f182850
0x55b25f34d460:0x00000000000000000x000055b25ff0d760
0x55b25f34d470:0x000055b2603bc7300x0000000000000001
gdb-peda$ si//拷贝后
[----------------------------------registers-----------------------------------]
RAX: 0x55b25f34d430 --> 0x55b2deadbeef 
RDX: 0xdeadbeef [-------------------------------------code-------------------------------------]
   0x55b25db4e700 <pcnet_receive+1409>:mov    DWORD PTR [rax],edx
=> 0x55b25db4e702 <pcnet_receive+1411>:add    DWORD PTR [rbp-0xe4],0x4
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
1083                size += 4;
gdb-peda$ x/10x 0x55b25f34d430//拷贝结束后deadbeef覆盖了irq结构
0x55b25f34d430:0x000055b2deadbeef0x000055b25db4be11

当我们覆盖irq结构后,在pcnet_receive函数结束时更新irq结构,调用关系是pcnet_receive()->pcnet_update_irq()->qemu_set_irq()

 

RDI: 0x55b2deadbeef 
   0x55b25db4d31d <pcnet_update_irq+497>:mov    esi,edx
   0x55b25db4d31f <pcnet_update_irq+499>:mov    rdi,rax
=> 0x55b25db4d322 <pcnet_update_irq+502>:
    call   0x55b25daf6c86 <qemu_set_irq>

这时,irq的值已经被覆盖了,我们跟入qemu_set_irq,这个函数在/path/to/qemu/hw/core/irq.c中。

 

gdb-peda$ disas qemu_set_irq
Dump of assembler code for function qemu_set_irq:
   0x000055b25daf6c86 <+0>:push   rbp
   0x000055b25daf6c87 <+1>:mov    rbp,rsp
   0x000055b25daf6c8a <+4>:sub    rsp,0x10
   0x000055b25daf6c8e <+8>:mov    QWORD PTR [rbp-0x8],rdi
   0x000055b25daf6c92 <+12>:mov    DWORD PTR [rbp-0xc],esi
   0x000055b25daf6c95 <+15>:cmp    QWORD PTR [rbp-0x8],0x0
   0x000055b25daf6c9a <+20>:je     0x55b25daf6cbd <qemu_set_irq+55>
=> 0x000055b25daf6c9c <+22>:mov    rax,QWORD PTR [rbp-0x8]
   0x000055b25daf6ca0 <+26>:mov    rax,QWORD PTR [rax+0x30]
   0x000055b25daf6ca4 <+30>:mov    rdx,QWORD PTR [rbp-0x8]
   0x000055b25daf6ca8 <+34>:mov    esi,DWORD PTR [rdx+0x40]
   0x000055b25daf6cab <+37>:mov    rdx,QWORD PTR [rbp-0x8]
   0x000055b25daf6caf <+41>:mov    rcx,QWORD PTR [rdx+0x38]
   0x000055b25daf6cb3 <+45>:mov    edx,DWORD PTR [rbp-0xc]
   0x000055b25daf6cb6 <+48>:mov    rdi,rcx
   0x000055b25daf6cb9 <+51>:call   rax
   0x000055b25daf6cbb <+53>:jmp    0x55b25daf6cbe <qemu_set_irq+56>
   0x000055b25daf6cbd <+55>:nop
   0x000055b25daf6cbe <+56>:leave  
   0x000055b25daf6cbf <+57>:ret    
End of assembler dump.

这里rax会作为s->irq被引用,+0x30位置存放的是handler,这个值会作为一个函数被引用,可以看上面汇编call rax,这也正是我们可以通过构造fake irq结构体来控制rip的方法,而这里由于0xdeadbeef的覆盖,引用的是无效地址,从而引发了异常,导致qemu崩溃。

 

gdb-peda$ x/10x 0x55b2deadbeef 
0x55b2deadbeef:Cannot access memory at address 0x55b2deadbeef
gdb-peda$ si
Thread 3 "qemu-system-x86" received signal SIGSEGV, Segmentation fault.

 

0x04 补丁对比

QEMU针对这两个CVE漏洞进行了修补,首先是CVE-2015-5165的patch,在rtl8139_cplus_transmit_one函数中,在be16_to_cpu(ip->ip_len)-hlen之间做了一个判断,首先是单独执行be16_to_cpu()。

 

gdb-peda$ si
[----------------------------------registers-----------------------------------]
RDI: 0x1300 //ip->ip_len
[-------------------------------------code-------------------------------------]
   0x5599f558bd83 <rtl8139_cplus_transmit_one+2020>:movzx  eax,ax
   0x5599f558bd86 <rtl8139_cplus_transmit_one+2023>:mov    edi,eax
=> 0x5599f558bd88 <rtl8139_cplus_transmit_one+2025>:
    call   0x5599f55881f7 <be16_to_cpu>
   0x5599f558bd8d <rtl8139_cplus_transmit_one+2030>:
    mov    WORD PTR [rbp-0x14a],ax
   0x5599f558bd94 <rtl8139_cplus_transmit_one+2037>:
    movzx  eax,WORD PTR [rbp-0x14a]
   0x5599f558bd9b <rtl8139_cplus_transmit_one+2044>:
    cmp    eax,DWORD PTR [rbp-0x118]
   0x5599f558bda1 <rtl8139_cplus_transmit_one+2050>:
    jl     0x5599f558c5d5 <rtl8139_cplus_transmit_one+4150>
Guessed arguments:
arg[0]: 0x1300 
Legend: code, data, rodata, value
0x00005599f558bd882126            ip_data_len = be16_to_cpu(ip->ip_len);

在be16_to_cpu之后,值仍然会变成0x13,但是不会直接和hlen相减,而是和hlen做了一个判断。

 

Legend: code, data, rodata, value
0x00005599f558bd9b2127            if (ip_data_len < hlen || ip_data_len > eth_payload_len) {
gdb-peda$ info register eax
eax            0x130x13
gdb-peda$ x 0x7f1f47693830-0x118
0x7f1f47693718:0x0000080000000014

如果小于,则会跳转到skip offload分支,直接将save_buffer交还缓冲区,并且增加计数,而不会进行后续处理。

 

gdb-peda$ si
[-------------------------------------code-------------------------------------]
   0x5599f558c5d1 <rtl8139_cplus_transmit_one+4146>:nop
   0x5599f558c5d2 <rtl8139_cplus_transmit_one+4147>:
    jmp    0x5599f558c5d5 <rtl8139_cplus_transmit_one+4150>
   0x5599f558c5d4 <rtl8139_cplus_transmit_one+4149>:nop
=> 0x5599f558c5d5 <rtl8139_cplus_transmit_one+4150>:
    mov    rax,QWORD PTR [rbp-0x158]
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
2330        ++s->tally_counters.TxOk;
skip_offload:
        /* update tally counter */
        ++s->tally_counters.TxOk;
……

来看一下补丁前后的对比。

http://p8.qhimg.com/t01a36f06e942d001d3.png

关于CVE-2015-7504的修补在那个位置的上面增加了一处判断。

http://p7.qhimg.com/t012e0cd8476acc1501.png

这里对size的大小进行了判断,给4096字节的buffer留出了4字节存放fcs的值,这里有个比较有意思的事情,就是刚开始我以为这里修补了漏洞,但是我在这个函数下断点的时候,却意外的发现没有命中而是直接退出了。

所以好奇跟了一下,发现实际上真正封堵这个漏洞的是在外层调用pcnet_transmit函数中,在外层函数中会有另外一处判断。

 

gdb-peda$ p *(struct PCNetState_st*)0x55e53ecafc80
$2 = {
  ……
  xmit_pos = 0x0, 
  ……}
//关键判断
gdb-peda$ si
[----------------------------------registers-----------------------------------]
RAX: 0x1000 //bcnt
RDX: 0x0 //s->xmit_pos
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x55e53c39cc26 <pcnet_transmit+704>:mov    rax,QWORD PTR [rbp-0x58]
   0x55e53c39cc2a <pcnet_transmit+708>:mov    edx,DWORD PTR [rax+0x218c]
   0x55e53c39cc30 <pcnet_transmit+714>:mov    eax,DWORD PTR [rbp-0x3c]
=> 0x55e53c39cc33 <pcnet_transmit+717>:add    eax,edx
   0x55e53c39cc35 <pcnet_transmit+719>:cmp    eax,0xffc
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055e53c39cc331250        if (s->xmit_pos + bcnt > sizeof(s->buffer) - 4) {

这里s->buffer的大小为4096,为它留出4字节的空间给CRC校验,也就是当我们长度设置为4096,这里xmit_pos为0,bcnt为4096,那么是不满足要求的,则在这里就进入异常处理。

 

[-------------------------------------code-------------------------------------]
   0x55e53c39cc35 <pcnet_transmit+719>:cmp    eax,0xffc
   0x55e53c39cc3a <pcnet_transmit+724>:
    jbe    0x55e53c39cc4f <pcnet_transmit+745>
   0x55e53c39cc3c <pcnet_transmit+726>:mov    rax,QWORD PTR [rbp-0x58]
=> 0x55e53c39cc40 <pcnet_transmit+730>:
    mov    DWORD PTR [rax+0x218c],0xffffffff
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000055e53c39cc401251            s->xmit_pos = -1;
gdb-peda$ p *(struct PCNetState_st*)0x55e53ecafc80
$2 = {
  ……
  xmit_pos = 0xffffffff, 
  ……}

而在后面的代码逻辑中,最后传入漏洞函数的size大小,就是s->xmit_pos+bcnt,因此外层函数一定满足size<=4092的条件,似乎里面的修补显得没有那么必要了。因此我也不太明白为什么会修补了两处,但至少这样做,彻底将这里的Heap Buffer Overflow修补了。

http://p4.qhimg.com/t01dfe5b26b30a38f63.png

这样我们完成了对PoC的调试,关于利用在phrack上的描述已经非常详细,只需要按照调试PoC的思路构造可控结构体,就可以完成最后的利用了。

http://p0.qhimg.com/t01ee5e1f33164e31f0.png

从虚拟机逃逸到宿主机获得宿主机权限的整个过程还是很有意思的,真的像从黑暗之门穿越到另一个世界,在内存中寻寻觅觅,最后一举突破的感觉非常棒。也希望自己未来能够挖到更多更强更高危的漏洞吧,请师傅们多多指正,感谢阅读!

 

 

参考文章

http://www.phrack.org/papers/vm-escape-qemu-case-study.html

http://bbs.pediy.com/thread-217997.htm

http://bbs.pediy.com/thread-218045.htm

http://bbs.pediy.com/thread-217999.htm

http://www.freebuf.com/articles/87949.html

(完)