Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

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

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

2017-08-16 17:34:12

阅读:527次
点赞(0)
收藏
来源: 安全客





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

作者:k0shl





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

作者:k0shl

预估稿费:600RMB

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


0x00 前言

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

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


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

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

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


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

0x01 QEMU环境搭建

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

$gitclonegit://git.qemu-project.org/qemu.git $cdqemu $mkdir-pbin/debug/native $cdbin/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-imgcreat-fqcow2ubuntu.qcow220G

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

$qemu-system-x86_64-enable-kvm-m2048-hda/path/to/ubuntu.qcow2-cdrom/path/to/ubuntu.iso $apt-getinstallxvnc4viewer

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

$./qemu-system-x86_64-enable-kvm-m2048-displayvnc=:89-netdevuser,id=t0,-devicertl8139,netdev=t0,id=nic0-netdevuser,id=t1,-devicepcnet,netdev=t1,id=nic1-drivefile=/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的方法下断点,方便跟踪调试。


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

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>:shral,0x4 0x55b25db58483<rtl8139_cplus_transmit_one+1857>:movzxeax,al 0x55b25db58486<rtl8139_cplus_transmit_one+1860>:andeax,0xf =>0x55b25db58489<rtl8139_cplus_transmit_one+1863>:cmpeax,0x4 [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db584892173if(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(carryPARITYadjustzerosigntrapINTERRUPTdirectionoverflow) [-------------------------------------code-------------------------------------] 0x55b25db584f7<rtl8139_cplus_transmit_one+1973>: movzxeax,WORDPTR[rax+0x2] 0x55b25db584fb<rtl8139_cplus_transmit_one+1977>:movzxeax,ax 0x55b25db584fe<rtl8139_cplus_transmit_one+1980>:movedi,eax =>0x55b25db58500<rtl8139_cplus_transmit_one+1982>: call0x55b25db54a37<be16_to_cpu> 0x55b25db58505<rtl8139_cplus_transmit_one+1987>:movedx,eax Guessedarguments: arg[0]:0x1300//ip->ip_len=0x1300 [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db585002181ip_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>:movzxeax,ax 0x55b25db584fe<rtl8139_cplus_transmit_one+1980>:movedi,eax 0x55b25db58500<rtl8139_cplus_transmit_one+1982>: call0x55b25db54a37<be16_to_cpu> =>0x55b25db58505<rtl8139_cplus_transmit_one+1987>:movedx,eax [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db585052181ip_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>: call0x55b25db54a37<be16_to_cpu> 0x55b25db58505<rtl8139_cplus_transmit_one+1987>:movedx,eax 0x55b25db58507<rtl8139_cplus_transmit_one+1989>: moveax,DWORDPTR[rbp-0x13c] =>0x55b25db5850d<rtl8139_cplus_transmit_one+1995>:subedx,eax 0x55b25db5850f<rtl8139_cplus_transmit_one+1997>:moveax,edx [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db5850d2181ip_data_len=be16_to_cpu(ip->ip_len)-hlen; gdb-peda$si [----------------------------------registers-----------------------------------] RDX:0xffffffff//相减之后为0xffffffff,这个变量是一个unsigned类型,此值极大 [-------------------------------------code-------------------------------------] 0x55b25db5850d<rtl8139_cplus_transmit_one+1995>:subedx,eax =>0x55b25db5850f<rtl8139_cplus_transmit_one+1997>:moveax,edx [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db5850f2181ip_data_len=be16_to_cpu(ip->ip_len)-hlen;

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

uint16_tip_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>: subeax,DWORDPTR[rbp-0x10c]//hlen的大小是0x14 0x55b25db586c8<rtl8139_cplus_transmit_one+2438>: movDWORDPTR[rbp-0x108],eax [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db586c22231inttcp_data_len=ip_data_len-tcp_hlen; gdb-peda$si [----------------------------------registers-----------------------------------] RAX:0xffeb//相减后tcp_data_len长度是0xffeb [-------------------------------------code-------------------------------------] 0x55b25db586c2<rtl8139_cplus_transmit_one+2432>: subeax,DWORDPTR[rbp-0x10c] =>0x55b25db586c8<rtl8139_cplus_transmit_one+2438>: movDWORDPTR[rbp-0x108],eax [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db586c82231inttcp_data_len=ip_data_len-tcp_hlen; //计算chunk_size=0x5b4 gdb-peda$ni [----------------------------------registers-----------------------------------] RAX:0x5b4 [-------------------------------------code-------------------------------------] 0x55b25db586ce<rtl8139_cplus_transmit_one+2444>:moveax,0x5dc 0x55b25db586d3<rtl8139_cplus_transmit_one+2449>: subeax,DWORDPTR[rbp-0x13c]//ETH_MTU-hlen 0x55b25db586d9<rtl8139_cplus_transmit_one+2455>: subeax,DWORDPTR[rbp-0x10c]//-tcp_hlen =>0x55b25db586df<rtl8139_cplus_transmit_one+2461>: movDWORDPTR[rbp-0x104],eax [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db586df2232inttcp_chunk_size=ETH_MTU-hlen-tcp_hlen;

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

inttcp_data_len=ip_data_len-tcp_hlen;//tcp_data_len=0xffff-0x14=0xffeb inttcp_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_tchunk_size=tcp_chunk_size;//0x5b4 /*checkifthisisthelastframe*/ if(tcp_send_offset+tcp_chunk_size>=tcp_data_len)//ifpacketlength>tcpdatalength { is_last_frame=1; chunk_size=tcp_data_len-tcp_send_offset; } DPRINTF("+++C+modeTSOTCPseqno%08x\n", be32_to_cpu(p_tcp_hdr->th_seq)); /*add4TCPpseudoheaderfields*/ /*copyIPsourceanddestinationfields*/ memcpy(data_to_checksum,saved_ip_header+12,8); DPRINTF("+++C+modeTSOcalculatingTCPchecksumfor" "packetwith%dbytesdata\n",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);//disclousekey!!!p_tcp_hdr=ip_headerp_tcp_hdr+tcp_hlen=datasection // } /*keepPUSHandFINflagsonlyforthelastframe*/ if(!is_last_frame) { TCP_HEADER_CLEAR_FLAGS(p_tcp_hdr,TCP_FLAG_PUSH|TCP_FLAG_FIN); } /*recalculateTCPchecksum*/ 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; inttcp_checksum=ip_checksum(data_to_checksum,tcp_hlen+chunk_size+12); DPRINTF("+++C+modeTSOTCPchecksum%04x\n", tcp_checksum); p_tcp_hdr->th_sum=tcp_checksum; /*restoreIPheader*/ memcpy(eth_payload_data,saved_ip_header,hlen); /*setIPdatalengthandrecalculateIPchecksum*/ ip->ip_len=cpu_to_be16(hlen+tcp_hlen+chunk_size); /*incrementIPidforsubsequentframes*/ 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+modeTSOIPheaderlen=%d" "checksum=%04x\n",hlen,ip->ip_sum); inttso_send_size=ETH_HLEN+hlen+tcp_hlen+chunk_size; DPRINTF("+++C+modeTSOtransferringpacketsize" "%d\n",tso_send_size); rtl8139_transfer_frame(s,saved_buffer,tso_send_size, 0,(uint8_t*)dot1q_buffer); /*addtransferredcounttoTCPsequencenumber*/ 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>:addrcx,rdx 0x55b25db58811<rtl8139_cplus_transmit_one+2767>:movrdx,rax 0x55b25db58814<rtl8139_cplus_transmit_one+2770>:movrdi,rcx =>0x55b25db58817<rtl8139_cplus_transmit_one+2773>:call0x55b25d9361a8//memcpy Guessedarguments: arg[0]:0x7f49f003a846-->0x0 arg[1]:0x7f49f003adfa-->0x9000000000000000 arg[2]:0x5b4 arg[3]:0x7f49f003a846-->0x0 [------------------------------------------------------------------------------] Legend:code,data,rodata,value Thread3"qemu-system-x86"hitBreakpoint4,0x000055b25db58817inrtl8139_cplus_transmit_one(s=0x55b26083d430) at/home/sh1/Desktop/qemu/hw/net/rtl8139.c:2267 2267memcpy((uint8_t*)p_tcp_hdr+tcp_hlen,(uint8_t*)p_tcp_hdr+tcp_hlen+tcp_send_offset,chunk_size); gdb-peda$x/50x0x7f49f003adfa//src 0x7f49f003adfa:0x90000000000000000x100000007f49e7c7 0x7f49f003ae0a:0xc0000000000000000x100000007f49e456 0x7f49f003ae1a:0x80000000000000000x100000007f49ef8b gdb-peda$ni [----------------------------------registers-----------------------------------] RAX:0x7f49f003a846-->0x9000000000000000 [-------------------------------------code-------------------------------------] 0x55b25db58811<rtl8139_cplus_transmit_one+2767>:movrdx,rax 0x55b25db58814<rtl8139_cplus_transmit_one+2770>:movrdi,rcx 0x55b25db58817<rtl8139_cplus_transmit_one+2773>:call0x55b25d9361a8 =>0x55b25db5881c<rtl8139_cplus_transmit_one+2778>: cmpDWORDPTR[rbp-0x130],0x0 [------------------------------------------------------------------------------] Legend:code,data,rodata,value 2271if(!is_last_frame) gdb-peda$x/50x0x7f49f003a846//targetdst 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>:movr8,rcx 0x55b25db58a3d<rtl8139_cplus_transmit_one+3323>:movecx,0x0 0x55b25db58a42<rtl8139_cplus_transmit_one+3328>:movrdi,rax =>0x55b25db58a45<rtl8139_cplus_transmit_one+3331>: call0x55b25db5776d<rtl8139_transfer_frame> Guessedarguments: 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 0x000055b25db58a452307rtl8139_transfer_frame(s,saved_buffer,tso_send_size, gdb-peda$x/50x0x7f49f003a810//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(carryparityadjustzerosigntrapINTERRUPTdirectionoverflow) [-------------------------------------code-------------------------------------] 0x55b25db5880e<rtl8139_cplus_transmit_one+2764>:addrcx,rdx 0x55b25db58811<rtl8139_cplus_transmit_one+2767>:movrdx,rax 0x55b25db58814<rtl8139_cplus_transmit_one+2770>:movrdi,rcx =>0x55b25db58817<rtl8139_cplus_transmit_one+2773>:call0x55b25d9361a8//memcpy Guessedarguments: arg[0]:0x7f49f003a846-->0x9000000000000000 arg[1]:0x7f49f003b3ae-->0x7f49cc0de0000000 arg[2]:0x5b4 arg[3]:0x7f49f003a846-->0x9000000000000000 [------------------------------------------------------------------------------] Legend:code,data,rodata,value Thread3"qemu-system-x86"hitBreakpoint4,0x000055b25db58817inrtl8139_cplus_transmit_one(s=0x55b26083d430) at/home/sh1/Desktop/qemu/hw/net/rtl8139.c:2267 2267memcpy((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中读取,这样就会造成信息泄露了。


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

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*(structPCNetState_st*)0x55b25f34a1a0 $1={ …… buffer="RT\000\022\064VRT\000\022\064V\b",'\000', irq=0x55b2603bc910, …… looptest=0x1 }

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

//stores->buffertosrc [----------------------------------registers-----------------------------------] RAX:0x55b25f34a1a0-->0x55b2603bca00-->0x55b2603bca20-->0x55b25e13d940-->0x1 [-------------------------------------code-------------------------------------] 0x55b25db4e537<pcnet_receive+952>:movWORDPTR[rax+0x212c],dx 0x55b25db4e53e<pcnet_receive+959>: jmp0x55b25db4effb<pcnet_receive+3708> 0x55b25db4e543<pcnet_receive+964>:movrax,QWORDPTR[rbp-0xa8] =>0x55b25db4e54a<pcnet_receive+971>:addrax,0x2290//offset [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db4e54a1062uint8_t*src=s->buffer;

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

//looptest [----------------------------------registers-----------------------------------] RAX:0x1//s->looptest [-------------------------------------code-------------------------------------] 0x55b25db4e587<pcnet_receive+1032>:movDWORDPTR[rbp-0xd8],0x0 0x55b25db4e591<pcnet_receive+1042>:movrax,QWORDPTR[rbp-0xa8] 0x55b25db4e598<pcnet_receive+1049>:moveax,DWORDPTR[rax+0x32b4] =>0x55b25db4e59e<pcnet_receive+1055>:testeax,eax 0x55b25db4e5a0<pcnet_receive+1057>: jne0x55b25db4e635<pcnet_receive+1206> [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db4e59e1067if(!s->looptest){ //s->looptest=PCNET_LOOPTEST_CRC [----------------------------------registers-----------------------------------] RAX:0x1[-------------------------------------code-------------------------------------] =>0x55b25db4e645<pcnet_receive+1222>: je0x55b25db4e66c<pcnet_receive+1261> 0x55b25db4e647<pcnet_receive+1224>:movrax,QWORDPTR[rbp-0xa8] 0x55b25db4e64e<pcnet_receive+1231>:movzxeax,WORDPTR[rax+0x206a] 0x55b25db4e655<pcnet_receive+1238>:movzxeax,ax [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db4e6451075}elseif(s->looptest==PCNET_LOOPTEST_CRC||

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

elseif(s->looptest==PCNET_LOOPTEST_CRC|| !CSR_DXMTFCS(s)||size<MIN_BUF_SIZE+4){ uint32_tfcs=~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>:movDWORDPTR[rbp-0xd4],0xffffffff 0x55b25db4e676<pcnet_receive+1271>:movrax,QWORDPTR[rbp-0x98] 0x55b25db4e67d<pcnet_receive+1278>:movQWORDPTR[rbp-0xb8],rax =>0x55b25db4e684<pcnet_receive+1285>: jmp0x55b25db4e6ce<pcnet_receive+1359> [------------------------------------------------------------------------------] Legend:code,data,rodata,value Thread3"qemu-system-x86"hitBreakpoint10,pcnet_receive( nc=0x55b2603bca20,buf=0x55b25f34c430"RT",size_=0x1000) at/home/sh1/Desktop/qemu/hw/net/pcnet.c:1080 1080while(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/10x0x55B25F34D400 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>:call0x55b25d936078 =>0x55b25db4e6f7<pcnet_receive+1400>:movedx,eax 0x55b25db4e6f9<pcnet_receive+1402>:movrax,QWORDPTR[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>:movDWORDPTR[rax],edx [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055b25db4e7001082*(uint32_t*)p=htonl(fcs); gdb-peda$x/10x0x55b25f34d430//拷贝前 0x55b25f34d430:0x000055b2603bc9100x000055b25db4be11 0x55b25f34d440:0x000055b25db4bdd90x000055b25f349920 0x55b25f34d450:0x00000001000000010x000055b25f182850 0x55b25f34d460:0x00000000000000000x000055b25ff0d760 0x55b25f34d470:0x000055b2603bc7300x0000000000000001 gdb-peda$si//拷贝后 [----------------------------------registers-----------------------------------] RAX:0x55b25f34d430-->0x55b2deadbeef RDX:0xdeadbeef[-------------------------------------code-------------------------------------] 0x55b25db4e700<pcnet_receive+1409>:movDWORDPTR[rax],edx =>0x55b25db4e702<pcnet_receive+1411>:addDWORDPTR[rbp-0xe4],0x4 [------------------------------------------------------------------------------] Legend:code,data,rodata,value 1083size+=4; gdb-peda$x/10x0x55b25f34d430//拷贝结束后deadbeef覆盖了irq结构 0x55b25f34d430:0x000055b2deadbeef0x000055b25db4be11

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

RDI:0x55b2deadbeef 0x55b25db4d31d<pcnet_update_irq+497>:movesi,edx 0x55b25db4d31f<pcnet_update_irq+499>:movrdi,rax =>0x55b25db4d322<pcnet_update_irq+502>: call0x55b25daf6c86<qemu_set_irq>

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

gdb-peda$disasqemu_set_irq Dumpofassemblercodeforfunctionqemu_set_irq: 0x000055b25daf6c86<+0>:pushrbp 0x000055b25daf6c87<+1>:movrbp,rsp 0x000055b25daf6c8a<+4>:subrsp,0x10 0x000055b25daf6c8e<+8>:movQWORDPTR[rbp-0x8],rdi 0x000055b25daf6c92<+12>:movDWORDPTR[rbp-0xc],esi 0x000055b25daf6c95<+15>:cmpQWORDPTR[rbp-0x8],0x0 0x000055b25daf6c9a<+20>:je0x55b25daf6cbd<qemu_set_irq+55> =>0x000055b25daf6c9c<+22>:movrax,QWORDPTR[rbp-0x8] 0x000055b25daf6ca0<+26>:movrax,QWORDPTR[rax+0x30] 0x000055b25daf6ca4<+30>:movrdx,QWORDPTR[rbp-0x8] 0x000055b25daf6ca8<+34>:movesi,DWORDPTR[rdx+0x40] 0x000055b25daf6cab<+37>:movrdx,QWORDPTR[rbp-0x8] 0x000055b25daf6caf<+41>:movrcx,QWORDPTR[rdx+0x38] 0x000055b25daf6cb3<+45>:movedx,DWORDPTR[rbp-0xc] 0x000055b25daf6cb6<+48>:movrdi,rcx 0x000055b25daf6cb9<+51>:callrax 0x000055b25daf6cbb<+53>:jmp0x55b25daf6cbe<qemu_set_irq+56> 0x000055b25daf6cbd<+55>:nop 0x000055b25daf6cbe<+56>:leave 0x000055b25daf6cbf<+57>:ret Endofassemblerdump.

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

gdb-peda$x/10x0x55b2deadbeef 0x55b2deadbeef:Cannotaccessmemoryataddress0x55b2deadbeef gdb-peda$si Thread3"qemu-system-x86"receivedsignalSIGSEGV,Segmentationfault.

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>:movzxeax,ax 0x5599f558bd86<rtl8139_cplus_transmit_one+2023>:movedi,eax =>0x5599f558bd88<rtl8139_cplus_transmit_one+2025>: call0x5599f55881f7<be16_to_cpu> 0x5599f558bd8d<rtl8139_cplus_transmit_one+2030>: movWORDPTR[rbp-0x14a],ax 0x5599f558bd94<rtl8139_cplus_transmit_one+2037>: movzxeax,WORDPTR[rbp-0x14a] 0x5599f558bd9b<rtl8139_cplus_transmit_one+2044>: cmpeax,DWORDPTR[rbp-0x118] 0x5599f558bda1<rtl8139_cplus_transmit_one+2050>: jl0x5599f558c5d5<rtl8139_cplus_transmit_one+4150> Guessedarguments: arg[0]:0x1300 Legend:code,data,rodata,value 0x00005599f558bd882126ip_data_len=be16_to_cpu(ip->ip_len);

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

Legend:code,data,rodata,value 0x00005599f558bd9b2127if(ip_data_len<hlen||ip_data_len>eth_payload_len){ gdb-peda$inforegistereax eax0x130x13 gdb-peda$x0x7f1f47693830-0x118 0x7f1f47693718:0x0000080000000014

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

gdb-peda$si [-------------------------------------code-------------------------------------] 0x5599f558c5d1<rtl8139_cplus_transmit_one+4146>:nop 0x5599f558c5d2<rtl8139_cplus_transmit_one+4147>: jmp0x5599f558c5d5<rtl8139_cplus_transmit_one+4150> 0x5599f558c5d4<rtl8139_cplus_transmit_one+4149>:nop =>0x5599f558c5d5<rtl8139_cplus_transmit_one+4150>: movrax,QWORDPTR[rbp-0x158] [------------------------------------------------------------------------------] Legend:code,data,rodata,value 2330++s->tally_counters.TxOk; skip_offload: /*updatetallycounter*/ ++s->tally_counters.TxOk; ……

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


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

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


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

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

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

gdb-peda$p*(structPCNetState_st*)0x55e53ecafc80 $2={ …… xmit_pos=0x0, ……} //关键判断 gdb-peda$si [----------------------------------registers-----------------------------------] RAX:0x1000//bcnt RDX:0x0//s->xmit_pos EFLAGS:0x206(carryPARITYadjustzerosigntrapINTERRUPTdirectionoverflow) [-------------------------------------code-------------------------------------] 0x55e53c39cc26<pcnet_transmit+704>:movrax,QWORDPTR[rbp-0x58] 0x55e53c39cc2a<pcnet_transmit+708>:movedx,DWORDPTR[rax+0x218c] 0x55e53c39cc30<pcnet_transmit+714>:moveax,DWORDPTR[rbp-0x3c] =>0x55e53c39cc33<pcnet_transmit+717>:addeax,edx 0x55e53c39cc35<pcnet_transmit+719>:cmpeax,0xffc [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055e53c39cc331250if(s->xmit_pos+bcnt>sizeof(s->buffer)-4){

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

[-------------------------------------code-------------------------------------] 0x55e53c39cc35<pcnet_transmit+719>:cmpeax,0xffc 0x55e53c39cc3a<pcnet_transmit+724>: jbe0x55e53c39cc4f<pcnet_transmit+745> 0x55e53c39cc3c<pcnet_transmit+726>:movrax,QWORDPTR[rbp-0x58] =>0x55e53c39cc40<pcnet_transmit+730>: movDWORDPTR[rax+0x218c],0xffffffff [------------------------------------------------------------------------------] Legend:code,data,rodata,value 0x000055e53c39cc401251s->xmit_pos=-1; gdb-peda$p*(structPCNetState_st*)0x55e53ecafc80 $2={ …… xmit_pos=0xffffffff, ……}

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


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

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


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

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


参考文章

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.CodeSec.Net/articles/87949.html



【漏洞分析】前往黑暗之门!Debugee in QEMU
【漏洞分析】前往黑暗之门!Debugee in QEMU
本文由 安全客 原创发布,如需转载请注明来源及本文地址。
本文地址:http://bobao.360.cn/learning/detail/4265.html

Viewing all articles
Browse latest Browse all 12749