In this post, we will cover the vulnerabilities used at Pwn2Own 2020 for the Oracle VirtualBox escape. These two vulnerabilities affect Oracle VirtualBox 6.1.4 and prior versions.
The Vulnerabilities
The exploit chain includes 2 vulnerabilities:
- Intel PRO 1000 MT Desktop (E1000) Network Adapter - Out-Of-Bounds Read Vulnerability https://www.zerodayinitiative.com/advisories/ZDI-20-581/
- Open Host Controller Interface (OHCI) USB Controller - Uninitialized Variable https://www.zerodayinitiative.com/advisories/ZDI-20-582/
E1000 Out-Of-Bounds Read Vulnerability
For more information about the inner workings of the E1000 Network Adapter, you can read about it here.
While sending an Ethernet frame with the E1000 network adapter, we can control the insertion of the IP checksum by setting the IXSM
bit in the Data Descriptor Option Field:
// 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;
...
}
With pThis->fIPcsum
flag enabled, an IP checksum will be inserted to the Ethernet frame:
// 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);
Function e1kInsertChecksum()
will compute the checksum and puts it in the frame body. The three fields u8CSO
, u8CSS
and u16CSE
of pThis->contextNormal
can be specified by the Context Descriptor:
// 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);
}
...
}
The implementation of function e1kInsertChecksum()
:
// 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
is the offset in the packet to start computing the checksum from, it needs to be less thanu16PktLen
which is the total size of the current packet (check[1]
).cse
is the offset in the packet to stop computing the checksum.- Setting
cse
field to 0 indicates that the checksum will cover fromcss
to the end of the packet (check[3]
). cse
needs to be larger thancss
(check[4]
).
- Setting
cso
is the offset in the packet to write the checksum at, it needs to be less thanu16PktLen - 1
(check[2]
).
Since there is no check against the maximum value of cse
, we can set this field to be larger than the total size of the current packet, leading to an out-of-bounds access and causes e1kCSum16()
to calculate the checksum of the data right after the packet body pPkt
.
The “overread” checksum will be inserted into the Ethernet frame and can be read by the receiver later.
Information Leakage
So if we want to leak some information from an overread checksum, we need a reliable way to know which data is adjacent to the overread buffer. In the emulated E1000 device, the transmit buffer is allocated by e1kXmitAllocBuf()
function:
// 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;
}
The LBM
(loopback mode) field in the RCTL
register controls the loopback mode of the Ethernet controller, it affects how the packet buffer is allocated (see [1]
):
- Without loopback mode:
e1kXmitAllocBuf()
usespDrv->pfnAllocBuf()
callback to allocates the packet buffer, this callback will use either OS allocator or VirtualBox’s custom one. - With loopback mode: the packet buffer is the
aTxPacketFallback
array (see[2]
).
The aTxPacketFallback
array is a property of the PE1KSTATE pThis
object:
// 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;
So by enabling the loopback mode:
- The packet receiver is us, we don’t need another host to read the overread checksum
- The packet buffer resides in the
pThis
structure, so the overread data are the other fields of thepThis
object
Now we know which data is adjacent to the packet buffer, we can leak word-by-word with the following steps:
- Send a frame containing the CRC-16 checksum of
E1K_MAX_TX_PKT_SIZE
bytes, call itcrc0
. - Send the second frame containing the checksum of
E1K_MAX_TX_PKT_SIZE + 2
bytes, call itcrc1
. - Since the checksum algorithm is CRC-16, by calculating the difference between
crc0
andcrc1
, we would know the value of the two bytes right after theaTxPacketFallback
array.
Keep increasing the overread size by 2 bytes each time and doing this until we get some interesting data. Fortunately, after the pThis
object, we can find a pointer to a global variable in the VBoxDD.dll
module at offset E1K_MAX_TX_PKT_SIZE + 0x1f7
.
One small problem is that in the pThis
object, after the aTxPacketFallback
array, there are other device’s counter registers that keep increasing each time a frame is sent, so if we send two frames with a same overread size, it also results in two different checksums, but the counter increment is similar each time so this difference is predictable and can be equalized by adding 0x5a
to the second checksum.
OHCI Controller Uninitialized Variable
You can read more about the VirtualBox OHCI device here.
While sending a control message URB to the USB device, we can include a setup packet to update the message URB:
// 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;
// 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
is our URB packet, pExtra
is the current extra data for a control pipe, if the size of the setup request is larger than the size of the current control pipe extra data (check [1]
), pExtra
will be reallocated with a bigger size at [2]
.
The original pExtra
was allocated and initialized in vusbMsgAllocExtraData()
:
// 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;
}
Function RTMemRealloc()
doesn’t perform any initialization so the resulting buffer will contain two parts:
- Part A: The old and small
pExtra
body. - Part B: The newly allocated with uninitialized data.
After the reallocation:
- The
pExtra->Urb.pVUsb
object will be updated with a newpVUsb
, which resides in part B (at[3]
) - But the new
pVUsb
resides in the uninitialized data and onlypVUsb->pUrb
is updated at[4]
,
So the other properties of pExtra->Urb.pVUsb
object remain uninitialized, include the pExtra->Urb.pVUsb->pDev
object (see [5]
).
pExtra->Urb
object will be used later in vusbMsgDoTransfer()
function:
// 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);
...
}
// 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);
...
}
An access violation will occur when the VM host process dereferencs the uninitialized pDev
.
To take advantage of the uninitialized object, we can perform a heap spraying before the reallocation then hope the pDev
object will have resided in our data.
Since there is a virtual table call and VirtualBox hasn’t mitigated with CFG yet so we can combine the vulnerability and heap spraying with faked pDev
objects to control the host process’ instruction pointer (RIP).
Code Execution
Our previous post describes how to perform heap spraying to obtain the address range of the VRAM buffer in the host process. We will pick one address within this range as our faked pDEv
pointer.
Then the full exploit flow will be like:
- Leak the
VBoxDD.dll
module base address using the E1000 vulnerability then collect some ROP gadgets - Our faked
pDEv
pointer is pointing to somewhere in the VRAM, so we spray the VRAM with blocks, each containing:- aligned
PVUSBDEV
objects with fake vtable containing stack pivot gadgets to point the stack pointer to the host’s VRAM buffer - the fake stack that contains a
WinExec
ROP chain
- aligned
- Spray the heap, fill the uninitialized memory with our picked VRAM address, which would make the
pExtra->Urb.pVUsb->pDev
object points to one of our fakedPVUSBDEV
objects. - Trigger the OHCI vulnerability, which in turn executes the ROP chain