本文介绍了两个近期公布的Virtual Box 虚拟机逃逸漏洞,问题存在于 Oracle VirtualBox 5.1.30和5.2-rc1中。漏洞的发现归功于独立安全研究人员Niklas Baumstark。目前漏洞已被提交至 Beyond Security 的 SecuriTeam。
厂商回应
Oracle已针对漏洞发布补丁。更多细节详见:http://www.oracle.com/technetwork/security-advisory/cpujan2018-3236628.html
CVE 编号:CVE: CVE-2018-2698
漏洞详情
漏洞存在于 VirtualBox 的核心图形框架(VBVA子组件)中,影响所有主机操作系统,在用户级VirtualBox主机进程中提供了任意读/写原语。
VirtualBox 模拟的VGA设备与一定数量的VRAM相关联,VRAM 在host(宿主机)上的 VM 进程中,以及 guest(客户机)内核内存中连续映射。VRAM 的一部分被用作通用的共享内存,用于 host 和 guest 之间的通信(host – guest shared memory interface,HGSMI)。借助这种共享内存机制,guest 可以向 host 发出某些命令,例如实现鼠标自动捕获和无缝窗口功能。guest 还可以通过一个名为 VDMA 的子系统,通知 host 代表其在VRAM内部复制数据。
1. vboxVDMACmdExecBpbTransfer 中存在的越界读写漏洞
VBOXVDMACMD_DMA_BPB_TRANSFER 结构体的定义如下((详见 virtualbox 源码 \include\VBox\Graphics\VBoxVideo.h):
typedef struct VBOXVDMACMD_DMA_BPB_TRANSFER { uint32_t cbTransferSize; uint32_t fFlags; union { uint64_t phBuf; VBOXVIDEOOFFSET offVramBuf; } Src; union { uint64_t phBuf; VBOXVIDEOOFFSET offVramBuf; } Dst; } VBOXVDMACMD_DMA_BPB_TRANSFER, *PVBOXVDMACMD_DMA_BPB_TRANSFER;
当发送一个类型为 VBOXVDMACMD_TYPE_DMA_BPB_TRANSFER 的 VDMA 命令时,这种类型的请求对象驻留在 HGSMI 堆中,并完全由 guest 控制。
在 host 上,一个指向该对象的指针最终被传递给\src\VBox\Devices\Graphics\DevVGA_VDMA.cpp 中的如下函数:
static int vboxVDMACmdExecBpbTransfer(PVBOXVDMAHOST pVdma, const PVBOXVDMACMD_DMA_BPB_TRANSFER pTransfer, uint32_t cbBuffer) { // ... uint32_t cbTransfer = pTransfer->cbTransferSize; uint32_t cbTransfered = 0; // ... do { uint32_t cbSubTransfer = cbTransfer; if (pTransfer->fFlags & VBOXVDMACMD_DMA_BPB_TRANSFER_F_SRC_VRAMOFFSET) { // [[ Note 1 ]] pvSrc = pvRam + pTransfer->Src.offVramBuf + cbTransfered; } else { // ... } if (pTransfer->fFlags & VBOXVDMACMD_DMA_BPB_TRANSFER_F_DST_VRAMOFFSET) { // [[ Note 2 ]] pvDst = pvRam + pTransfer->Dst.offVramBuf + cbTransfered; } else { // ... } if (RT_SUCCESS(rc)) { memcpy(pvDst, pvSrc, cbSubTransfer); cbTransfer -= cbSubTransfer; cbTransfered += cbSubTransfer; } else { cbTransfer = 0; /* to break */ } // ... } while (cbTransfer); if (RT_SUCCESS(rc)) return sizeof (*pTransfer); return rc; }
以上代码中标注的 Note 1 和 Note 2 处, guest 控制的偏移量 pTransfer-> Src.offVramBuf 和 pTransfer-> Dst.offVramBuf ,与 VRAM 地址相加,没有进行任何验证或边界检查。在之后的 memcpy 中,size 来自一个 guest 可控的变量,pTransfer->cbTransferSize。
以上,获得了 memcpy(VRAM + X, VRAM + Y, Z) ,其中 X,Y,Z 可被 guest 控制。
2. vboxVDMACmdExecBpbTransfer 中存在的越界读写漏洞
VBOXVDMACMD_DMA_PRESENT_BLT 结构体定义如下:
typedef uint64_t VBOXVIDEOOFFSET; /* [...] */ typedef struct VBOXVDMACMD_DMA_PRESENT_BLT { VBOXVIDEOOFFSET offSrc; VBOXVIDEOOFFSET offDst; VBOXVDMA_SURF_DESC srcDesc; VBOXVDMA_SURF_DESC dstDesc; VBOXVDMA_RECTL srcRectl; VBOXVDMA_RECTL dstRectl; uint32_t u32Reserved; uint32_t cDstSubRects; VBOXVDMA_RECTL aDstSubRects[1]; } VBOXVDMACMD_DMA_PRESENT_BLT, *PVBOXVDMACMD_DMA_PRESENT_BLT;
当发送一个 VBOXVDMACMD_TYPE_DMA_PRESENT_BLT 类型的 VDMA 命令时,这种类型的请求对象会驻留在 HGSMI 堆中,并完全由 guest 控制。
在 host 上,一个指向该对象的指针最终被传递给 \src\VBox\Devices\Graphics\DevVGA_VDMA.cpp中的如下函数:
static int vboxVDMACmdExecBlt(PVBOXVDMAHOST pVdma, const PVBOXVDMACMD_DMA_PRESENT_BLT pBlt, uint32_t cbBuffer) { const uint32_t cbBlt = VBOXVDMACMD_BODY_FIELD_OFFSET(uint32_t, VBOXVDMACMD_DMA_PRESENT_BLT, aDstSubRects[pBlt->cDstSubRects]); Assert(cbBlt <= cbBuffer); if (cbBuffer < cbBlt) return VERR_INVALID_FUNCTION; /* we do not support stretching for now */ Assert(pBlt->srcRectl.width == pBlt->dstRectl.width); Assert(pBlt->srcRectl.height == pBlt->dstRectl.height); if (pBlt->srcRectl.width != pBlt->dstRectl.width) return VERR_INVALID_FUNCTION; if (pBlt->srcRectl.height != pBlt->dstRectl.height) return VERR_INVALID_FUNCTION; Assert(pBlt->cDstSubRects); /* [[ Note 2 ]] */ uint8_t * pvRam = pVdma->pVGAState->vram_ptrR3; VBOXVDMA_RECTL updateRectl = {0, 0, 0, 0}; if (pBlt->cDstSubRects) { /* [...] */ } else { /* [[ Note 1 ]] */ int rc = vboxVDMACmdExecBltPerform(pVdma, pvRam + pBlt->offDst, pvRam + pBlt->offSrc, &pBlt->dstDesc, &pBlt->srcDesc, &pBlt->dstRectl, &pBlt->srcRectl); AssertRC(rc); if (!RT_SUCCESS(rc)) return rc; vboxVDMARectlUnite(&updateRectl, &pBlt->dstRectl); } return cbBlt; }
以上代码中标注的 Note 1 处, guest 可控的偏移 pBlt->offDst 和 pBlt->offSrc 与 VRAM 地址相加,没有进行任何验证或边界检查。
请注意,Note 2 中的 Assert 在生产版本中不可用,所以代码可以到达 else 分支。之后调用的 vboxVDMACmdExecBltPerform 在计算的地址之间执行一个memcpy:
static int vboxVDMACmdExecBltPerform(PVBOXVDMAHOST pVdma, uint8_t *pvDstSurf, const uint8_t *pvSrcSurf, const PVBOXVDMA_SURF_DESC pDstDesc, const PVBOXVDMA_SURF_DESC pSrcDesc, const VBOXVDMA_RECTL * pDstRectl, const VBOXVDMA_RECTL * pSrcRectl) { /* [...] /* if (pDstDesc->width == pDstRectl->width && pSrcDesc->width == pSrcRectl->width && pSrcDesc->width == pDstDesc->width) { Assert(!pDstRectl->left); Assert(!pSrcRectl->left); uint32_t cbOff = pDstDesc->pitch * pDstRectl->top; uint32_t cbSize = pDstDesc->pitch * pDstRectl->height; memcpy(pvDstSurf + cbOff, pvSrcSurf + cbOff, cbSize); } else { /* [...] /* } return VINF_SUCCESS; }
通过设置 pDstDesc-> pitch = 1,pDstRectl-> top = 0,可以得到 cbOff = 0 和 cbSize = pDstRectl-> height(这里同样是通过 guest 控制)。最后调用 memcpy(VRAM + X,VRAM + Y,Z),其中 X,Y,Z 可被 guest 控制。