**Of Course We Can Escape**
距离上篇有一个多月的时间了,趁着上周的两天空档终于复现了一个虚拟机逃逸漏洞的利用全过程。在4月底的时候,35C3比赛中解出题目的另一位选手@TheFloW也发布了一篇wp,chromacity: Escaping the VM with newlines。加上之前Tea Delivers在知乎上的wp,凑齐了比赛时间内完成题目的两篇题解,大家都是在比赛结束的几个月后才来发布wp,一方面说明这是真实存在的0day,需要尽量降低漏洞的危害;另一方面也大致体现了这个漏洞学习的价值。
Debug Script
上篇完成了Virtualbox 5.2.22带debug symbol的版本编译,并且在文章结尾尝试用gdb attach上Virtualbox的进程进行调试,为了快速启动系统,我完善虚拟机系统的配置后创建了一个镜像。进行一次调试的步骤如下:
- 恢复虚拟机的镜像状态
- 启动虚拟机
- gdb附加VirtualBox进程,加载断点
- ssh 连接上虚拟机
- 在虚拟机中执行poc,断点处调试
为了方便操作,编写了如下的一键启动调试脚本,期间了解到了Virtualbox命令行管理工具VBoxManage的用法:
1 |
|
Exploit Analysis
接下来的工作就是分析两篇wp中的exp,逐点弄懂背后的漏洞利用逻辑。
Leaking CRConnection
Address
两个exp的第一步都是泄露出一个CRConnection
结构体的地址,而且作者们都直接使用了出题人@niklasb提供的代码
1 | def leak_conn(client): |
这段代码首先是通过堆喷射来填充空间,保证能够分配出相邻的CRConnection
和CRClient
对象,这里使用的是Tea delivers提出的越界读函数,实际上通过内存未初始化的漏洞也能泄露的出CRClient
的地址。
问题一:如何确定hgcm_connect
连接时候分配的结构体,以及确认它们的大小?
实际上我们可以动态调试获得这些信息,在crVBoxServerAddClient
和crAlloc
下断点就能追踪到分配结构体的过程,其中crAlloc
是chrome库中对malloc
函数的一个封装。此时我们可以看到程序陆续调用了crAlloc(0x9d0)
和crAlloc(0x298)
,也就是先分配了一个CRClient
再分配了一个CRConnection
,这点很重要,因为稍后我们需要通过泄露出CRClient
的地址和结构体大小来计算出CRConnection
的地址。接下来可以使用pahole插件来查看结构体的大小及成员变量偏移:
1 | gef> pahole CRConnection |
1 | gef> pahole CRClient |
同时,通过阅读源码我们也能定位到创建这两个结构体的具体位置:
1 | int32_t crVBoxServerAddClient(uint32_t u32ClientID) |
1 | /** |
问题二:越界读或者内存未初始化泄露地址的原理?
在漏洞函数crUnpackExtendGetUniformLocation
断点并且打印backtrace可以观察到函数调用的过程:
1 | [#0] 0x7fe6e19f77c5->crUnpackExtendGetUniformLocation() |
首先是服务端(宿主)收到一个svcCall请求,然后逐步分发消息,调用到crUpack
函数,走拓展功能的调用方法,最终会调用目标函数crUnpackExtendGetUniformLocation
。
1 | void crUnpackExtendGetUniformLocation(void) |
该函数中没有对传入的packet_length
做任何检查,由此会产生一个越界读。而@TheFloW也提到此处其实并不需要OOB,因为svcGetBuffer()
构造CRVBOXSVCBUFFER_t
结构体的时候并没有初始化内存的操作。disconnect时候回收的内存,随后又马上分配给了可读写的CRVBOXSVCBUFFER_t
,于是我们能够读出其中残留的指针。这里画了一幅图来展示整个堆喷射和泄露的内存结构:
1 | +-------------------------------+ +-------------------------------+ |
Arbitrary R/W
两篇wp中把漏洞的成因都介绍得非常清楚,问题是代码漏掉了对最后一层循环的操作进行越界检查,导致可以向后溢出使后方的\x00
字节被改成\xa0
字节。利用的方法是布置好连续的CRVBOXSVCBUFFER_t
,如下图所示,释放第一个buffer,重新填充上CR_SHADERSOURCE_EXTEND
的消息,执行crUnpackExtendShaderSource
漏洞函数以后会越界写把后方的\x00
都改成\xa0
。导致第二个结构体信息被更改:
uiID
被破坏,如:0x0000aabb -> 0x0a0aaabbuiSize
被增大,如: 0x00000030 -> 0x0a0a0a30
size变大以后的buffer可以继续覆盖第三个结构体,此时我们就可以随意控制第三个结构体的uiID
,uiSize
,pData
,通过修改pData
并利用伪造的uiID
,进行SHCRGL_GUEST_FN_WRITE_BUFFER
和SHCRGL_GUEST_FN_READ
,便可以进行任意读写了。下面是示意图:
1 | +-----------------------------+ |
Failed Attempt
然而事情并没有这么简单,编写exp进行到使用SHCRGL_GUEST_FN_WRITE_READ_BUFFERED
触发CR_SHADERSOURCE_EXTEND_OPCODE
之后程序就立马崩溃了。经过反复排查发现问题出在svcCall
中执行完CR_SHADERSOURCE_EXTEND_OPCODE
后调用的svcFreeBuffer
(下面代码第58行),观察内存发现越界修改\x00
的漏洞是没有问题了,但是free
的步骤会报错。
1 | CR_SHADERSOURCE_EXTEND_OPCODEstatic DECLCALLBACK(void) svcCall (void *, VBOXHGCMCALLHANDLE callHandle, uint32_t u32ClientID, void *pvClient, uint32_t u32Function, uint32_t cParms, VBOXHGCMSVCPARM paParms[]) |
由于是GUI程序,没有命令行来查看程序的报错信息。程序会直接闪退,但基本确认是free
函数内部的某些错误。想要加载glibc的源码定位错误但是没找到gdb加载多个项目源码的方法,最终使用bt命令打印错误点的调用栈再一步步回溯。追踪到以下的调用位置,这是一个abort函数的调用,看到调用参数处的$rcx
指向的错误信息字符串free(): invalid next size (fast)
,结合malloc源码可以确认程序崩溃的原因是越界写破坏掉了chunk meta中的size,导致fastbin free
流程的时候检查不通过:
1 | $rax : 0x0 |
那么为什么两篇wp都成功了呢,后来仔细想才发现是glibc版本的问题,比赛给的环境是Lubuntu 18.04 ,采用2.27版本的glibc,开启了tcache机制。而我在上篇中费尽心血编译出来的版本是运行在Ubuntu 16.04里面的,2.23版本的glibc,没有tcache机制。
在2.23版本中,释放大小为0x30的chunk会走fastbin的流程,其中包含对下一个chunk的合法检查,也就是上面报错卡住的地方。而开启tcache的版本就没有这些检查了,直接把chunk放入tcache中。因此原题的exp并不会出现这样的报错。确定了问题的原因,自然就有了解决方法,直接在18.04的系统中重新编译Virtualbox。当然也可以在16.04中想办法绕过这个free
,但只怕需要构造更加复杂的内存布局,老版本libc的利用看有没有机会以后再研究了。
Arbitrary Command Execution
从任意地址读写到任意命令执行的方法也比较简单直接。
- 第一步获取的
CRConnection
结构体地址中读取CRConnection->Free
的内容,该指针指向的是crVBoxHGCMFree
函数。 crVBoxHGCMFree
位于VBoxOGLhostcrutil.so
中,可以计算出VBoxOGLhostcrutil.so
的加载地址VBoxOGLhostcrutil.so
是chrome的共享库,其中也有GOT表,加载了部分libc函数的地址。- 计算GOT表中read函数的地址,泄露出read函数的libc地址。
- 通过偏移计算出libc中
system
函数的地址 - 将
CRConnection->Disconnect
的指针改写为system
,把任意指令写入CRConnection的头部。 - 通过
SHCRGL_GUEST_FN_READ
等操作触发disconnect,即可执行任意指令。
具体的exp如下,基本是根据两位大神的思路来写的,重构了一下希望逻辑能更加清晰吧。
1 | #!/usr/bin/env python2 |