结合CVE-2018-3055与CVE-2018-3085攻击VirtualBox 3D加速

通过精妙构造,VirtualBox虚拟机的主机进程可以访问VBoxDrv内核驱动程序。即便它以VM用户权限启动运行,同样也可以用于本地提权。

在这篇博文中,我将结合 CVE-2018-3055 和 CVE-2018-3085 来破坏VirtualBox中启用了3D加速的虚拟机。这两个漏洞都已在VirtualBox最新的5.2.16版本中被修复。

 

概述

3D加速功能作为共享OpenGL存在于代码库中,基于用来作分布式OpenGL渲染的Chromium库,不要与和它同名(但7年后才诞生)的Web浏览器相混淆
Chromium定义了一个描述OpenGL操作的网络协议,它可以传递给一个已存在的OpenGL实例

VirtualBox正在维护Chromium的一个分支,并通过HGCM(主机-客户端 通信管理器)隧道协议传输消息。HGCM本质上是一个非常简单的 客户端-主机 RPC协议,一旦连接到HGCM服务,客户端虚拟机就可以使用整数和缓冲区参数进行简单的远程调用,并在主机端处理它们。之后会返回一个状态代码,并且被调用者可能会更改参数,以便将数据传递回客户端虚拟机

值得注意的是,HGCM接口由 客户端虚拟机添加程序 暴露给非特权进程。如果没有安装 客户端虚拟机添加程序,则需要root权限才能安装 客户端虚拟机驱动程序 并且需要公开设备,以便攻击共享的OpenGL。

 

Chromium消息基础

存在不同类型的Chromium消息,它们表示为 CRMessage联合类型

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;

该类型存储在header.type字段中,我们重点关注 CR_MESSAGE_OPCODESCR_MESSAGE_REDIR_PTR 消息CR_MESSAGE_OPCODES消息 包含作为前缀的操作码数,紧接着的是描述实际Chromium操作码的字节数组,这些操作码以特殊方式编码。 例如,一条简单的消息可能如下所示:

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,
};

每个操作码都有一个关联的 解包器 和 调度程序,分别以 crUnpackcrServerDispatch 为前缀。 此特定操作码的解包器如下所示:

/* 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 );
}

这告诉Chromium将偏移量为8处的Payload写回到响应缓冲区,上面例子中的字符串是“ccccdddd”。我不确定这对此功能的正常使用有什么必要性,但它为我们提供了一个“echo”语句来写回我们控制的数据,这对于利用漏洞肯定是有很大帮助的。

 

CVE-2018-3055

Chromium消息解析器中有几个位置,其中 SET_RETURN_PTRSET_WRITEBACK_PTR 使用用户控制的偏移调用。比如 src/VBox/HostServices/SharedOpenGL/unpacker/unpack_program.c 里的 crUnpackExtendAreProgramsResidentNV

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);
}

我们在Chromium消息响应中的 return_ptrwriteback_ptr 处接收数据,并且能完全控制 n。这意味着泄漏的数据可以位于消息缓冲区中的任意偏移量处,而无需进行边界检查。唯一的限制是 n 必须为非负数,否则我们将遇到其他整数溢出问题,在调用程序时崩溃。由于我们能够通过值 n 控制消息的分配大小和泄漏的偏移量,因此这是一个完美的用来公开 存储在堆上的指针 和 数据 的语句。

 

CVE-2018-3085

Chromium消息最终由 src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c 里的 crServerDispatchMessage 函数处理

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
}

很明显,如果 msg 完全由 客户端 控制,将能够以各种方式中断。尤其,客户端虚拟机可以将消息类型设置为 CR_MESSAGE_REDIR_PTR,并设置 msg-> redirpt 用来将其指向伪造的 CR_MESSAGE_OPCODES 消息。如果伪造的消息产生响应,它将被写入 pCmdData-> pWriteback,这也是攻击者能够控制的,因为它是从 msg-> redirptr获取过来的。现在我们已经知道可以使用 CR_WRITEBACK_EXTEND_OPCODE消息 来控制8个字节的响应,如果我们可以注入 CR_MESSAGE_REDIR_PTR消息,问题将仍然存在。

如果通过HGCM访问Chromium子系统 src/VBox/GuestHost/OpenGL/util/vboxhgcm.c 里的 _crVBoxHGCMReceiveMessage 函数,将负责从缓冲区读取消息并将其放入Chromium处理队列中:

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);
    }
}

我们将看到两种不同的情况:
如果 conn-> allow_redir_ptr为 true,则分配 CR_MESSAGE_REDIR_PTR消息 并指向客户端虚拟机所提供的消息。如果不是这种情况,则将 客户端虚拟机消息 直接放入消息队列中。

 

触发漏洞

如果 allow_redir_ptr 永远为 true,那么由于 _crVBoxHGCMAlloc 的工作方式,use-after-free 将无法被利用。 那这个标志是什么意思呢? cr_net.h 中的注释给出了一个线索:

/* 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;

由于Chromium必须能够同时处理多个连接,即VirtualBox的多个HGCM连接,它需要复用来自不同客户端所有传入的OpenGL命令。如果一个客户端发送了 glBegin,它将无法处理来自其他客户端的命令,直到接受了相应的 glEnd
虽然情况确实如此,但对于其他客户端,allow_redir_ptr 仍然为 false。在 src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.ccrVBoxServerInternalClientWriteRead 函数中可见端疑:

    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;
    }

因此,要触发 allow_redir_ptr == 0分支,我们只需要在一个客户端发出 glBegin,然后在另一个客户端发送伪造的消息,它将被放入消息队列中而不进行检查。在发送了 glEnd 后,才会被处理。所以一下是第一个攻击计划:

  1. 在客户端A中发出 glBegin
  2. 在客户端B中发送伪造的 CR_MESSAGE_REDIR_PTR
  3. 在客户端A中发出 glEnd
  4. 炸了没?

然而这并没有什么卵用

我们必须继续处理,因为以下事件并不会自动完成:由于 路径[[5]] 中不合适的空闲时机,我们在步骤2中发送的消息在被处理之前释放掉了。如果我们选择通过 路径[[2]] 在步骤2中分配消息,那么来自步骤3的消息又将覆盖它并将被处理两次。如果我们通过 路径[[3]] 分配它,那么(至少在Linux和Windows上)在 free(释放)之后它所包含的一些堆元数据将会是些无效数据。

所以,替代方案如下

1 . 在客户端A中发出 glBegin
2 . 在客户端B中发送一个大的伪造 CR_MESSAGE_REDIR_PTR,它将触发 路径[[3]](操作系统提供的 malloc函数)
3 .通过调用具有相同大小和内容的 HGCM 来写入一些缓存,希望它们占用被释放掉的消息缓冲区
4 . 在客户端A中发出 glEnd
最终,来自 步骤3 的消息将在处理之前重用 步骤2 中的消息空间,且最终结果如我们所期待的那样:完全控制传递给 crServerDispatchMessage 的消息,并实现 write-what-where 语句
与之前的信息泄漏配合,这可以变成更灵活和可重复的写语句,并最终实现 任意读/写

(完)