Pwn2Own 2020:Oracle VirtualBox逃逸漏洞深度解析

本文详细分析了Pwn2Own 2020比赛中用于Oracle VirtualBox逃逸的两个关键漏洞:E1000网络适配器的越界读取漏洞和OHCI USB控制器的未初始化变量漏洞,涵盖技术原理、利用链构建和修复方案。

Pwn2Own 2020: Oracle VirtualBox Escape

September 25, 2020 · 9 min · Pham Hong Phi (@4nhdaden)

目录

漏洞概述

本文将介绍在Pwn2Own 2020中用于Oracle VirtualBox逃逸的漏洞链。这两个漏洞影响Oracle VirtualBox 6.1.4及更早版本。

漏洞链包含两个漏洞:

  1. Intel PRO 1000 MT Desktop (E1000) 网络适配器 - 越界读取漏洞 https://www.zerodayinitiative.com/advisories/ZDI-20-581/
  2. 开放主机控制器接口(OHCI) USB控制器 - 未初始化变量漏洞 https://www.zerodayinitiative.com/advisories/ZDI-20-582/

E1000越界读取漏洞

关于E1000网络适配器内部工作原理的更多信息,可参考相关文档。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 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)
            {
                /*
                 * 第一个片段:保存IXSM和TXSM选项
                 * 因为这些选项仅在第一个片段中有效
                 */
                pThis->fIPcsum  = pDesc->data.dw3.fIXSM;
                pThis->fTCPcsum = pDesc->data.dw3.fTXSM;
                        fTSE     = pDesc->data.cmd.fTSE;
    	...                    
}

pThis->fIPcsum标志启用时,IP校验和将被插入到以太网帧中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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()将计算校验和并将其放入帧体中。pThis->contextNormal的三个字段u8CSOu8CSSu16CSE可以通过上下文描述符指定:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 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()的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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)大于包长度-1(%X),不插入校验和\n",
                 pThis->szPrf, cso, u16PktLen));
        return;
    }

    if (cso >= u16PktLen - 1)						// [2]
    {
        E1kLog2(("%s cso(%X)大于包长度-2(%X),不插入校验和\n",
                 pThis->szPrf, cso, u16PktLen));
        return;
    }

    if (cse == 0)									// [3]
        cse = u16PktLen - 1;
    else if (cse < css)								// [4]
    {
        E1kLog2(("%s css(%X)大于cse(%X),不插入校验和\n",
                 pThis->szPrf, css, cse));
        return;
    }

    uint16_t u16ChkSum = e1kCSum16(pPkt + css, cse - css + 1);
    E1kLog2(("%s 插入校验和:%04X位于%02X,旧值:%04X\n", 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之后数据的校验和。 “过度读取"的校验和将被插入到以太网帧中,并可以被接收方读取。

信息泄露

如果我们想通过过度读取的校验和泄露某些信息,需要一种可靠的方法来知道哪些数据与过度读取缓冲区相邻。在模拟的E1000设备中,发送缓冲区由e1kXmitAllocBuf()函数分配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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
    {
        /* 使用回退缓冲区和预分配的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; /* 此处无GSO */
        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(环回模式)字段控制以太网控制器的环回模式,它影响包缓冲区的分配方式(见[1]):

  • 无环回模式:e1kXmitAllocBuf()使用pDrv->pfnAllocBuf()回调分配包缓冲区,此回调将使用操作系统分配器或VirtualBox的自定义分配器。
  • 有环回模式:包缓冲区是aTxPacketFallback数组(见[2])。

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// VirtualBox-6.1.4\src\VBox\Devices\Network\DevE1000.cpp:1024
typedef struct E1KSTATE
{
    ...
    /** TX: 用于TSE回退和环回的发送包缓冲区 */
    uint8_t     aTxPacketFallback[E1K_MAX_TX_PKT_SIZE];
    /** TX: 在TX包缓冲区中组装的字节数 */
    uint16_t    u16TxPktLen;
    ...    
} E1KSTATE;

/* 指向E1000设备状态的指针 */
typedef E1KSTATE *PE1KSTATE;

通过启用环回模式:

  • 包接收者是我们自己,不需要另一台主机来读取过度读取的校验和
  • 包缓冲区位于pThis结构中,因此过度读取的数据是pThis对象的其他字段

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

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

每次将过度读取大小增加2字节,重复此过程直到获得一些有趣的数据。幸运的是,在pThis对象之后,我们可以在偏移量E1K_MAX_TX_PKT_SIZE + 0x1f7处找到指向VBoxDD.dll模块中全局变量的指针。

一个小问题是,在pThis对象中,aTxPacketFallback数组之后还有其他设备的计数器寄存器,这些寄存器在每次发送帧时都会增加,因此如果我们发送两个具有相同过度读取大小的帧,也会产生两个不同的校验和,但计数器增量每次都是相似的,因此这种差异是可预测的,可以通过向第二个校验和添加0x5a来均衡。

OHCI控制器未初始化变量

关于VirtualBox OHCI设备的更多信息,可参考相关文档。

在向USB设备发送控制消息URB时,可以包含一个设置包来更新消息URB:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 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: 内存不足!!! 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);

    /*
     * 复制设置数据并准备数据
     */
    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]处重新分配为更大的大小。

原始的pExtravusbMsgAllocExtraData()中分配和初始化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// VirtualBox-6.1.4\src\VBox\Devices\USB\VUSBUrb.cpp:609
static PVUSBCTRLEXTRA vusbMsgAllocExtraData(PVUSBURB pUrb)
{
/** @todo 重用这些? */
    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)];
       
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计