CVE: CVE-2020-2674

Tested Versions:

  • Oracle VirtualBox 5.2.18 revision r123745

Product URL(s):

Description of the vulnerability

VirtualBox is a x86 and AMD64/Intel64 virtualization product for enterprise as well as home use. It is a solution commercially supported by Oracle, in addition to being made available as open source software. It runs on various host platforms like Windows, Linux, Mac and Solaris and also supports a large number of guest operating systems.

OHCI (Open Host Controller Interface for USB) is the default USB Controller in both Windows and Linux guest VMs. The URB (USB Request Block) processing has a vulnerability that allows an attacker with root/administrator privileges in the guest OS to execute arbitrary code in the host OS.

When the emulated device services a transport descriptor, it’ll allocate and initialize a new URB then submit it:

//*VirtualBox-6.0.12\src\VBox\Devices\USB\DevOHCI.cpp:2956*
static bool ohciR3ServiceTd(POHCI pThis, VUSBXFERTYPE enmType, PCOHCIED pEd, uint32_t EdAddr, uint32_t TdAddr,
                            uint32_t *pNextTdAddr, const char *pszListName)
{
	...    

    /*
     * Allocate and initialize a new URB.
     */
    PVUSBURB pUrb = VUSBIRhNewUrb(pThis->RootHub.pIRhConn, pEd->hwinfo & ED_HWINFO_FUNCTION, NULL,
                                  enmType, enmDir, Buf.cbTotal, 1, NULL);
    if (!pUrb)
        return false;                   /* retry later... */
	...
    /*
     * Submit the URB.
     */
    ohciR3InFlightAdd(pThis, TdAddr, pUrb);
    Log(("%s: ohciR3ServiceTd: submitting TdAddr=%#010x EdAddr=%#010x cbData=%#x\n",
         pUrb->pszDesc, TdAddr, EdAddr, pUrb->cbData));

    ohciR3Unlock(pThis);
    int rc = VUSBIRhSubmitUrb(pThis->RootHub.pIRhConn, pUrb, &pThis->RootHub.Led);
    ohciR3Lock(pThis);
    if (RT_SUCCESS(rc))
        return true;

	...    
}

Dig into the implement of VUSBIRhSubmitUrb():

//*VirtualBox-6.0.12\include\VBox\vusb.h:884*
DECLINLINE(int) VUSBIRhSubmitUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb, struct PDMLED *pLed)
{
    return pInterface->pfnSubmitUrb(pInterface, pUrb, pLed);
}
//*VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:1339*
pThis->IRhConnector.pfnSubmitUrb                  = vusbRhSubmitUrb;
//*VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:661*
static DECLCALLBACK(int) vusbRhSubmitUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb, PPDMLED pLed)
{
	...    
    /*
     * The device was resolved when we allocated the URB.
     * Submit it to the device if we found it, if not fail with device-not-ready.
     */
    int rc;
    if (   pUrb->pVUsb->pDev
        && pUrb->pVUsb->pDev->pUsbIns)
    {
        switch (pUrb->enmDir)
        {
            case VUSBDIRECTION_IN:
                pLed->Asserted.s.fReading = pLed->Actual.s.fReading = 1;
                rc = vusbUrbSubmit(pUrb);
                pLed->Actual.s.fReading = 0;
                break;
            case VUSBDIRECTION_OUT:
                pLed->Asserted.s.fWriting = pLed->Actual.s.fWriting = 1;
                rc = vusbUrbSubmit(pUrb);
                pLed->Actual.s.fWriting = 0;
                break;
            default:
                rc = vusbUrbSubmit(pUrb);
                break;
        }

        if (RT_FAILURE(rc))
        {
            LogFlow(("vusbRhSubmitUrb: freeing pUrb=%p\n", pUrb));
            pUrb->pVUsb->pfnFree(pUrb);
        }
    }
	...
    
    return rc;
}

If the vusbUrbSubmit() function returns failure, the URB will be freed with:

pUrb->pVUsb->pfnFree(pUrb);

Then the function vusbRhSubmitUrb() also return failure.

//*VirtualBox-6.0.12\src\VBox\Devices\USB\DevOHCI.cpp:2956*
static bool ohciR3ServiceTd(POHCI pThis, VUSBXFERTYPE enmType, PCOHCIED pEd, uint32_t EdAddr, uint32_t TdAddr,
                            uint32_t *pNextTdAddr, const char *pszListName)
{
	...    
    
    int rc = VUSBIRhSubmitUrb(pThis->RootHub.pIRhConn, pUrb, &pThis->RootHub.Led);
    ohciR3Lock(pThis);
    if (RT_SUCCESS(rc))
        return true;

    /* Failure cleanup. Can happen if we're still resetting the device or out of resources. */
    Log(("ohciR3ServiceTd: failed submitting TdAddr=%#010x EdAddr=%#010x pUrb=%p!!\n",
         TdAddr, EdAddr, pUrb));
    VUSBIRhFreeUrb(pThis->RootHub.pIRhConn, pUrb);
    ohciR3InFlightRemove(pThis, TdAddr);
    return false;
}

If the URB is failed submitting with VUSBIRhSubmitUrb(), it will be freed with VUSBIRhFreeUrb():

//*VirtualBox-6.0.12\include\VBox\vusb.h:2956*
DECLINLINE(int) VUSBIRhFreeUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb)
{
    return pInterface->pfnFreeUrb(pInterface, pUrb);
}
//*VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:1338*
pThis->IRhConnector.pfnFreeUrb                    = vusbRhConnFreeUrb;
//*VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:652*
static DECLCALLBACK(int) vusbRhConnFreeUrb(PVUSBIROOTHUBCONNECTOR pInterface, PVUSBURB pUrb)
{
    RT_NOREF(pInterface);
    pUrb->pVUsb->pfnFree(pUrb);
    return VINF_SUCCESS;
}

So an URB which failed to submit will be freed twice, the first time in vusbRhSubmitUrb() and again in ohciR3ServiceTd(). The callback pUrb->pVUsb->pfnFree() is initialized at:

//*VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:392*
static PVUSBURB vusbRhNewUrb(PVUSBROOTHUB pRh, uint8_t DstAddress, PVUSBDEV pDev, VUSBXFERTYPE enmType,
                             VUSBDIRECTION enmDir, uint32_t cbData, uint32_t cTds, const char *pszTag)
{
	...
    
    if (RT_LIKELY(pUrb))
    {
        pUrb->pVUsb->pvFreeCtx = pRh;
        pUrb->pVUsb->pfnFree   = vusbRhFreeUrb;
        pUrb->DstAddress       = DstAddress;
        pUrb->pVUsb->pDev      = pDev;
	...

    return pUrb;
}
//*VirtualBox-6.0.12\src\VBox\Devices\USB\DrvVUSBRootHub.cpp:356*
static DECLCALLBACK(void) vusbRhFreeUrb(PVUSBURB pUrb)
{
    /*
     * Assert sanity.
     */
    vusbUrbAssert(pUrb);
    PVUSBROOTHUB pRh = (PVUSBROOTHUB)pUrb->pVUsb->pvFreeCtx;
    Assert(pRh);

    Assert(pUrb->enmState != VUSBURBSTATE_FREE);

    /*
     * Free the URB description (logging builds only).
     */
    if (pUrb->pszDesc)
    {
        RTStrFree(pUrb->pszDesc);
        pUrb->pszDesc = NULL;
    }

    /* The URB comes from the roothub if there is no device (invalid address). */
    if (pUrb->pVUsb->pDev)
    {
        PVUSBDEV pDev = pUrb->pVUsb->pDev;

        vusbUrbPoolFree(&pUrb->pVUsb->pDev->UrbPool, pUrb);
        vusbDevRelease(pDev);
    }
    else
        vusbUrbPoolFree(&pRh->Hub.Dev.UrbPool, pUrb);
}

The vusbRhFreeUrb() callback will free the URB with vusbUrbPoolFree() which will append the current URB to the free URB list:

//*VirtualBox-6.0.12\src\VBox\Devices\USB\VUSBUrbPool.cpp:222*
DECLHIDDEN(void) vusbUrbPoolFree(PVUSBURBPOOL pUrbPool, PVUSBURB pUrb)
{
    PVUSBURBHDR pHdr = VUSBURBPOOL_URB_2_URBHDR(pUrb);

    /* URBs which aged too much because they are too big are freed. */
    if (pHdr->cAge == VUSBURB_AGE_MAX)
    {
        ASMAtomicDecU32(&pUrbPool->cUrbsInPool);
        RTMemFree(pHdr);
    }
    else
    {
        /* Put it into the list of free URBs. */
        VUSBXFERTYPE enmType = pUrb->enmType;
        AssertReturnVoid((size_t)enmType < RT_ELEMENTS(pUrbPool->aLstFreeUrbs));
        RTCritSectEnter(&pUrbPool->CritSectPool);
        pUrb->enmState = VUSBURBSTATE_FREE;
        RTListAppend(&pUrbPool->aLstFreeUrbs[enmType], &pHdr->NdFree);
        RTCritSectLeave(&pUrbPool->CritSectPool);
    }
}

Then the USB Device is released with vusbDevRelease():

//*VirtualBox-6.0.12\src\VBox\Devices\USB\VUSBInternal.h:727*
DECLINLINE(uint32_t) vusbDevRelease(PVUSBDEV pThis)
{
    AssertPtrReturn(pThis, UINT32_MAX);

    uint32_t cRefs = ASMAtomicDecU32(&pThis->cRefs);
    AssertMsg(cRefs < _1M, ("%#x %p\n", cRefs, pThis));
    if (cRefs == 0)
        vusbDevDestroy(pThis);
    return cRefs;
}

vusbDevRelease() will decrease the reference count of the device, if there are no references left to it, the USB device will be destroyed by vusbDevDestroy():

//*VirtualBox-6.0.12\src\VBox\Devices\USB\VUSBDevice.cpp:1268*
void vusbDevDestroy(PVUSBDEV pDev)
{
    LogFlow(("vusbDevDestroy: pDev=%p[%s] enmState=%d\n", pDev, pDev->pUsbIns->pszName, pDev->enmState));

    RTMemFree(pDev->paIfStates);
    TMR3TimerDestroy(pDev->pResetTimer);
    pDev->pResetTimer = NULL;
    for (unsigned i = 0; i < RT_ELEMENTS(pDev->aPipes); i++)
    {
        Assert(pDev->aPipes[i].pCtrl == NULL);
        RTCritSectDelete(&pDev->aPipes[i].CritSectCtrl);
    }

    if (pDev->hSniffer != VUSBSNIFFER_NIL)
        VUSBSnifferDestroy(pDev->hSniffer);

    vusbUrbPoolDestroy(&pDev->UrbPool);

    RTCritSectDelete(&pDev->CritSectAsyncUrbs);
    /* Not using vusbDevSetState() deliberately here because it would assert on the state. */
    pDev->enmState = VUSB_DEVICE_STATE_DESTROYED;
    pDev->pUsbIns->pvVUsbDev2 = NULL;
    RTMemFree(pDev);
}

In normal conditions, the USB Device Object is initialized with the reference count of 1. Allocating new URBs also increases the count by 1, and conversely when URB submissions fail, the count should be decreased by 1. The last reference will held until the USB root hub is detached when the OS is shutting down.

However, this double-free bug will cause the reference count to prematurely be reduced to zero and freed. By forcing URB submissions to fail, a use-after-free vulnerability will occur. This vulnerability can be used to gain code execution outside the VM, on the host machine.

Timeline:

  • 2019-10-16 Disclosed via HITB Driven2Pwn
  • 2020-01-14 Vendor patched

Vendor Response

The vendor has acknowledged the issue and released an update to address it.

The vendor advisory can be found here: https://www.oracle.com/security-alerts/cpujan2020.html.