Better slow than sorry – VirtualBox 3D acceleration considered harmful
UPDATE: Exploit code and HGCM/Chromium interface library now on Github.
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:
- Issue
glBegin
in client A. - Send a forged
CR_MESSAGE_REDIR_PTR
in client B. - Issue
glEnd
in client A. - 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:
- Issue
glBegin
in client A. - Send a large, forged
CR_MESSAGE_REDIR_PTR
in client B which will trigger path[[ 3 ]]
(OS-provided malloc). - Spray some buffers via HGCM calls that have the same size and content, hoping they will occupy the freed message buffer.
- 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
- Core Security advisory for CVE-2014-{0981,0982,0983}
- REcon 2014: Breaking Out of VirtualBox through 3D Acceleration by Francisco Falcon
- VirtualBox: unprivileged host user -> host kernel privesc via environment and ioctl (CVE-2017-3561) by Jann Horn
- Bypassing VirtualBox Process Hardening on Windows by James Forshaw
- Insomni’hack 2018: Unboxing your VirtualBox
- Zero Day Initiative: Advisories 2018
- Oracle Critical Patch Update Advisory - July 2018