通过精妙构造,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_OPCODES
和 CR_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,
};
每个操作码都有一个关联的 解包器 和 调度程序,分别以 crUnpack
和 crServerDispatch
为前缀。 此特定操作码的解包器如下所示:
/* 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_PTR
和 SET_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_ptr
和 writeback_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.c
的 crVBoxServerInternalClientWriteRead
函数中可见端疑:
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
后,才会被处理。所以一下是第一个攻击计划:
- 在客户端A中发出
glBegin
- 在客户端B中发送伪造的
CR_MESSAGE_REDIR_PTR
- 在客户端A中发出
glEnd
- 炸了没?
我们必须继续处理,因为以下事件并不会自动完成:由于 路径[[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
语句
与之前的信息泄漏配合,这可以变成更灵活和可重复的写语句,并最终实现 任意读/写
。