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.
High -
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:
buggy_surface (surface allocated with size 0)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)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.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.
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.
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.
VBoxDD.so with the value from 1VBoxDD.so to find a function pointer that will lead to the base of VBoxRT.so`memprotect to make the shellcode location executablepfnCommandClear with the first ROP chain gadget pivoting the stackAssuming the guest allocates two surfaces:
Buggy_surface that has an allocation of 0 backing it up and that will hold the role of srcTransfer_surface with a valid size and allocation and that will hold the role of destThe 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.
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]
Date reported: 04/01/2025
Date fixed: 04/15/2025
Date disclosed: 05/15/2025