导语:本文将介绍在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/如若转载,请注明原文地址: