Oracle VM VirtualBox – VM Escape via VGA Device

May 17, 2025 - 12:45
 0  0
Oracle VM VirtualBox – VM Escape via VGA Device

Summary

An integer overflow vulnerability exists within the VirtualBox vmsvga3dSurfaceMipBufferSize [source] function. This vulnerability allows an attacker to manipulate a malloc call such that 0 bytes are allocated while VirtualBox tracks the size of the buffer as a value greater than 0.

An attacker can exploit this condition and achieve linear read/write primitives which can then be escalated to arbitrary read/write access within the host's memory. We provide a proof-of-concept that demonstrates how to exploit this vulnerability to fully escape a virtual machine.

Severity

High -

Proof of Concept

We were able to exploit the VMSVGAGBO defined inside a VMSVGAMOB object, which are defined below:

typedef struct VMSVGAGBO
{
    uint32_t                fGboFlags;
    uint32_t                cTotalPages;
    uint32_t                cbTotal;
    uint32_t                cDescriptors;
    PVMSVGAGBODESCRIPTOR    paDescriptors;
    void                   *pvHost; /* Pointer to cbTotal bytes on the host if VMSVGAGBO_F_HOST_BACKED is set. */
} VMSVGAGBO, *PVMSVGAGBO;

typedef struct VMSVGAMOB
{
    AVLU32NODECORE          Core; /* Key is the mobid. */
    RTLISTNODE              nodeLRU;
    VMSVGAGBO               Gbo;
} VMSVGAMOB, *PVMSVGAMOB;

The algorithm is the following:

  1. Trigger the allocation of a buggy_surface (surface allocated with size 0)
  2. Allocate a GBO object with a value in cbTotal that could be used to finger print our object (a.k.a an “egg”). After trial an error the value 0x1421337 proved to be reliable enough (~100% success rate)
  3. Exploit the out of bounds read and check the bytes just after buggy_surface if we can find the egg within a short range (the first 0x5a bytes), assume that our target GBO object is allocated right after the surface.
  4. If not, go back to 1

This algorithm proved to be 100% reliable to get a heap grooming within the first <10 attempts.

An arbitrary read can be achieved by corrupting cbTotal and pvHost using the linear write out of bounds with values of the attacker’s choice, then a guest can issue a vmsvga3dDXReadbackCOTable command, that will end up calling vmsvgaR3MobBackingStoreWriteToGuest which will use both corrupted variables to write cbTotal bytes from pvHost into the guest memory.

Similarly, an arbitrary write can be achieved with a GrowCOTable command, which upon calling vmsvgaR3MobBackingStoreCreate will eventually result in the device reading cbTotal bytes from guest memory into pvHost.

Arbitrary Heap Allocation

Another useful primitive that can be achieved via the GrowCOTable is the ability to allocate arbitrary chunks of heap memory, this is done via corrupting the fGboFlags field, which will result in the device allocating a chunk of memory of cbTotal size. This primitive proved to be useful later on, to get a place to store a shellcode for the last exploit stage.

Breaking ASLR and Gaining RIP Control

Another huge benefit of the VMSVGAMOB object is that the field nodeLRU contains a pointer to the VMSVGAR3STATE structure of the device.

The latter struct is helpful because it contains a variety of function pointers that can be corrupted via the arbitrary write primitive and later on used to get RIP control and arbitrary code execution.

Escaping the VM

  1. Leak the value of the function pointer pfnCommandClear
  2. Deduce the base of VBoxDD.so with the value from 1
  3. Read the GOT table of VBoxDD.so to find a function pointer that will lead to the base of VBoxRT.so`
  4. Using the arbitrary heap allocation primitive to plant a shellcode
  5. Construct a ROP chain with gadgets found on both files
  6. 5a. Pivoting the stack to a controlled section of the heap
  7. 5b. Call memprotect to make the shellcode location executable
  8. 5c. Jump into the shellcode
  9. Corrupt the value of pfnCommandClear with the first ROP chain gadget pivoting the stack
  10. Issue a vmsvga3dCommandClear command
  11. Initiate RCE

Further Analysis

Linear out-of-bounds read

Assuming the guest allocates two surfaces:

  1. Buggy_surface that has an allocation of 0 backing it up and that will hold the role of src
  2. Transfer_surface with a valid size and allocation and that will hold the role of dest

The following steps will perform a linear out-of-bounds read of an almost arbitrary size, transferring the contents from buggy_surface to transfer_surface

In order to achieve linear read out of bounds, a guest can issue the command SVGA_3D_CMD_DX_BUFFER_COPY which transfers data between two surfaces

    /*
     * Map the source buffer.
     */
    VMSVGA3D_MAPPED_SURFACE mapBufferSrc;
    rc = vmsvga3dSurfaceMap(pThisCC, &imageBufferSrc, NULL, VMSVGA3D_SURFACE_MAP_READ, &mapBufferSrc);
    if (RT_SUCCESS(rc))
    {
        /*
         * Map the destination buffer.
         */
        VMSVGA3D_MAPPED_SURFACE mapBufferDest;
        rc = vmsvga3dSurfaceMap(pThisCC, &imageBufferDest, NULL, VMSVGA3D_SURFACE_MAP_WRITE, &mapBufferDest);
        if (RT_SUCCESS(rc))
        {
            /*
             * Copy the source buffer to the destination.
             */
            uint8_t const *pu8BufferSrc = (uint8_t *)mapBufferSrc.pvData;
            uint32_t const cbBufferSrc = mapBufferSrc.cbRow;

            uint8_t *pu8BufferDest = (uint8_t *)mapBufferDest.pvData;
            uint32_t const cbBufferDest = mapBufferDest.cbRow;

            if (   pCmd->srcX < cbBufferSrc
                && pCmd->width <= cbBufferSrc- pCmd->srcX
                && pCmd->destX < cbBufferDest
                && pCmd->width <= cbBufferDest - pCmd->destX)
            {
                RT_UNTRUSTED_VALIDATED_FENCE();

                memcpy(&pu8BufferDest[pCmd->destX], &pu8BufferSrc[pCmd->srcX], pCmd->width);
            }

The source argument of the memcpy operation mapBufferSrc.pvData corresponds to the buffer previously allocated with size 0.

The condition that guards the memcpy call can be bypassed due to how cbBufferSrc (and by extension mapBufferSrc.cbRow) is calculated:
vmsvga3dSurfaceMap ends up calling vmsvga3dSurfaceMapInit with the dimensions of the surface that were calculated in the previous step.

 else
    {
        clipBox.x = 0;
        clipBox.y = 0;
        clipBox.z = 0;
        clipBox.w = pMipLevel->mipmapSize.width;
        clipBox.h = pMipLevel->mipmapSize.height;
        clipBox.d = pMipLevel->mipmapSize.depth;
    }

    /// @todo Zero the box?
    //if (enmMapType == VMSVGA3D_SURFACE_MAP_WRITE_DISCARD)
    //    RT_BZERO(.);

    vmsvga3dSurfaceMapInit(pMap, enmMapType, &clipBox, pSurface,
                           pMipLevel->pSurfaceData, pMipLevel->cbSurfacePitch, pMipLevel->cbSurfacePlane);

Inside vmsvga3dSurfaceMapInit these dimensions will be used to determine the value of cbRow

void vmsvga3dSurfaceMapInit(VMSVGA3D_MAPPED_SURFACE *pMap, VMSVGA3D_SURFACE_MAP enmMapType, SVGA3dBox const *pBox,
                            PVMSVGA3DSURFACE pSurface, void *pvData, uint32_t cbRowPitch, uint32_t cbDepthPitch)
{
    uint32_t const cxBlocks = (pBox->w + pSurface->cxBlock - 1) / pSurface->cxBlock;
    uint32_t const cyBlocks = (pBox->h + pSurface->cyBlock - 1) / pSurface->cyBlock;

    pMap->enmMapType   = enmMapType;
    pMap->format       = pSurface->format;
    pMap->box          = *pBox;
    pMap->cbBlock      = pSurface->cbBlock;
    pMap->cxBlocks     = cxBlocks;
    pMap->cyBlocks     = cyBlocks;
    pMap->cbRow        = cxBlocks * pSurface->cbPitchBlock;
    pMap->cbRowPitch   = cbRowPitch;
    pMap->cRows        = (cyBlocks * pSurface->cbBlock) / pSurface->cbPitchBlock;
    pMap->cbDepthPitch = cbDepthPitch;
    pMap->pvData       = (uint8_t *)pvData
                       + (pBox->x / pSurface->cxBlock) * pSurface->cbPitchBlock
                       + (pBox->y / pSurface->cyBlock) * cbRowPitch
                       + pBox->z * cbDepthPitch;
}

cxBlocks is derived from the width of the surface provided by the guest in its definition.

Since the check that controls the memcpy operation mentioned above only depends on the value of cbRow and not the size of the memory region, a vm can read up to cbRow bytes from the buffer allocated with size 0:

...
            uint32_t const cbBufferSrc = mapBufferSrc.cbRow;
...
if (   pCmd->srcX < cbBufferSrc
                && pCmd->width <= cbBufferSrc- pCmd->srcX
                && pCmd->destX < cbBufferDest
                && pCmd->width <= cbBufferDest - pCmd->destX)
            {
                RT_UNTRUSTED_VALIDATED_FENCE();

                memcpy(&pu8BufferDest[pCmd->destX], &pu8BufferSrc[pCmd->srcX], pCmd->width);
            }

This memcpy call will copy the out-of-bounds read contents from the buggy_surface into the transfer_surface, an attacker can then issue a READBACK_SUBRESOURCE command to get the contents of transfer_surface back to guest memory.

Linear out-of-bounds write

For this case, a malicious guest only needs to define one surface: buggy_surface. Linear out-of-bounds write into host memory can be achieved by issuing an UPDATE_SUBRESOURCE command, which will in turn call the function vmsvgaR3TransferSurfaceLevel with attacker controlled arguments.

Similar to the linear read out of bounds case, the device will first map the surface dimensions of the data to transfer, this time it does so with the function vmsvga3dGetBoxDimensions which does almost exactly the same as vmsvga3dSurfaceMapInit.

In contrast with linear read, for this case, the attacker has an opportunity to define a “box” of the surface to transfer, the device will make sure that said box is within the bounds of the image size:

    [...]

    SVGA3dBox clipBox;
    if (pBox)
    {
        clipBox = *pBox;
        vmsvgaR3ClipBox(&pMipLevel->mipmapSize, &clipBox);
        ASSERT_GUEST_RETURN(clipBox.w && clipBox.h && clipBox.d, VERR_INVALID_PARAMETER);
    }
    
    [...]

[source]

Both vmsvga3dGetBoxDimensions and vmsvga3dSurfaceMapInit have the same bug: They calculate the size of cbRow using only the dimensions specified by the guest and fail to take into account the size of the buffer that backs up the surface they work with [1,2]

[...]
    pMap->cbRow        = cxBlocks * pSurface->cbPitchBlock;
[...]

This value is then used to transfer an almost arbitrary number of bytes from guest memory and into the buffer of size 0:

 if (enmTransfer == SVGA3D_READ_HOST_VRAM)
    rc = vmsvgaR3GboWrite(pSvgaR3State, &pMob->Gbo, offMob, pu8Map, dims.cbRow);
 else
    rc = vmsvgaR3GboRead(pSvgaR3State, &pMob->Gbo, offMob, pu8Map, dims.cbRow);

[source]

Timeline

Date reported: 04/01/2025
Date fixed: 04/15/2025
Date disclosed: 05/15/2025

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0