Disclaimer - this assumes some knowledge of how the emulated USB devices work. please refer to the VirtualBox code to gain a better understanding of how the relevant functions and structures are used.
All the emulated USB devices (EHCI, OHCI, and XHCI) use the same format for the USB Request Block (URB
) type.
URBs are allocated using the pfnNewUrb()
function, which ends up calling vusbRhConnNewUrb()
. This then calls vusbUrbPoolAlloc()
, which either fetches a pre-existing freed URB from the pUrbPool->aLstFreeUrbs
free-list, or allocates a new one:
DECLHIDDEN(PVUSBURB) vusbUrbPoolAlloc(PVUSBURBPOOL pUrbPool, VUSBXFERTYPE enmType,
VUSBDIRECTION enmDir, size_t cbData, size_t cbHci,
size_t cbHciTd, unsigned cTds)
{
// [ ... ]
RTListForEachSafe(&pUrbPool->aLstFreeUrbs[enmType], pIt, pItNext, VUSBURBHDR, NdFree)
{
// [ ... ]
// Faith: try to find an appropriately sized URB from the free list
}
if (!pHdr)
{
// [ ... ]
// Faith: if the code reaches here, it means it couldn't find an
// URB in the free-list to satisfy this allocation, so it will
// allocate a new one.
}
// [ ... ]
}
Now, after the URB is allocated, the code will call pfnSubmitUrb()
, which ends up in vusbUrbSubmit()
. In this situation, the URB's enmState
is set to VUSBURBSTATE_ALLOCATED
.
If this function returns through any error path that calls vusbUrbSubmitHardError()
, it will actually return success, which will leave the URB in the allocated state. One such error path is easy to reach:
int vusbUrbSubmit(PVUSBURB pUrb)
{
vusbUrbAssert(pUrb);
Assert(pUrb->enmState == VUSBURBSTATE_ALLOCATED);
// [ ... ]
if (pUrb->EndPt >= VUSB_PIPE_MAX)
{
Log(("%s: pDev=%p[%s]: SUBMIT: ep %i >= %i!!!\n", pUrb->pszDesc, pDev, pDev->pUsbIns->pszName, pUrb->EndPt, VUSB_PIPE_MAX));
return vusbUrbSubmitHardError(pUrb);
}
// [ ... ]
}
pUrb->EndPt
is fully controllable from the guest and can be between 0 and 31, and VUSB_PIPE_MAX
is 16.
If the code returns success here, the URB is never freed, and stays allocated.
An attacker can abuse this to empty out the URB pool and subsequently groom the heap.
This is especially nice because the URB contains a field called abData
, and it's size can be set up to 0x2000000
(with some restrictions depending on the USB device being used).
Additionally, abData
can contain completely arbitrary data, which makes it perfect to store ROP chains to pivot into, or to craft fake objects for type confusions.
I haven't used this primitive in an exploit yet, but I definitely might consider it once I find a nice bug. It is especially nice because USB is enabled by default (at least that's the case on an Ubuntu 22.04 Server guest).