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;
}
cssis the offset in the packet to start computing the checksum from, it needs to be less thanu16PktLenwhich is the total size of the current packet (check[1]).cseis the offset in the packet to stop computing the checksum.- Setting
csefield to 0 indicates that the checksum will cover fromcssto the end of the packet (check[3]). cseneeds to be larger thancss(check[4]).
- Setting
csois 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
aTxPacketFallbackarray (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
pThisstructure, so the overread data are the other fields of thepThisobject
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_SIZEbytes, call itcrc0. - Send the second frame containing the checksum of
E1K_MAX_TX_PKT_SIZE + 2bytes, call itcrc1. - Since the checksum algorithm is CRC-16, by calculating the difference between
crc0andcrc1, we would know the value of the two bytes right after theaTxPacketFallbackarray.
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
pExtrabody. - Part B: The newly allocated with uninitialized data.
After the reallocation:
- The
pExtra->Urb.pVUsbobject will be updated with a newpVUsb, which resides in part B (at[3]) - But the new
pVUsbresides in the uninitialized data and onlypVUsb->pUrbis 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.dllmodule base address using the E1000 vulnerability then collect some ROP gadgets - Our faked
pDEvpointer is pointing to somewhere in the VRAM, so we spray the VRAM with blocks, each containing:- aligned
PVUSBDEVobjects with fake vtable containing stack pivot gadgets to point the stack pointer to the host’s VRAM buffer - the fake stack that contains a
WinExecROP chain
- aligned
- Spray the heap, fill the uninitialized memory with our picked VRAM address, which would make the
pExtra->Urb.pVUsb->pDevobject points to one of our fakedPVUSBDEVobjects. - Trigger the OHCI vulnerability, which in turn executes the ROP chain