利用Oracle VirtualBox实现虚拟机逃逸

  • A+
所属分类:安全文章

利用Oracle VirtualBox实现虚拟机逃逸


在这篇文章中,我们将讨论在Pwn2Own 2020中使用的Oracle VirtualBox escape漏洞。这两个漏洞影响Oracle VirtualBox 6.1.4和更早的版本。

 

利用Oracle VirtualBox实现虚拟机逃逸
漏洞

利用Oracle VirtualBox实现虚拟机逃逸

我们利用了两个漏洞完成了这次逃逸:

CVE-2020-2894
CVE-2020-2575

(请点击“阅读原文”查看链接)


CVE-2020-2894 E1000 越界读取漏洞

有关E1000网络适配器内部工作原理的更多信息,可以请点击“阅读原文”查看链接阅读。

当使E1000网络适配器发送一个以太网帧时,我们可以通过设置IXSM位控制插入的IP校验和:

// VirtualBox-6.1.4srcVBoxDevicesNetworkDevE1000.cpp:5191static 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.4srcVBoxDevicesNetworkDevE1000.cpp:4997static 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()将计算校验和并将其放入框架主体中。pThis->contextNormal的三个字段u8CSOu8CSSu16CSE可以通过上下文描述符(Context Descriptor)指定:

// VirtualBox-6.1.4srcVBoxDevicesNetworkDevE1000.cpp:5158DECLINLINE(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.4srcVBoxDevicesNetworkDevE1000.cpp:4155static 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 insertedn", pThis->szPrf, cso, u16PktLen)); return; }
if (cso >= u16PktLen - 1) // [2] { E1kLog2(("%s cso(%X) is greater than packet length-2(%X), checksum is not insertedn", 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 insertedn", pThis->szPrf, css, cse)); return; }
uint16_t u16ChkSum = e1kCSum16(pPkt + css, cse - css + 1); E1kLog2(("%s Inserting csum: %04X at %02X, old value: %04Xn", pThis->szPrf, u16ChkSum, cso, *(uint16_t*)(pPkt + cso))); *(uint16_t*)(pPkt + cso) = u16ChkSum;}

css是数据包中开始计算校验和的偏移量,它需要小于u16PktLen,它是当前数据包的总大小(代码中[1])。

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

cse字段设置为0表示校验和将覆盖从css到包的末尾(代码中[3])。

cse需要比css大(代码中[4])。

cso是数据包中写入校验和的偏移量,它需要小于u16PktLen – 1(代码中[2])。

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

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


信息泄漏

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

// VirtualBox-6.1.4srcVBoxDevicesNetworkDevE1000.cpp:3833DECLINLINE(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;}

RCTL寄存器的LBM(环回模式)字段控制以太网控制器的环回模式,它影响包缓冲区(packet buffer)的分配(代码中[1]):

没有环回模式:e1kXmitAllocBuf()使用pDrv->pfnAllocBuf()回调来分配数据包缓冲区,这个回调将使用OS分配器或VirtualBox的自定义分配器。
环回模式:数据包缓冲区是
aTxPacketFallback数组(代码中[2])。

aTxPacketFallback数组是PE1KSTATE pThis对象的属性:

// VirtualBox-6.1.4srcVBoxDevicesNetworkDevE1000.cpp:1024typedef 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;

因此,通过启用环回模式可以做到:

数据包接收方是我们,我们不需要另一个主机来读取overread校验和
数据包缓冲区驻留在
pThis结构中,因此被覆盖的数据是pThis`对象的其他字段

现在我们知道了哪些数据是与数据包缓冲区相邻的,我们可以通过以下步骤泄露信息:

发送包含E1K_MAX_TX_PKT_SIZE字节的CRC-16校验和的帧,称其为crc0
发送包含
E1K_MAX_TX_PKT_SIZE + 2字节校验和的第二帧,称为crc1
由于校验和算法是
CRC-16,通过计算crc0和crc1之间的差异,我们可以知道紧跟在aTxPacketFallback数组之后的两个字节的值。

每次增加2字节的大小,直到我们得到一些有趣的数据。幸运的是,在pThis对象之后,我们可以在VBoxDD.dll模块的E1K_MAX_TX_PKT_SIZE0x1f7处找到一个指向全局变量的指针。

一个小问题是,在pThis对象中,aTxPacketFallback数组后,还有其他设备的计数器寄存器,每次发送帧都会增加。即使我们发送两个帧大小相同,它也导致了两种不同的校验和。但由于每次的增加是可以预测的,我们可以添加0x5a到第二个校验和中使得两个校验和一致。


OHCI控制器没有初始化变量

你可以在这里阅读更多关于VirtualBox OHCI设备的信息。

当发送一个控制消息URB到USB设备中时,我们可以在其中夹带一个设置包来更新消息URB:

// VirtualBox-6.1.4srcVBoxDevicesUSBVUSBUrb.cpp:834static int vusbUrbSubmitCtrl(PVUSBURB pUrb){    ...        if (pUrb->enmDir == VUSBDIRECTION_SETUP)    {        LogFlow(("%s: vusbUrbSubmitCtrl: pPipe=%p state %s->SETUPn",                 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.4srcVBoxDevicesUSBVUSBUrb.cpp:664static 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 %zun", 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])。

下面的代码演示了在vusbMsgAllocExtraData()中分配初始化的pExtra

// VirtualBox-6.1.4srcVBoxDevicesUSBVUSBUrb.cpp:609static 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

在重新分配后:

pExtra->Urb.pVUsb对象将被更新为新的pVUsb,它驻留在B部分(代码中[3])
但是新的
pVUsb驻留在未初始化的数据中,只有pVUsb->pUrb在代码中[4]的地方更新。

此时pExtra->Urb.pVUsb对象仍然未初始化,包括pExtra->Urb.pVUsb->pDev对象(代码中[5])。

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

// VirtualBox-6.1.4srcVBoxDevicesUSBVUSBUrb.cpp:752static void vusbMsgDoTransfer(PVUSBURB pUrb, PVUSBSETUP pSetup, PVUSBCTRLEXTRA pExtra, PVUSBPIPE pPipe){    ...        int rc = vusbUrbQueueAsyncRh(&pExtra->Urb);    ...    }
// VirtualBox-6.1.4srcVBoxDevicesUSBVUSBUrb.cpp:439int vusbUrbQueueAsyncRh(PVUSBURB pUrb){    ...        PVUSBDEV pDev = pUrb->pVUsb->pDev;    ...        int rc = pDev->pUsbIns->pReg->pfnUrbQueue(pDev->pUsbIns, pUrb);    ...    }

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

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

由于存在一个虚拟表调用,并且VirtualBox使用了CFG。我们可以结合漏洞、堆喷射和伪造的pDev对象来控制主机进程的指令指针(RIP)。


代码执行

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

那么完整的利用过程将如下:

1.使用E1000漏洞获取VBoxDD.dll模块基地址,然后收集一些ROP gadgets
2.我们伪造的
pDEv指针指向VRAM中的某个地方,所以我们在VRAM中喷射block,每个block包含:
1)用包含stack pivot的假的虚函数对齐
PVUSBDEV对象,以指向堆栈指针主机的VRAM缓冲区
2)包含
WinExecROP链的伪堆栈
3.用我们选择的
VRAM地址填充未初始化的内存完成堆喷射,这将使pExtra->Urb.pVUsb->pDev对象指向一个伪造的PVUSBDEV对象。
4.触发
OHCI漏洞,进而执行ROP

 

利用Oracle VirtualBox实现虚拟机逃逸
补丁

利用Oracle VirtualBox实现虚拟机逃逸

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

(请点击“阅读原文”查看链接)

利用Oracle VirtualBox实现虚拟机逃逸


- End -

精彩推荐

微软再爆“死亡之ping”漏洞

专项行动的意外收获——2020年9月墨子(Mozi)僵尸网络分析报告

CVE-2020-3535:Cisco Webex Teams windows客户端dll劫持漏洞分析

使用fuzzilli对Javascript引擎QuickJS进行Fuzzing和漏洞分析

利用Oracle VirtualBox实现虚拟机逃逸


戳“阅读原文”查看更多内容

本文始发于微信公众号(安全客):利用Oracle VirtualBox实现虚拟机逃逸

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: