Zombie COTables: Resurrecting Freed Memory to Escape VirtualBox
Zombie COTables: Resurrecting Freed M 2026-6-15 15:44:54 Author: blog.exodusintel.com(查看原文) 阅读量:4 收藏


Zombie COTables: Resurrecting Freed Memory to Escape VirtualBox

By Luca Ginex

Overview

This blog post discusses a use-after-free vulnerability that we found in VirtualBox in 2025. This vulnerability was patched on Oracle Critical Patch Update – January 2026. The vulnerability was also presented, along with others, at OffensiveCon 2026. This post describes the exploitation process for the vulnerability on a Linux system.

First, a general overview of the SVGA device is given. Next, an analysis of the vulnerability is provided. Finally, the exploitation strategy is presented.

Preliminaries

In this section we give a general overview of the SVGA device and describe the relevant structures involved in SVGA operations.

Super Video Graphics Array (SVGA)

The Super Video Graphics Array (SVGA) is a display standard that extends the capabilities of the original Video Graphics Array (VGA) standard introduced by IBM in 1987. SVGA provides support for a wide range of screen resolutions and color depths, surpassing the limits of VGA.

Common SVGA resolutions include: 800×600 and 1024×768, with modern implementations supporting much higher resolutions dependent on the graphics card and monitor capabilities.

VMware SVGA-II

VMware SVGA-II is an SVGA-compatible virtual graphics adapter developed by VMware for use in its virtual machines (VMs). It is a software-defined device that provides guest operating systems with enhanced graphics capabilities, including 3D acceleration. This virtual graphics device is a standard component in VMware products like Workstation, Fusion, and ESXi, and is also supported by other hypervisors, including Oracle VirtualBox.

The VMware SVGA-II virtual device is exposed to the guest VM through the PCI bus, presenting both I/O ports and a Memory-Mapped I/O (MMIO) region. The base address for the I/O ports (SVGA_PORT_BASE) is dynamically assigned during PCI bus enumeration.

The following table describes the primary I/O port offsets exposed by the virtual device:

Name              Index        Description
---------------   ---------    ----------------------------------------------------------------------
SVGA_INDEX        0x00         Index of the SVGA register to read from or write to.
SVGA_VALUE        0x01         Value read or to write to the register specified in the SVGA_INDEX offset.
SVGA_BIOS         0x02         Unknown.
SVGA_IRQSTATUS    0x03         Unknown.

The guest VM can use these I/O ports to read from and write to the internal registers of the SVGA-II device. To access a register, the guest driver writes the register’s index to the SVGA_INDEX port (SVGA_PORT_BASE + SVGA_INDEX). It can then read the register’s value from the SVGA_VALUE port (SVGA_PORT_BASE + SVGA_VALUE) or write a new value to it through the same port.

The following table shows the defined registers by the SVGA-II specification:

Register Name                         Index   Description
----------------------------          -----   ----------------------------------------------------------
SVGA_REG_ID                           0x00    SVGA device ID. Guest writes expected ID; host responds if
                                              supported.
SVGA_REG_ENABLE                       0x01    Enables or disables the SVGA device.
SVGA_REG_WIDTH                        0x02    Framebuffer width (in pixels).
SVGA_REG_HEIGHT                       0x03    Framebuffer height (in pixels).
SVGA_REG_MAX_WIDTH                    0x04    Maximum width supported by the device.
SVGA_REG_MAX_HEIGHT                   0x05    Maximum height supported by the device.
SVGA_REG_DEPTH                        0x06    Bits per pixel (usually 24 or 32).
SVGA_REG_BITS_PER_PIXEL               0x07    Guest-visible bits per pixel (may differ from DEPTH).
SVGA_REG_PSEUDOCOLOR                  0x08    Boolean; set if using pseudocolor mode.
SVGA_REG_RED_MASK                     0x09    Red mask (bitfield).
SVGA_REG_GREEN_MASK                   0x0A    Green mask.
SVGA_REG_BLUE_MASK                    0x0B    Blue mask.
SVGA_REG_BYTES_PER_LINE               0x0C    Pitch: bytes per scanline.
SVGA_REG_FB_START                     0x0D    Framebuffer physical address (read-only).
SVGA_REG_FB_OFFSET                    0x0E    Offset to start of visible framebuffer.
SVGA_REG_VRAM_SIZE                    0x0F    Size of video RAM.
SVGA_REG_FB_SIZE                      0x10    Size of framebuffer.
SVGA_REG_CAPABILITIES                 0x11    Bitmask of supported features.
SVGA_REG_MEM_START                    0x12    MMIO (memory-mapped I/O) region base address.
SVGA_REG_MEM_SIZE                     0x13    Size of MMIO region.
SVGA_REG_CONFIG_DONE                  0x14    Write 1 when configuration is done.
SVGA_REG_SYNC                         0x15    Write 1 to sync guest/host framebuffer.
SVGA_REG_BUSY                         0x16    Read 1 if device is busy processing commands.
SVGA_REG_GUEST_ID                     0x17    Guest OS identifier.
SVGA_REG_CURSOR_ID                    0x18    Cursor identifier.
SVGA_REG_CURSOR_X                     0x19    Cursor X position.
SVGA_REG_CURSOR_Y                     0x1A    Cursor Y position.
SVGA_REG_CURSOR_ON                    0x1B    Show/hide cursor.
SVGA_REG_HOST_BITS_PER_PIXEL          0x1C    Bits per pixel on host display (read-only).
SVGA_REG_SCRATCH_SIZE                 0x1D    Number of scratch registers available.
SVGA_REG_MEM_REGS                     0x1E    Number of FIFO registers.
SVGA_REG_NUM_DISPLAYS                 0x1F    Number of virtual displays supported.
SVGA_REG_PITCHLOCK                    0x20    Lock bytes-per-line (pitch).
SVGA_REG_IRQMASK                      0x21    Interrupt bitmask.
SVGA_REG_NUM_GUEST_DISPLAYS           0x22    Guest-specified number of displays.
SVGA_REG_DISPLAY_ID                   0x23    ID of current display being modified.
SVGA_REG_DISPLAY_IS_PRIMARY           0x24    Boolean; is this display the primary one?
SVGA_REG_DISPLAY_POSITION_X           0x25    Display position X (for multi-monitor layout).
SVGA_REG_DISPLAY_POSITION_Y           0x26    Display position Y.
SVGA_REG_DISPLAY_WIDTH                0x27    Width of a specific display.
SVGA_REG_DISPLAY_HEIGHT               0x28    Height of a specific display.
SVGA_REG_GMR_ID                       0x29    ID of the Guest Memory Region
SVGA_REG_GMR_DESCRIPTOR               0x2A    Unknown
SVGA_REG_GMR_MAX_IDS                  0x2B    Unknown
SVGA_REG_GMR_MAX_DESCRIPTOR_LENGTH    0x2C    Unknown
SVGA_REG_TRACES                       0x2D    Unknown
SVGA_REG_GMRS_MAX_PAGES               0x2E    Unknown
SVGA_REG_MEMORY_SIZE                  0x2F    Total dedicated device memory size excluding FIFO
SVGA_REG_COMMAND_LOW                  0x30    Lower 32 bits of the command buffer physical address
SVGA_REG_COMMAND_HIGH                 0x31    Upper 32 bits of command buffer physical address

The SVGA-II driver uses a FIFO (First In, First Out) queue, which is a region of memory shared between the guest and the host, to process commands. The guest driver writes command sequences into this FIFO buffer and updates registers, such as SVGA_FIFO_NEXT_CMD, to notify the host that new commands are available for execution. Another way to submit commands is by registering a command buffer: the guest sets the SVGA_REG_COMMAND_HIGH and the SVGA_REG_COMMAND_LOW registers with the physical address of a buffer that contains an SVGACBHeader structure. Its definition is shown in the following code listing.

typedef
#include "vmware_pack_begin.h"
struct {
   volatile SVGACBStatus status; /* Modified by device. */
   volatile uint32 errorOffset;  /* Modified by device. */
   uint64 id;
   SVGACBFlags flags;
   uint32 length;
   union {
      PA pa;
      struct {
         SVGAMobId mobid;
         uint32 mobOffset;
      } mob;
   } ptr;
   uint32 offset; /* Valid if CMD_BUFFERS_2 cap set, must be zero otherwise,
                   * modified by device.
                   */
   uint32 dxContext; /* Valid if DX_CONTEXT flag set, must be zero otherwise */
   uint32 mustBeZero[6];
}
#include "vmware_pack_end.h"
SVGACBHeader;

The ptr.pa field contains the physical address of the command buffer. The length field contains the size of the command buffer. The maximum size is 512KB.

SVGA Memory Objects

The VirtualBox SVGA implementation uses the generic VMSVGAMOB structure to keep track of several types of memory objects. The following listing shows the definition of the VMSVGAMOBstructure.

/* MOB is also a GBO.
 */
typedef struct VMSVGAMOB
{
    AVLU32NODECORE          Core; /* Key is the mobid. */
    RTLISTNODE              nodeLRU;

[1]

    VMSVGAGBO               Gbo;
} VMSVGAMOB, *PVMSVGAMOB;

VirtualBox uses this structure on the host while object data can be stored on guest memory. In this scenario the object is a Guest-Backed Object (GBO). In order for the host to access object data it must keep track of where the data is stored on the guest side. In order to do that the VMSVGAGBO structure is used. Note that the VMSVGAMOB structure contains a VMSVGAGBOstructure, at [1].

The following listing shows the VMSVGAGBO structure definition.

/* GBO.
 */
typedef struct VMSVGAGBO
{
    uint32_t                fGboFlags;
    uint32_t                cTotalPages;
    uint32_t                cbTotal;
#ifndef VMSVGA_WITH_PGM_LOCKING
    uint32_t                cDescriptors;
    PVMSVGAGBODESCRIPTOR    paDescriptors;
#else
    uint32_t                cSegsUsed;        /**< Number of segments used in VMSVGAGBO::paSegs. */
    void                   *pvDescriptors;    /**< Pointer to the memory for holding all the parallel arrays. */
    RTGCPHYS               *paGCPhysPages;    /**< Pointer to the array of guest physical address for the pages. */
    PPGMPAGEMAPLOCK         paPageLocks;      /**< Pointer to the array of PGM page map locks. */
    void                  **papvPages;        /**< Pointer to the host adresses of mapped pages. */
    PRTSGSEG                paSegs;           /**< Pointer to an array of segments. */
#endif

[2]

    void                   *pvHost; /* Pointer to cbTotal bytes on the host if VMSVGAGBO_F_HOST_BACKED is set. */
} VMSVGAGBO, *PVMSVGAGBO;
typedef VMSVGAGBO const *PCVMSVGAGBO;

The pvHost field, at [2], contains a pointer to a memory region where the host keeps a copy of guest object data.

The DX Context

The VirtualBox SVGA implementation uses the VMSVGA3DDXCONTEXT structure to keep track of different DirectX contexts. Each context contains all the rendering parameters and resources used by the GPU (shaders, textures). The following listing shows the VMSVGA3DDXCONTEXTstructure definition.

/**
 * VMSVGA3D DX context (VGPU10+). DX contexts ids are a separate namespace from legacy context ids.
 */
typedef struct VMSVGA3DDXCONTEXT
{

[3]

    /** The DX context id. */
    uint32_t                  cid;
    /** . */
    uint32_t                  u32Reserved;
    /** . */
    uint32_t                  cRenderTargets;
    /** Backend specific data. */
    PVMSVGA3DBACKENDDXCONTEXT pBackendDXContext;
    /** Copy of the guest memory for this context. The guest will be updated on unbind. */
    SVGADXContextMobFormat    svgaDXContext;

[4]

    /* Context-Object Tables bound to this context. */
    PVMSVGAMOB aCOTMobs[VBSVGA_NUM_COTABLES];
    struct
    {
        SVGACOTableDXRTViewEntry          *paRTView;
        SVGACOTableDXDSViewEntry          *paDSView;
        SVGACOTableDXSRViewEntry          *paSRView;
        SVGACOTableDXElementLayoutEntry   *paElementLayout;
        SVGACOTableDXBlendStateEntry      *paBlendState;
        SVGACOTableDXDepthStencilEntry    *paDepthStencil;
        SVGACOTableDXRasterizerStateEntry *paRasterizerState;
        SVGACOTableDXSamplerEntry         *paSampler;
        SVGACOTableDXStreamOutputEntry    *paStreamOutput;
        SVGACOTableDXQueryEntry           *paQuery;
        SVGACOTableDXShaderEntry          *paShader;
        SVGACOTableDXUAViewEntry          *paUAView;
        uint32_t                           cRTView;
        uint32_t                           cDSView;
        uint32_t                           cSRView;
        uint32_t                           cElementLayout;
        uint32_t                           cBlendState;
        uint32_t                           cDepthStencil;
        uint32_t                           cRasterizerState;
        uint32_t                           cSampler;
        uint32_t                           cStreamOutput;
        uint32_t                           cQuery;
        uint32_t                           cShader;
        uint32_t                           cUAView;

        VBSVGACOTableDXVideoProcessorEntry *paVideoProcessor;
        VBSVGACOTableDXVideoDecoderOutputViewEntry  *paVideoDecoderOutputView;
        VBSVGACOTableDXVideoDecoderEntry  *paVideoDecoder;
        VBSVGACOTableDXVideoProcessorInputViewEntry  *paVideoProcessorInputView;
        VBSVGACOTableDXVideoProcessorOutputViewEntry  *paVideoProcessorOutputView;
        uint32_t                           cVideoProcessor;
        uint32_t                           cVideoDecoderOutputView;
        uint32_t                           cVideoDecoder;
        uint32_t                           cVideoProcessorInputView;
        uint32_t                           cVideoProcessorOutputView;
    } cot;
} VMSVGA3DDXCONTEXT;

Each structure has a unique context ID saved in the cid field, at [3]. Each context also keeps track of all the memory objects (MOBs) used in that specific context. MOBs are stored into the Context-Object Table (COTable) [4]. The table contains one entry for each type of MOB supported. At any given time only one MOB of a specific type can be stored into the COTable.

Vulnerability

The vulnerability is caused by how guest-backed memory objects (MOBs) are handled. By issuing an SVGA_3D_CMD_DX_SET_COTABLE SVGA command it is possible to bind a MOB to a context-object table (COTable). The COTable holds a pointer to the bound MOB. Later it is possible to issue an SVGA_3D_CMD_DESTROY_GB_MOB SVGA command to destroy the same MOB, while the COTable holds a dangling pointer to the freed MOB. The destroy handler does not verify if the requested MOB is bound to a COTable before freeing it. By manipulating the heap memory it is possible to reuse the freed MOB memory to corrupt memory and eventually achieve guest-to-host escape and execute code in the context of the hypervisor process.

COTable Initialization

When an SVGA_3D_CMD_DX_SET_COTABLE SVGA command is sent from a guest VM to the host the vmsvga3dDXSetCOTable() function is called to handle the command. The following code listing shows the vmsvga3dDXSetCOTable() function.

// File: src/VBox/Devices/Graphics/DevVGA-SVGA3d-dx.cpp

[Truncated]

int vmsvga3dDXSetCOTable(PVGASTATECC pThisCC, SVGA3dCmdDXSetCOTable const *pCmd, PVMSVGAMOB pMob)
{
    int rc;
    PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
    AssertReturn(pSvgaR3State->pFuncsDX && pSvgaR3State->pFuncsDX->pfnDXSetCOTable, VERR_INVALID_STATE);
    PVMSVGA3DSTATE p3dState = pThisCC->svga.p3dState;
    AssertReturn(p3dState, VERR_INVALID_STATE);

    PVMSVGA3DDXCONTEXT pDXContext;
    rc = vmsvga3dDXContextFromCid(p3dState, pCmd->cid, &pDXContext);
    AssertRCReturn(rc, rc);
    RT_UNTRUSTED_VALIDATED_FENCE();

[1]

    return dxSetOrGrowCOTable(pThisCC, pDXContext, pMob, pCmd->type, pCmd->validSizeInBytes, false);
}

[Truncated]

static int dxSetOrGrowCOTable(PVGASTATECC pThisCC, PVMSVGA3DDXCONTEXT pDXContext, PVMSVGAMOB pMob,
                              SVGACOTableType enmType, uint32_t validSizeInBytes, bool fGrow)
{
    PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
    int rc = VINF_SUCCESS;

    uint32_t idxCOTable;
    if (enmType < SVGA_COTABLE_MAX)

[2]

        idxCOTable = enmType;
    else if (enmType >= VBSVGA_COTABLE_MIN && enmType < VBSVGA_COTABLE_MAX)
        idxCOTable = SVGA_COTABLE_MAX + (enmType - VBSVGA_COTABLE_MIN);
    else
        ASSERT_GUEST_FAILED_RETURN(VERR_INVALID_PARAMETER);
    RT_UNTRUSTED_VALIDATED_FENCE();

    uint32_t cbCOT;
    if (pMob)
    {

[3]

        /* Bind a new mob to the COTable. */
        cbCOT = vmsvgaR3MobSize(pMob);

[4]

        ASSERT_GUEST_RETURN(validSizeInBytes <= cbCOT, VERR_INVALID_PARAMETER);
        RT_UNTRUSTED_VALIDATED_FENCE();

        /* When growing a COTable, the valid size can't be greater than the old COTable size. */
        if (fGrow)
            validSizeInBytes = RT_MIN(validSizeInBytes, vmsvgaR3MobSize(pDXContext->aCOTMobs[idxCOTable]));

[5]

        /* Create a memory pointer, which is accessible by host. */
        rc = vmsvgaR3MobBackingStoreCreate(pSvgaR3State, pMob, fGrow ? 0 : validSizeInBytes);
    }
    else
    {
        /* Unbind. */
        validSizeInBytes = 0;
        cbCOT = 0;
        vmsvgaR3MobBackingStoreDelete(pSvgaR3State, pDXContext->aCOTMobs[idxCOTable]);
    }

[Truncated]

    if (RT_SUCCESS(rc))
    {

[6]

        pDXContext->aCOTMobs[idxCOTable] = pMob;

[Truncated]

    return rc;
}

The pCmd parameter points to a structure that wraps all the guest-submitted SVGA command parameters. The command requires the following parameters:

  • cid: DX context ID.
  • mobid: ID of the MOB to bind.
  • type: Type of the MOB to bind.
  • validSizeInBytes: Size of the MOB to bind.

The pMob parameter holds a pointer to the target MOB structure that the guest VM requested to bind.

The vmsvga3dDXSetCOTable() function, at [1], calls the dxSetOrGrowCOTable() function. The dxSetOrGrowCOTable() function, at [2], validates the guest-supplied type for the MOB. If it is a valid type the value is stored into the idxCOTable variable. The code at [3] calls the vmsvgaR3MobSize() function on the submitted MOB to get the size of the MOB. At [4] the code validates that the validSizeInBytes value is not bigger than the size of the submitted MOB.

The code at [5] calls the vmsvgaR3MobBackingStoreCreate() function to allocate a memory region to store the content of the guest-backed object into the host memory. This function populates the Gbo.pvHost field of the submitted MOB with a pointer to the allocated memory region. Note that this function performs the copy of the content of guest memory into the allocated memory region. In order to complete the binding process, at [6], the function sets the aCOTMobs[idxCOTable] array entry to a pointer to the requested MOB. This completes the binding operation of a MOB to the COTable.

MOB Destruction

Later a guest VM can issue an SVGA_3D_CMD_DESTROY_GB_MOB SVGA command. The handler for this command is the vmsvgaR3MobDestroy() function. The guest must supply a valid MOB ID to the function. The following code listing shows the vmsvgaR3MobDestroy() function.

// File: src/VBox/Devices/Graphics/DevVGA-SVGA-cmd.cpp

[Truncated]

static void vmsvgaR3MobFree(PVMSVGAR3STATE pSvgaR3State, PVMSVGAMOB pMob)
{
    vmsvgaR3GboDestroy(pSvgaR3State, &pMob->Gbo);

[7]

    RTMemFree(pMob);
}

static int vmsvgaR3MobDestroy(PVMSVGAR3STATE pSvgaR3State, SVGAMobId mobid)
{
    /* Update the entry in the pSvgaR3State->pGboOTableMob. */
    SVGAOTableMobEntry entry;
    RT_ZERO(entry);
    vmsvgaR3OTableWrite(pSvgaR3State, &pSvgaR3State->aGboOTables[SVGA_OTABLE_MOB],
                        mobid, SVGA3D_OTABLE_MOB_ENTRY_SIZE, &entry, sizeof(entry));

[8]

    PVMSVGAMOB pMob = (PVMSVGAMOB)RTAvlU32Remove(&pSvgaR3State->MOBTree, mobid);
    if (pMob)
    {
        RTListNodeRemove(&pMob->nodeLRU);

[9]

        vmsvgaR3MobFree(pSvgaR3State, pMob);
        return VINF_SUCCESS;
    }

    return VERR_INVALID_PARAMETER;
}

The function at [8] removes the MOB entry corresponding to the MOB ID provided by the guest (if it exists) from a tree data structure. At [9], the vmsvgaR3MobFree() function is called. At [7] the RTMemFree() function is called on the pointer to the MOB structure. This terminates the deleting process for a MOB. The code however does not check if the target MOB is bound to a COTable before freeing it. By exploiting this vulnerability it is possible to have a COTable entry that still points to a freed MOB. By manipulating the heap memory it is possible to reuse the freed MOB memory to corrupt memory and eventually achieve guest-to-host escape and execute code in the context of the hypervisor process.

Exploitation

Exploitation Steps

Exploiting this vulnerability involves the following steps:

  1. Defining a MOB. This causes the allocation of a MOB structure with size 104.
  2. Binding a MOB to a COTable. The included exploit uses the context ID 2 in this step. Note that any type in the COTable can be used. The included exploit uses the type 7. The validSizeInBytes parameter must be set to the size of the VMSVGA3DDXCONTEXT structure (0x2170) that the exploit will later use. This causes an allocation of a buffer with size 0x2170. In the following discussion this MOB will be referred as MOB1.
  3. Destroying MOB1. In the process, both the MOB structure (size 104) and the buffer to hold guest data (size 0x2170) are freed. This causes COTable[7] to still hold a pointer to MOB1.
  4. Reclaiming MOB1 freed memory by binding another MOB to the COTable. The validSizeInBytes parameter must be set to 104. This will likely cause the memory associated with MOB1 to be used as the buffer to hold guest data of MOB2.
  5. Trigger allocation of a new VMSVGA3DDXCONTEXT structure. This will likely cause the structure to be placed in the same memory used for MOB1 guest buffer.

The following sections further explore the above steps and explain how to achieve guest-to-host escape.

Binding Process

When a MOB structure is allocated it does not have an associated buffer to store guest-backed data. The buffer is allocated when the MOB is bound to a COTable. The following code listing shows the vmsvgaR3GboBackingStoreCreate() function.

// File: src/VBox/Devices/Graphics/DevVGA-SVGA-cmd.cpp

static int vmsvgaR3GboBackingStoreCreate(PVMSVGAR3STATE pSvgaR3State, PVMSVGAGBO pGbo, uint32_t cbValid)
{
    int rc;

    /* Just reread the data if pvHost has been allocated already. */
    if (!(pGbo->fGboFlags & VMSVGAGBO_F_HOST_BACKED))

[1]

        pGbo->pvHost = RTMemAllocZ(pGbo->cbTotal);

    if (pGbo->pvHost)
    {
        cbValid = RT_MIN(cbValid, pGbo->cbTotal);

[2]

        rc = vmsvgaR3GboRead(pSvgaR3State, pGbo, 0, pGbo->pvHost, cbValid);
    }
    else
        rc = VERR_NO_MEMORY;

    if (RT_SUCCESS(rc))
        pGbo->fGboFlags |= VMSVGAGBO_F_HOST_BACKED;
    else
    {
        RTMemFree(pGbo->pvHost);
        pGbo->pvHost = NULL;
    }
    return rc;
}

In case of a just-allocated MOB, code at [1] is executed. This causes the allocation of a buffer with an arbitrary size that the guest controls. Note how the allocated memory is also zeroed out by the usage of the RTMemAllocZ() function. The Gbo.pvHost field contains the pointer to the memory region. Note at [2] that the function also copies content from the guest-controlled memory into the newly-allocated buffer.

It is important to note that a pointer to the memory region is also stored in the cot structure of the VMSVGA3DDXCONTEXT structure during the binding process. Specifically the pointer associated with the type specified during the binding process is populated. For example binding a MOB with type 7, which corresponds to the SVGA_COTABLE_SAMPLER type, causes the paSampler pointer to be populated with the pvHost value of the MOB.

After the binding process, the COTable state is the following.

Destruction Process And Reallocation

By issuing an SVGA_3D_CMD_DESTROY_GB_MOB SVGA command, MOB1 memory and MOB1’s pvHost buffer are freed. The freed memory is also zeroed out.

After this step, the exploit reclaims the two freed memory chunks. MOB1 memory can be reclaimed by binding another MOB to the COTable with a guest buffer size of 104. This causes MOB2’s pvHost pointer to point to MOB1. Since the goal is to gain control over a VMSVGA3DDXCONTEXT structure, MOB1’s pvHost buffer must have the size of that structure, which is 0x2170. In order to trigger allocation of a DX context, DX contexts management must be analyzed.

The following code listing shows the vmsvga3dDXDefineContext() function.

// File: src/VBox/Devices/Graphics/DevVGA-SVGA3d-dx.cpp

/**
 * Create a new 3D DX context.
 *
 * @returns VBox status code.
 * @param   pThisCC         The VGA/VMSVGA state for ring-3.
 * @param   cid             Context id to be created.
 */
int vmsvga3dDXDefineContext(PVGASTATECC pThisCC, uint32_t cid)
{
    int rc;
    PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
    AssertReturn(pSvgaR3State->pFuncsDX && pSvgaR3State->pFuncsDX->pfnDXDefineContext, VERR_INVALID_STATE);
    PVMSVGA3DSTATE p3dState = pThisCC->svga.p3dState;
    AssertReturn(p3dState, VERR_INVALID_STATE);

    PVMSVGA3DDXCONTEXT pDXContext;

    LogFunc(("cid %d\n", cid));

    AssertReturn(cid < SVGA3D_MAX_CONTEXT_IDS, VERR_INVALID_PARAMETER);

[3]

    if (cid >= p3dState->cDXContexts)
    {
        /* Grow the array. */

[4]

        uint32_t cNew = RT_ALIGN(cid + 15, 16);
        void *pvNew = RTMemRealloc(p3dState->papDXContexts, sizeof(p3dState->papDXContexts[0]) * cNew);
        AssertReturn(pvNew, VERR_NO_MEMORY);
        p3dState->papDXContexts = (PVMSVGA3DDXCONTEXT *)pvNew;
        while (p3dState->cDXContexts < cNew)
        {

[5]

            pDXContext = (PVMSVGA3DDXCONTEXT)RTMemAllocZ(sizeof(*pDXContext));
            AssertReturn(pDXContext, VERR_NO_MEMORY);
            pDXContext->cid = SVGA3D_INVALID_ID;
            p3dState->papDXContexts[p3dState->cDXContexts++] = pDXContext;
        }
    }
    /* If one already exists with this id, then destroy it now. */
    if (p3dState->papDXContexts[cid]->cid != SVGA3D_INVALID_ID)
        vmsvga3dDXDestroyContext(pThisCC, cid);

    pDXContext = p3dState->papDXContexts[cid];
    memset(pDXContext, 0, sizeof(*pDXContext));

[Truncated]

    return rc;
}

When the exploit defines the first context it uses (context ID 2), at [3], the function first checks if the requested context ID is greater than the max ID currently allocated. If that’s the case, the function, at [4], rounds the number of context IDs to allocate by adding 15 to the requested context ID and aligning that to 16. By submitting a context ID value of 2, context IDs 0-31 are allocated. So if the exploit uses a new context ID inside the range 0-31, no actual allocation is performed.

In order to force a call to the RTMemAllocZ() function, at [5], the exploit must define a context with a context ID greater than 31. This causes the context ID 32 (the first one to be allocated) to use MOB1’s pvHost freed buffer for the VMSVGA3DDXCONTEXT structure.

If the exploit is successful, the COTable status is the following.

With this setup, the exploit has control over a VMSVGA3DDXCONTEXT structure through the pDXContext->cot field of context ID 2. An interesting feature of the cot structure is that it is possible to arbitrarily set the content of the buffers that the pointers in the cot structure point to. For each COT type a function setter is defined that can be used to populate the corresponding buffer. In the exploit case the cot.paSampler contains the address of the VMSVGA3DDXCONTEXT structure. By using the SVGA_3D_CMD_DX_DEFINE_SAMPLER_STATE SVGA command it is possible to insert arbitrary data into the cot.paSampler buffer. The following code listing shows the vmsvga3dDXDefineSamplerState() function, which is the handler for the aforementioned SVGA command.

// File: src/VBox/Devices/Graphics/DevVGA-SVGA3d-dx.cpp

int vmsvga3dDXDefineSamplerState(PVGASTATECC pThisCC, uint32_t idDXContext, SVGA3dCmdDXDefineSamplerState const *pCmd)
{
    int rc;
    PVMSVGAR3STATE const pSvgaR3State = pThisCC->svga.pSvgaR3State;
    AssertReturn(pSvgaR3State->pFuncsDX && pSvgaR3State->pFuncsDX->pfnDXDefineSamplerState, VERR_INVALID_STATE);
    PVMSVGA3DSTATE p3dState = pThisCC->svga.p3dState;
    AssertReturn(p3dState, VERR_INVALID_STATE);

    PVMSVGA3DDXCONTEXT pDXContext;
    rc = vmsvga3dDXContextFromCid(p3dState, idDXContext, &pDXContext);
    AssertRCReturn(rc, rc);

[6]

    SVGA3dSamplerId const samplerId = pCmd->samplerId;

[Truncated]

[7]

    SVGACOTableDXSamplerEntry *pEntry = &pDXContext->cot.paSampler[samplerId];
    pEntry->filter         = pCmd->filter;
    pEntry->addressU       = pCmd->addressU;
    pEntry->addressV       = pCmd->addressV;
    pEntry->addressW       = pCmd->addressW;
    pEntry->mipLODBias     = pCmd->mipLODBias;
    pEntry->maxAnisotropy  = pCmd->maxAnisotropy;
    pEntry->comparisonFunc = pCmd->comparisonFunc;
    pEntry->borderColor    = pCmd->borderColor;
    pEntry->minLOD         = pCmd->minLOD;
    pEntry->maxLOD         = pCmd->maxLOD;

[Truncated]

    return rc;
}

At [6], the function extracts the samplerId from the guest-provided command parameters. This ID is used as an offset into the cot.paSampler buffer. The chosen entry is populated with guest-provided data [7]. This allows the exploit to overwrite certain fields of the VMSVGA3DDXCONTEXT structure. This process can also be used to create an arbitrary writeprimitive: by modifying the paSamplerState pointer it is possible to write to any arbitrary memory location.

Information Disclosure

The exploit leverages the VMSVGA3DDXCONTEXT structure overwrite to achieve information disclosure. This allows the exploit to know where the VBoxDD.so library is loaded into the hypervisor process and to further exploit the target.

The exploit performs the following steps:

  1. Allocate 6 MOBs. This will likely result in the six MOBs to be allocated one after another in memory.
  2. Free MOBs number 2, 4, and 6.
  3. Bind the remaining 1, 3, and 5 MOBs to the COTable with context ID 32. The size of the guest buffer must be 104 (0x68).

This, with a high chance, creates the layout shown above.

The exploit then modifies the counter values of all the entries in the cot structure of the context ID 32 by leveraging the VMSVGA3DDXCONTEXT overwrite primitive shown before. This keeps the pointers in the cot structure intact. The modified 0xffffffff counter instead allows the exploit to perform an out-of-bounds write into adjacent memory of the COT pointers. By modifying the cot.cSampler value it is possible to overwrite memory past the size of the paSampler buffer. If the heap grooming process is successful, this allows the exploit to modify the pMob->Gbo.cbTotal field of an adjacent MOB.

In the shown example, the exploit can modify the MOB bound in entry #4. Note that this part of the exploit is not 100% reliable as the heap layout for the six allocated MOBs depends on memory pressure on the hypervisor side. Testing shows the exploit to be reliable with a probability of 70%.

By issuing an SVGA_3D_CMD_DX_GROW_COTABLE command it is possible to read data from the pvHost pointer with the modified Gbo.cbTotal field of entry 8, leaking the adjacent MOB structure. From this leak it is possible to recover the base address of the VBoxDD.so library in memory and the address in memory of the VMSVGAR3STATE structure. The following code listing shows its definition.

/**
 * Internal SVGA ring-3 only state.
 */
typedef struct VMSVGAR3STATE
{
    PPDMDEVINS              pDevIns; /* Stored here to use with PDMDevHlp* */
    GMR                    *paGMR; // [VMSVGAState::cGMR]
    struct
    {
        SVGAGuestPtr RT_UNTRUSTED_GUEST         ptr;
        uint32_t RT_UNTRUSTED_GUEST             bytesPerLine;
        SVGAGMRImageFormat RT_UNTRUSTED_GUEST   format;
    } GMRFB;
    struct
    {
        bool                fActive;
        uint32_t            xHotspot;
        uint32_t            yHotspot;
        uint32_t            width;
        uint32_t            height;
        uint32_t            cbData;
        void               *pData;
    } Cursor;
    SVGAColorBGRX           colorAnnotation;

# ifdef VMSVGA_USE_EMT_HALT_CODE
    /** Number of EMTs in BusyDelayedEmts (quicker than scanning the set). */
    uint32_t volatile       cBusyDelayedEmts;
    /** Set of EMTs that are   */
    VMCPUSET                BusyDelayedEmts;
# else
    /** Number of EMTs waiting on hBusyDelayedEmts. */
    uint32_t volatile       cBusyDelayedEmts;
    /** Semaphore that EMTs wait on when reading SVGA_REG_BUSY and the FIFO is
     *  busy (ugly).  */
    RTSEMEVENTMULTI         hBusyDelayedEmts;
# endif

    /** Information about screens. */
    VMSVGASCREENOBJECT      aScreens[64];

    /** Command buffer contexts. */
    PVMSVGACMDBUFCTX        apCmdBufCtxs[SVGA_CB_CONTEXT_MAX];
    /** The special Device Context for synchronous commands. */
    VMSVGACMDBUFCTX         CmdBufCtxDC;
    /** Flag which indicates that there are buffers to be processed. */
    uint32_t volatile       fCmdBuf;
    /** Critical section for accessing the command buffer data. */
    RTCRITSECT              CritSectCmdBuf;

    /** Object Tables: MOBs, etc. see SVGA_OTABLE_* */
    VMSVGAGBO               aGboOTables[SVGA_OTABLE_MAX];

    /** Tree of guest's Memory OBjects. Key is mobid. */
    AVLU32TREE              MOBTree;
    /** Least Recently Used list of MOBs.
     * To unmap older MOBs when the guest exceeds SVGA_REG_SUGGESTED_GBOBJECT_MEM_SIZE_KB (SVGA_REG_GBOBJECT_MEM_SIZE_KB) value. */
    RTLISTANCHOR            MOBLRUList;

# ifdef VBOX_WITH_VMSVGA3D
#  ifdef VMSVGA3D_DX
    /** DX context of the currently processed command buffer */
    uint32_t                 idDXContextCurrent;
    uint32_t                 u32Reserved;
#  endif

[8]

    VMSVGA3DBACKENDFUNCS3D  *pFuncs3D;
    VMSVGA3DBACKENDFUNCSVGPU9 *pFuncsVGPU9;
    VMSVGA3DBACKENDFUNCSMAP *pFuncsMap;
    VMSVGA3DBACKENDFUNCSGBO *pFuncsGBO;
    VMSVGA3DBACKENDFUNCSDX  *pFuncsDX;
    VMSVGA3DBACKENDFUNCSDXVIDEO *pFuncsDXVideo;
# endif

[Truncated]

} VMSVGAR3STATE, *PVMSVGAR3STATE;

It is important to note at [8] how this structure contains some function pointers.

Arbitrary Read

The arbitrary read primitive is achieved in the following manner:

  1. Define a DX Context.
  2. Define two MOBs.
  3. Bind one MOB to the COTable of the newly-created context.
  4. Destroy the MOB.
  5. Reclaim the MOB’s memory as shown earlier. This allows the guest to overwrite MOB’s structure with arbitrary data. Set Gbo.pvHost pointer to point to where to read.
  6. Issue an SVGA_3D_CMD_DX_GROW_COTABLE to read back the content from MOB’s Gbo.pvHostpointer.

ROP Chain

With the arbitrary read/write primitives and the information leak, it is possible to overwrite a function pointer in the VMSVGAR3STATE structure with the address of the first gadget of a ROP chain. The exploit uses the pFuncsVGPU9->pfnCommandClear function pointer. It is possible to invoke the function pointer by issuing the SVGA_3D_CMD_CLEAR SVGA command. This command is very helpful as it allows to provide a variable number of parameters that can be used as places to put ROP gadgets. The included ROP chain performs the following operations:

  • Performs stack pivoting into the buffer that contains guest-provided command parameters.
  • Calls RTMemProtect() to set one of the MOB’s buffer with RWX privileges.
  • Jumps into the buffer.

Since the content of the buffer can be controlled by the guest, the guest can place shellcode there and control execution of the hypervisor process.

Process Continuation

The exploit during the ROP chain loses any reference to the stack. It is possible however to create a fake stack that redirects execution to the vmsvgaR3FifoLoop() function, which is the main loop of the SVGA thread. By doing so, the guest VM doesn’t crash and it continues its execution.

Conclusion

In this blogpost we presented a use-after-free vulnerability in the SVGA subsystem in Oracle VirtualBox and how we exploited it to achieve code execution on the host Linux machine while keeping the guest VM alive. In the tested setup the exploit reliability is around 70%.

Demo

About Exodus Intelligence

Our world class team of vulnerability researchers discover hundreds of exclusive Zero-Day vulnerabilities, providing our clients with proprietary knowledge before the adversaries find them. We also conduct N-Day research, where we select critical N-Day vulnerabilities and complete research to prove whether these vulnerabilities are truly exploitable in the wild. Our researchers create and use in-house agentic AI tooling to supplement parts of their vulnerability research and exploit development workflow. In addition to efficiency gains, we’re able to ensure AI-enabled research output maintains the same standards of quality as traditional research.

For more information on our products and how we can help your vulnerability efforts, visit www.exodusintel.com or contact [email protected] for further discussion.


文章来源: https://blog.exodusintel.com/2026/06/15/zombie-cotables-resurrecting-freed-memory-to-escape-virtualbox/
如有侵权请联系:admin#unsafe.sh