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:
- Trigger the allocation of a
buggy_surface
(surface allocated with size 0) - Allocate a
GBO
object with a value incbTotal
that could be used to finger print our object (a.k.a an “egg”). After trial an error the value0x1421337
proved to be reliable enough (~100% success rate) - 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 first0x5a
bytes), assume that our target GBO object is allocated right after the surface. - 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
- Leak the value of the function pointer pfnCommandClear
- Deduce the base of
VBoxDD.so
with the value from 1 - Read the GOT table of
VBoxDD.so
to find a function pointer that will lead to the base of VBoxRT.so` - Using the arbitrary heap allocation primitive to plant a shellcode
- Construct a ROP chain with gadgets found on both files
- 5a. Pivoting the stack to a controlled section of the heap
-
5b. Call
memprotect
to make the shellcode location executable - 5c. Jump into the shellcode
- Corrupt the value of
pfnCommandClear
with the first ROP chain gadget pivoting the stack - Issue a vmsvga3dCommandClear command
- Initiate RCE
Further Analysis
Linear out-of-bounds read
Assuming the guest allocates two surfaces:
Buggy_surface
that has an allocation of 0 backing it up and that will hold the role ofsrc
Transfer_surface
with a valid size and allocation and that will hold the role ofdest
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?






