Pwn2Own 2020: Oracle VirtualBox Escape | STAR Labs
September 25, 2020 · 9 min · Pham Hong Phi (@4nhdaden)
Table of Contents
- The Vulnerabilities
- E1000 Out-Of-Bounds Read Vulnerability
- Information Leakage
- OHCI Controller Uninitialized Variable
- Code Execution
- The Patches
The Vulnerabilities
在本篇文章中,我们将介绍在Pwn2Own 2020中用于Oracle VirtualBox逃逸的漏洞。这两个漏洞影响Oracle VirtualBox 6.1.4及更早版本。
漏洞利用链包含2个漏洞:
-
Intel PRO 1000 MT Desktop (E1000) Network Adapter - 越界读漏洞
https://www.zerodayinitiative.com/advisories/ZDI-20-581/
-
Open Host Controller Interface (OHCI) USB Controller - 未初始化变量漏洞
https://www.zerodayinitiative.com/advisories/ZDI-20-582/
E1000 Out-Of-Bounds Read Vulnerability
有关E1000网络适配器内部工作原理的更多信息,您可以在此处阅读。
在使用E1000网络适配器发送以太网帧时,我们可以通过设置数据描述符选项字段中的IXSM位来控制IP校验和的插入:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:5191
static bool e1kLocateTxPacket(PE1KSTATE pThis)
{
...
E1KTXDESC *pDesc = &pThis->aTxDescriptors[i];
switch (e1kGetDescType(pDesc))
{
...
case E1K_DTYP_DATA:
...
if (cbPacket == 0)
{
/*
* The first fragment: save IXSM and TXSM options
* as these are only valid in the first fragment.
*/
pThis->fIPcsum = pDesc->data.dw3.fIXSM;
pThis->fTCPcsum = pDesc->data.dw3.fTXSM;
fTSE = pDesc->data.cmd.fTSE;
...
}
|
启用pThis->fIPcsum
标志后,IP校验和将被插入到以太网帧中:
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
|
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:4997
static int e1kXmitDesc(PPDMDEVINS pDevIns, PE1KSTATE pThis, PE1KSTATECC pThisCC, E1KTXDESC *pDesc,
RTGCPHYS addr, bool fOnWorkerThread)
{
...
switch (e1kGetDescType(pDesc))
{
...
case E1K_DTYP_DATA:
{
STAM_COUNTER_INC(pDesc->data.cmd.fTSE?
&pThis->StatTxDescTSEData:
&pThis->StatTxDescData);
E1K_INC_ISTAT_CNT(pThis->uStatDescDat);
STAM_PROFILE_ADV_START(&pThis->CTX_SUFF_Z(StatTransmit), a);
if (pDesc->data.cmd.u20DTALEN == 0 || pDesc->data.u64BufAddr == 0)
{
...
}
else
{
...
else if (!pDesc->data.cmd.fTSE)
{
...
if (pThis->fIPcsum)
e1kInsertChecksum(pThis, (uint8_t *)pThisCC->CTX_SUFF(pTxSg)->aSegs[0].pvSeg, pThis->u16TxPktLen,
pThis->contextNormal.ip.u8CSO,
pThis->contextNormal.ip.u8CSS,
pThis->contextNormal.ip.u16CSE);
|
函数e1kInsertChecksum()
将计算校验和并将其放入帧体中。pThis->contextNormal
的三个字段u8CSO
、u8CSS
和u16CSE
可以通过上下文描述符指定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:5158
DECLINLINE(void) e1kUpdateTxContext(PE1KSTATE pThis, E1KTXDESC *pDesc)
{
if (pDesc->context.dw2.fTSE)
{
...
}
else
{
pThis->contextNormal = pDesc->context;
STAM_COUNTER_INC(&pThis->StatTxDescCtxNormal);
}
...
}
|
函数e1kInsertChecksum()
的实现:
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
|
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:4155
static void e1kInsertChecksum(PE1KSTATE pThis, uint8_t *pPkt, uint16_t u16PktLen, uint8_t cso, uint8_t css, uint16_t cse)
{
RT_NOREF1(pThis);
if (css >= u16PktLen) // [1]
{
E1kLog2(("%s css(%X) is greater than packet length-1(%X), checksum is not inserted\n",
pThis->szPrf, cso, u16PktLen));
return;
}
if (cso >= u16PktLen - 1) // [2]
{
E1kLog2(("%s cso(%X) is greater than packet length-2(%X), checksum is not inserted\n",
pThis->szPrf, cso, u16PktLen));
return;
}
if (cse == 0) // [3]
cse = u16PktLen - 1;
else if (cse < css) // [4]
{
E1kLog2(("%s css(%X) is greater than cse(%X), checksum is not inserted\n",
pThis->szPrf, css, cse));
return;
}
uint16_t u16ChkSum = e1kCSum16(pPkt + css, cse - css + 1);
E1kLog2(("%s Inserting csum: %04X at %02X, old value: %04X\n", pThis->szPrf,
u16ChkSum, cso, *(uint16_t*)(pPkt + cso)));
*(uint16_t*)(pPkt + cso) = u16ChkSum;
}
|
css
是数据包中开始计算校验和的偏移量,它需要小于u16PktLen
(即当前数据包的总大小)(检查[1])。
cse
是数据包中停止计算校验和的偏移量。
将cse
字段设置为0表示校验和将从css
覆盖到数据包的末尾(检查[3])。
cse
需要大于css
(检查[4])。
cso
是数据包中写入校验和的偏移量,它需要小于u16PktLen - 1
(检查[2])。
由于没有对cse
的最大值进行检查,我们可以将此字段设置为大于当前数据包的总大小,导致越界访问,并使e1kCSum16()
计算数据包体pPkt
之后数据的校验和。“过度读取"的校验和将被插入到以太网帧中,并可以被接收方 later 读取。
因此,如果我们想通过过度读取的校验和泄漏某些信息,我们需要一种可靠的方法来知道哪些数据与过度读取的缓冲区相邻。在模拟的E1000设备中,发送缓冲区由e1kXmitAllocBuf()
函数分配:
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
|
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:3833
DECLINLINE(int) e1kXmitAllocBuf(PE1KSTATE pThis, PE1KSTATECC pThisCC, bool fGso)
{
...
PPDMSCATTERGATHER pSg;
if (RT_LIKELY(GET_BITS(RCTL, LBM) != RCTL_LBM_TCVR)) // [1]
{
...
int rc = pDrv->pfnAllocBuf(pDrv, pThis->cbTxAlloc, fGso ? &pThis->GsoCtx : NULL, &pSg);
...
}
else
{
/* Create a loopback using the fallback buffer and preallocated SG. */
AssertCompileMemberSize(E1KSTATE, uTxFallback.Sg, 8 * sizeof(size_t));
pSg = &pThis->uTxFallback.Sg;
pSg->fFlags = PDMSCATTERGATHER_FLAGS_MAGIC | PDMSCATTERGATHER_FLAGS_OWNER_3;
pSg->cbUsed = 0;
pSg->cbAvailable = sizeof(pThis->aTxPacketFallback);
pSg->pvAllocator = pThis;
pSg->pvUser = NULL; /* No GSO here. */
pSg->cSegs = 1;
pSg->aSegs[0].pvSeg = pThis->aTxPacketFallback; // [2]
pSg->aSegs[0].cbSeg = sizeof(pThis->aTxPacketFallback);
}
pThis->cbTxAlloc = 0;
pThisCC->CTX_SUFF(pTxSg) = pSg;
return VINF_SUCCESS;
}
|
RCTL寄存器中的LBM(环回模式)字段控制以太网控制器的环回模式,它影响数据包缓冲区的分配方式(参见[1]):
- 无环回模式:
e1kXmitAllocBuf()
使用pDrv->pfnAllocBuf()
回调分配数据包缓冲区,此回调将使用操作系统分配器或VirtualBox的自定义分配器。
- 启用环回模式:数据包缓冲区是
aTxPacketFallback
数组(参见[2])。
aTxPacketFallback
数组是PE1KSTATE pThis
对象的一个属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:1024
typedef struct E1KSTATE
{
...
/** TX: Transmit packet buffer use for TSE fallback and loopback. */
uint8_t aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
/** TX: Number of bytes assembled in TX packet buffer. */
uint16_t u16TxPktLen;
...
} E1KSTATE;
/* Pointer to the E1000 device state. */
typedef E1KSTATE *PE1KSTATE;
|
因此,通过启用环回模式:
- 数据包接收者是我们自己,不需要另一台主机来读取过度读取的校验和
- 数据包缓冲区位于
pThis
结构中,因此过度读取的数据是pThis
对象的其他字段
现在我们知道了哪些数据与数据包缓冲区相邻,我们可以通过以下步骤逐字泄漏:
- 发送一个包含
E1K_MAX_TX_PKT_SIZE
字节的CRC-16校验和的帧,称为crc0
。
- 发送第二个包含
E1K_MAX_TX_PKT_SIZE + 2
字节的校验和的帧,称为crc1
。
- 由于校验和算法是CRC-16,通过计算
crc0
和crc1
之间的差异,我们可以知道aTxPacketFallback
数组之后的两个字节的值。
每次将过度读取大小增加2字节,并重复此操作,直到获取到一些有趣的数据。幸运的是,在pThis
对象之后,我们可以在偏移量E1K_MAX_TX_PKT_SIZE + 0x1f7
处找到指向VBoxDD.dll模块中全局变量的指针。
一个小问题是,在pThis
对象中,aTxPacketFallback
数组之后还有其他设备的计数器寄存器,这些寄存器在每次发送帧时都会增加,因此如果我们发送两个具有相同过度读取大小的帧,也会导致两个不同的校验和,但计数器增量每次相似,因此这种差异是可预测的,可以通过向第二个校验和添加0x5a来均衡。
OHCI Controller Uninitialized Variable
您可以在此处阅读有关VirtualBox OHCI设备的更多信息。
在向USB设备发送控制消息URB时,我们可以包含一个设置数据包来更新消息URB:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// VirtualBox-6.1.4\src\VBox\Devices\USB\VUSBUrb.cpp:834
static int vusbUrbSubmitCtrl(PVUSBURB pUrb)
{
...
if (pUrb->enmDir == VUSBDIRECTION_SETUP)
{
LogFlow(("%s: vusbUrbSubmitCtrl: pPipe=%p state %s->SETUP\n",
pUrb->pszDesc, pPipe, g_apszCtlStates[pExtra->enmStage]));
pExtra->enmStage = CTLSTAGE_SETUP;
}
...
switch (pExtra->enmStage)
{
case CTLSTAGE_SETUP:
...
if (!vusbMsgSetup(pPipe, pUrb->abData, pUrb->cbData))
{
pUrb->enmState = VUSBURBSTATE_REAPED;
pUrb->enmStatus = VUSBSTATUS_DNR;
vusbUrbCompletionRh(pUrb);
break;
|
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
|
// VirtualBox-6.1.4\src\VBox\Devices\USB\VUSBUrb.cpp:664
static bool vusbMsgSetup(PVUSBPIPE pPipe, const void *pvBuf, uint32_t cbBuf)
{
PVUSBCTRLEXTRA pExtra = pPipe->pCtrl;
const VUSBSETUP *pSetupIn = (PVUSBSETUP)pvBuf;
...
if (pExtra->cbMax < cbBuf + pSetupIn->wLength + sizeof(VUSBURBVUSBINT)) // [1]
{
uint32_t cbReq = RT_ALIGN_32(cbBuf + pSetupIn->wLength + sizeof(VUSBURBVUSBINT), 1024);
PVUSBCTRLEXTRA pNew = (PVUSBCTRLEXTRA)RTMemRealloc(pExtra, RT_UOFFSETOF_DYN(VUSBCTRLEXTRA, Urb.abData[cbReq])); // [2]
if (!pNew)
{
Log(("vusbMsgSetup: out of memory!!! cbReq=%u %zu\n",
cbReq, RT_UOFFSETOF_DYN(VUSBCTRLEXTRA, Urb.abData[cbReq])));
return false;
}
if (pExtra != pNew)
{
pNew->pMsg = (PVUSBSETUP)pNew->Urb.abData;
pExtra = pNew;
pPipe->pCtrl = pExtra;
}
pExtra->Urb.pVUsb = (PVUSBURBVUSB)&pExtra->Urb.abData[cbBuf + pSetupIn->wLength]; // [3]
pExtra->Urb.pVUsb->pUrb = &pExtra->Urb; // [4]
pExtra->cbMax = cbReq;
}
Assert(pExtra->Urb.enmState == VUSBURBSTATE_ALLOCATED);
/*
* Copy the setup data and prepare for data.
*/
PVUSBSETUP pSetup = pExtra->pMsg;
pExtra->fSubmitted = false;
pExtra->Urb.enmState = VUSBURBSTATE_IN_FLIGHT;
pExtra->pbCur = (uint8_t *)(pSetup + 1);
pSetup->bmRequestType = pSetupIn->bmRequestType;
pSetup->bRequest = pSetupIn->bRequest;
pSetup->wValue = RT_LE2H_U16(pSetupIn->wValue);
pSetup->wIndex = RT_LE2H_U16(pSetupIn->wIndex);
pSetup->wLength = RT_LE2H_U16(pSetupIn->wLength);
...
return true;
}
|
pSetupIn
是我们的URB数据包,pExtra
是控制管道的当前额外数据,如果设置请求的大小大于当前控制管道额外数据的大小(检查[1]),pExtra
将在[2]处重新分配为更大的大小。
原始的pExtra
在vusbMsgAllocExtraData()
中分配和初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// VirtualBox-6.1.4\src\VBox\Devices\USB\VUSBUrb.cpp:609
static PVUSBCTRLEXTRA vusbMsgAllocExtraData(PVUSBURB pUrb)
{
/** @todo reuse these? */
PVUSBCTRLEXTRA pExtra;
const size_t cbMax = sizeof(VUSBURBVUSBINT) + sizeof(pExtra->Urb.abData) + sizeof(VUSBSETUP);
pExtra = (PVUSBCTRLEXTRA)RTMemAllocZ(RT_UOFFSETOF_DYN(VUSBCTRLEXTRA, Urb.abData[cbMax]));
if (pExtra)
{
...
pExtra->Urb.pVUsb = (PVUSBURBVUSB)&pExtra->Urb.abData[sizeof(pExtra->Urb.abData) + sizeof(VUSBSETUP)];
//pExtra->Urb.pVUsb->pCtrlUrb = NULL;
//pExtra->Urb.pVUsb->pNext = NULL;
//pExtra->Urb.pVUsb->ppPrev = NULL;
pExtra->Urb.pVUsb->pUrb = &pExtra->Urb;
pExtra->Urb.pVUsb->pDev = pUrb->pVUsb->pDev; // [5]
pExtra->Urb.pVUsb->pfnFree = vusbMsgFreeUrb;
pExtra->Urb.pVUsb->pvFreeCtx = &pExtra->Urb;
...
}
return pExtra;
}
|
函数RTMemRealloc()
不执行任何初始化,因此结果缓冲区将包含两个部分:
- 部分A:旧的小
pExtra
主体。
- 部分B:新分配的未初始化数据。
重新分配后:
pExtra->Urb.pVUsb
对象将使用新的pVUsb
更新,该pVUsb
位于部分B中(在[3]处)
- 但是新的
pVUsb
位于未初始化的数据中,并且只有pVUsb->pUrb
在[4]处更新,
因此pExtra->Urb.pVUsb
对象的其他属性保持未初始化,包括pExtra->Urb.pVUsb->pDev
对象(参见[5])。
pExtra->Urb
对象稍后将在vusbMsgDoTransfer()
函数中使用:
1
2
3
4
5
6
7
|
// VirtualBox-6.1.4\src\VBox\Devices\USB\VUSBUrb.cpp:752
static void vusbMsgDoTransfer(PVUSBURB pUrb, PVUSBSETUP pSetup, PVUSBCTRLEXTRA pExtra, PVUSBPIPE pPipe)
{
...
int rc = vusbUrbQueueAsyncRh(&pExtra->Urb);
...
}
|
1
2
3
4
5
6
7
8
9
|
// VirtualBox-6.1.4\src\VBox\Devices\USB\VUSBUrb.cpp:439
int vusbUrbQueueAsyncRh(PVUSBURB pUrb)
{
...
PVUSBDEV pDev = pUrb->pVUsb->pDev;
...
int rc = pDev->pUsbIns->pReg->pfnUrbQueue(pDev->pUsbIns, pUrb);
...
}
|
当VM主机进程解引用未初始化的pDev
时,将发生访问冲突。
为了利用未初始化的对象,我们可以在重新分配之前执行堆喷射,然后希望pDev
对象位于我们的数据中。
由于存在虚拟表调用,并且VirtualBox尚未使用CFG进行缓解,因此我们可以结合漏洞和堆喷射与伪造的pDev
对象来控制主机进程的指令指针(RIP)。
Code Execution
我们之前的文章描述了如何执行堆喷射以获取主机进程中VRAM缓冲区的地址范围。我们将选择此范围内的一个地址作为我们的伪造pDev
指针。
然后完整的漏洞利用流程如下:
- 使用E1000漏洞泄漏VBoxDD.dll模块基地址,然后收集一些ROP小工具
- 我们的伪造
pDev
指针指向VRAM中的某个位置,因此我们使用块喷洒VRAM,每个块包含:
- 对齐的
PVUSBDEV
对象,其中包含虚假的vtable,其中包含堆栈旋转小工具,以将堆栈指针指向主机的VRAM缓冲区
- 包含
WinExec
ROP链的虚假堆栈
- 喷洒堆,用我们选择的VRAM地址填充未初始化的内存,这将使
pExtra->Urb.pVUsb->pDev
对象指向我们伪造的PVUSBDEV
对象之一
- 触发OHCI漏洞,进而执行ROP链
The Patches
© 2025 STAR Labs
Powered by Hugo & PaperMod