Pwn2Own 2020 利用 Oracle VirtualBox 网卡、USB 设备驱动实现虚拟机逃逸的细节
2020-11-24 10:38:08 Author: www.4hou.com(查看原文) 阅读量:227 收藏

导语:本文将介绍在Pwn2Own 2020中的Oracle VirtualBox逃逸漏洞,这两个漏洞会影响Oracle VirtualBox 6.1.4和更低的版本。

本文将介绍在Pwn2Own 2020中的Oracle VirtualBox逃逸漏洞,这两个漏洞会影响Oracle VirtualBox 6.1.4和更低的版本。

0x01 漏洞分析

漏洞利用链包括2个漏洞:

· 英特尔PRO 1000 MT桌面(E1000)网络适配器-越界读取漏洞

https://www.zerodayinitiative.com/advisories/ZDI-20-581/

· 开放主机控制器接口(OHCI)USB控制器-未初始化变量漏洞

https://www.zerodayinitiative.com/advisories/ZDI-20-582/

1.E1000越界读取漏洞

有关E1000网络适配器内部工作的更多信息,可以在此处阅读有关信息。

https://github.com/hongphipham95/Vulnerabilities/blob/master/VirtualBox/Oracle VirtualBox Intel PRO 1000 MT Desktop - Integer Underflow Vulnerability/Oracle VirtualBox Intel PRO 1000 MT Desktop - Integer Underflow Vulnerability.md

使用E1000网络适配器发送以太网帧时,我们可以通过设置IXSM数据描述符选项字段中的位来控制IP校验和的插入:

// 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校验将被插入到以太网帧:

// 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()将计算校验和并将其放入框架中,u8CSO,u8CSS以及u16CSE中pThis->contextNormal可以通过上下文描述符指定:

// 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():

// 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当前数据包的总大小(check [1])。

· cse是数据包中的偏移量,用于停止计算校验和。

     · 将cse字段设置为0表示校验和将覆盖css到数据包的末尾(校验[3])。

     · cse需要大于css(检查[4])。

· cso是要写入校验和的数据包中的偏移量,它必须小于u16PktLen - 1(check [2])。

由于不检查cse的最大值,我们可以将此字段设置为大于当前数据包的总大小,从而导致越界访问,并导致e1kCSum16()在数据包之后立即计算数据的校验和pPkt。

“ overread”校验和将被插入以太网帧中,稍后可以由接收器读取。

信息泄漏利用

因此,如果我们想从过校验和中泄漏一些信息,我们需要一种可靠的方法来知道哪些数据与过缓冲器相邻。在仿真的E1000设备中,发送缓冲区由以下e1kXmitAllocBuf()函数分配:

// 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;
}

寄存器中的LBM(环回模式)字段RCTL控制以太网控制器的环回模式,它会影响数据包缓冲区的分配方式(请参阅参考资料[1]):

· 没有回送模式:e1kXmitAllocBuf()使用pDrv->pfnAllocBuf()回调分配数据包缓冲区,此回调将使用OS分配器或VirtualBox的自定义分配器。

· 使用回送模式:数据包缓冲区是aTxPacketFallback数组(请参阅参考资料[2])。

aTxPacketFallback是一个PE1KSTATE pThis属性对象:

// 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对象之后,我们可以在offset处找到指向全局变量VBoxDD.dll的指针E1K_MAX_TX_PKT_SIZE + 0x1f7。

在pThis对象之后,aTxPacketFallback数组之后,每发送一帧,其他设备的计数器寄存器就会不断增加,因此,如果我们发送两个具有相同大小的帧,也会导致两个不同的校验和,但是每次计数器的增量都是相似的,因此这种差异是可以预测的,可以通过加到0x5a第二个校验和上来使之相等。

2.OHCI控制器未初始化的变量漏洞

可以在此处阅读有关VirtualBox OHCI设备的更多信息。

https://github.com/hongphipham95/Vulnerabilities/blob/master/VirtualBox/Oracle VirtualBox OHCI Use-After-Free Vulnerability/Oracle VirtualBox OHCI Use-After-Free.md

在向USB设备发送控制消息URB时,我们可以包含一个设置包来更新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是我们的URB数据包,pExtra是控制管道的当前额外数据,如果设置请求的大小大于当前控制管道的额外数据(检查[1])的大小,则会在pExtra处以更大的大小重新分配[2]。

原始文件pExtra已在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;
}

函数RTMemRealloc()不执行任何初始化,因此生成的缓冲区将包含两部分:

· A部分:pExtra。

· B部分:新分配的未初始化数据。

重新分配后:

· pExtra->Urb.pVUsb对象将用新的更新pVUsb,它驻留在B部分(在[3])

· 但是新内容pVUsb驻留在未初始化的数据中,并且仅pVUsb->pUrb在更新[4],

因此,pExtra->Urb.pVUsb对象的其他属性保持未初始化,包括pExtra->Urb.pVUsb->pDev对象(请参阅参考资料[5])。

pExtra->Urb对象将在以后的vusbMsgDoTransfer()函数中使用:

// 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);
    ...    
}

当VM主机进程取消引用未初始化的时,将发生pDev访问冲突。

为了利用未初始化的对象,我们可以在重新分配之前执行堆喷,然后希望该pDev对象已驻留在我们的数据中。

由于存在虚表调用,而VirtualBox尚未通过CFG缓解,因此我们可以将漏洞和伪造pDev对象的堆喷射结合起来,以控制主机进程的指令指针(RIP)。

代码执行利用

我们之前的文章描述了如何执行堆喷涂以在主机进程中获取VRAM缓冲区的地址范围,我们将在此范围内选择一个地址作为伪造的pDEv指针。

https://starlabs.sg/blog/2020/04/adventures-in-hypervisor-oracle-virtualbox-research/

然后,完整的利用流程将如下所示:

· VBoxDD.dll使用E1000漏洞泄漏模块基地址,然后收集一些ROP gadget

· 我们伪造的pDEv指针指向VRAM中的某个地方,因此我们在VRAM中喷射了多个块,每个块包含:

     · PVUSBDEV使用包含堆栈gadget的假vtable对齐对象,以将堆栈指针指向主机的VRAM缓冲区

     · 包含WinExecROP链的假堆栈

· 堆喷,用我们选择的VRAM地址填充未初始化的内存,这将使pExtra->Urb.pVUsb->pDev对象指向伪造的PVUSBDEV对象之一。

· 触发OHCI漏洞,进而执行ROP链

0x02 漏洞补丁

· https://www.virtualbox.org/changeset/83613/vbox/trunk/src/VBox/Devices/Network/DevE1000.cpp

trunk/src/VBox/Devices/Network/DevE1000.cpp
r82968 r83613  
4171 4171     } 
4172 4172  
4173       if (cse == 0) 
  4173     if (cse == 0 || cse >= u16PktLen) 
4174 4174         cse = u16PktLen - 1; 
4175 4175     else if (cse < css)

· https://www.virtualbox.org/changeset/83617/vbox/trunk/src/VBox/Devices/USB/VUSBUrb.cpp

trunk/src/VBox/Devices/USB/VUSBUrb.cpp
r83592 r83617  
703 703     if (pExtra->cbMax < cbBuf + pSetupIn->wLength + sizeof(VUSBURBVUSBINT)) 
704 704     { 
  705 #if 1 
  706         LogRelMax(10, ("VUSB: Control URB too large (wLength=%u)!\n", pSetupIn->wLength)); 
  707         return false; 
  708 #else 
705 709         uint32_t cbReq = RT_ALIGN_32(cbBuf + pSetupIn->wLength + sizeof(VUSBURBVUSBINT), 1024); 
706 710         PVUSBCTRLEXTRA pNew = (PVUSBCTRLEXTRA)RTMemRealloc(pExtra, RT_UOFFSETOF_DYN(VUSBCTRLEXTRA, Urb.abData[cbReq])); 
… …  
717 721             pPipe->pCtrl = pExtra; 
718 722         } 
  723  
  724         PVUSBURBVUSB pOldVUsb = (PVUSBURBVUSB)&pExtra->Urb.abData[pExtra->cbMax - sizeof(VUSBURBVUSBINT)]; 
719 725         pExtra->Urb.pVUsb = (PVUSBURBVUSB)&pExtra->Urb.abData[cbBuf + pSetupIn->wLength]; 
  726         memmove(pExtra->Urb.pVUsb, pOldVUsb, sizeof(VUSBURBVUSBINT)); 
  727         memset(pOldVUsb, 0, (uint8_t *)pExtra->Urb.pVUsb - (uint8_t *)pOldVUsb); 
720 728         pExtra->Urb.pVUsb->pUrb = &pExtra->Urb; 
  729         pExtra->Urb.pVUsb->pvFreeCtx = &pExtra->Urb; 
721 730         pExtra->cbMax = cbReq; 
  731  
  732 #endif 
722 733     } 
723 734     Assert(pExtra->Urb.enmState == VUSBURBSTATE_ALLOCATED);

本文翻译自:https://starlabs.sg/blog/2020/09/pwn2own-2020-oracle-virtualbox-escape/如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/LnjD
如有侵权请联系:admin#unsafe.sh