Better slow than sorry – VirtualBox 3D acceleration considered harmful
Jul 27, 2018 • By niklasb
更新: 漏洞利用代码和HGCM/Chromium接口库现已发布在Github上。
VirtualBox的3D加速功能今年经历了一段艰难时期。从技术角度来说,由于文档中明确警告不建议使用此功能(强调原文):
不应允许不受信任的客户系统使用VirtualBox的3D加速功能,就像不应允许不受信任的主机软件使用3D加速一样。3D硬件驱动程序通常过于复杂,无法确保安全性,任何被允许访问它们的软件都可能危及运行它们的操作系统。此外,启用3D加速使客户机能够直接访问VirtualBox主机进程中的大量额外程序代码,这可能被用来崩溃虚拟机。
但这只是部分真相:根据设计,VirtualBox虚拟机主机进程可以访问VBoxDrv内核驱动程序。因此,即使这些进程以启动VM的用户权限运行,攻破此类进程也可用于本地权限提升,正如Jann Horn、James Forshaw和我在Insomni’hack 2018演讲中详细阐述的那样(幻灯片9-22,视频)。
在本博客文章中,我将描述CVE-2018-3055和CVE-2018-3085(ZDI-18-684和ZDI-18-685),一个信息泄露和一个有趣的设计问题,可以组合使用以在启用3D加速的客户机中完全攻破VirtualBox。这两个漏洞已在2018年7月的CPU(VirtualBox版本5.2.16)中修复。
概述
3D加速功能在代码库中被称为共享OpenGL,基于用于分布式OpenGL渲染的Chromium库,不要与同名的Web浏览器混淆,它比后者早7年!Chromium定义了一个网络协议来描述OpenGL操作,可以将其传递给实际的OpenGL实现。
VirtualBox维护了一个Chromium分支,并通过HGCM(主机-客户机通信管理器)隧道传输协议。HGCM本质上是一个非常简单的客户机<->主机RPC协议。一旦连接到HGCM服务,客户机可以使用整数和缓冲区参数进行简单的远程调用,并在主机端处理它们。返回状态代码,并且参数可能会被被调用者更改以将数据传回客户机。
有关HGCM的更多信息以及VirtualBox Chromium集成的概述,请参阅Francisco Falcon的REcon 2014演讲。
需要注意的是,HGCM接口通过客户机附加驱动程序暴露给非特权进程。如果未安装客户机附加组件,则需要root权限来安装客户机驱动程序并暴露设备,以攻击共享OpenGL。
Chromium消息基础
有不同类型的Chromium消息,由CRMessage联合类型表示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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操作码的字节数组,这些操作码以特殊方式编码。
例如,一个简单的消息可能如下所示:
1
2
3
4
5
6
7
8
9
10
|
uint32_t message[] = {
CR_MESSAGE_OPCODES, // msg.header.type
0x41414141, // msg.header.conn_id
1, // msg.numOpcodes
CR_EXTEND_OPCODE << 24 // 8位操作码,指示接下来是扩展操作码
0x42424242, // <填充,无论什么原因>
CR_WRITEBACK_EXTEND_OPCODE // 32位扩展操作码
0x43434343, // 此操作码的一些额外有效载荷数据
0x44444444,
};
|
每个操作码都有一个关联的解包器和分发器,分别以crUnpack和crServerDispatch为前缀。此特定操作码的解包器如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/* 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)
{
/* 这将解包缓冲区的CRNetworkPointer复制到writeback_ptr */
SET_WRITEBACK_PTR( 8 );
cr_unpackDispatch.Writeback( NULL );
}
|
这告诉Chromium将消息有效载荷偏移8处的数据写回响应缓冲区,在上面的示例中是字符串"ccccdddd"。我不确定此功能在合法使用中为何必要,但它为我们提供了一个“回声”原语,可以写回我们控制的数据,这肯定对利用有用。
通过越界WRITEBACK_PTR的信息泄露(CVE-2018-3055)
Chromium消息解析器中有几个地方使用用户控制的偏移调用SET_RETURN_PTR和SET_WRITEBACK_PTR。其中一个例子是src/VBox/HostServices/SharedOpenGL/unpacker/unpack_program.c中的crUnpackExtendAreProgramsResidentNV:
1
2
3
4
5
6
7
8
|
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泄露的偏移,这是一个完美的原语,可以披露堆上存储的指针和数据。
通过CR_MESSAGE_REDIR_PTR的绝对任意写入(CVE-2018-3085)
Chromium消息最终由src/VBox/HostServices/SharedOpenGL/crserverlib/server_stream.c中的函数crServerDispatchMessage处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
static void
crServerDispatchMessage(CRConnection *conn, CRMessage *msg, int cbMsg)
{
// ...
if (msg->header.type == CR_MESSAGE_REDIR_PTR)
{
#ifdef VBOX_WITH_CRHGSMI // 这在生产版本中定义
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;
// 在这里处理操作码...
#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->redirptr设置为指向伪造的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处理队列:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
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
{
/* 我们现在应该永远不会在HGSMI命令中禁用redir_ptr */
CRASSERT(!conn->CmdData.pvCmd);
if ( len <= conn->buffer_size )
{
// [[ 2 ]]
/* 放入预分配缓冲区 */
hgcm_buffer = (CRVBOXHGCMBUFFER *) _crVBoxHGCMAlloc( conn ) - 1;
}
else
{
// [[ 3 ]]
/* 分配新缓冲区,
* 这里不使用池,因为这很可能是一次性传输大纹理
*/
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在crserverlib/server_stream.c中用crNetFree释放。
* OOB消息是程序员的问题。 -- 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消息并指向客户机提供的消息。但是,如果不是这种情况,客户机消息将直接放入消息队列。
还要注意cached_type是客户机控制的:它是最初发送的消息的类型。出于某种原因,此字段值用于决定是否应在位置[[ 5 ]]释放消息,即使它刚刚在位置[[ 4 ]]放入处理队列。这在此时绝不可能是有效的做法,因为消息稍后将出队并处理。
因此,这里至少有两个独立的问题:客户机控制的消息直接排队的事实,以及在某些情况下在处理之前释放的事实(释放后使用)。
触发漏洞
我相当确信,如果allow_redir_ptr始终为true,由于_crVBoxHGCMAlloc的工作方式,释放后使用本身将无法利用。那么这个标志是什么意思?cr_net.h中的注释提供了一个线索:
1
2
3
4
5
6
|
/* 在主机端使用,表示我们不允许存储上述指针以供以后在crVBoxHGCMReceiveMessage中使用。
* 因为这些消息将在相应的HGCM调用完成且内存释放后处理。所以我们必须存储一个副本。
* 当与此连接关联的客户端消息处理被另一个客户端阻塞时会发生这种情况,
* 该客户端向我们发送了glBegin调用,我们正在等待接收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中找到:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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。
- 崩溃?
不幸的是,我们需要做更多工作,因为这本身无法实现:我们在步骤2中发送的消息将在处理之前由于在[[ 5 ]]处的不合时宜的释放而被释放。
如果我们选择通过路径[[ 2 ]]在步骤2中分配消息,那么步骤3中的消息将覆盖它并被处理两次。
如果我们选择通过路径[[ 3 ]]分配它,那么(至少在Linux和Windows上)在释放后,它包含一些堆元数据,不再是有效消息。
因此,我们可以做的是:
- 在客户端A中发出glBegin。
- 在客户端B中发送一个大的伪造CR_MESSAGE_REDIR_PTR,这将触发路径[[ 3 ]](操作系统提供的malloc)。
- 通过HGCM调用喷洒一些具有相同大小和内容的缓冲区,希望它们占据释放的消息缓冲区。
- 在客户端A中发出glEnd。
这至少在Linux主机上有效。想法是步骤3中的消息将在处理之前重用步骤2中的消息空间。最终结果正是我们想要的:我们完全控制传递给crServerDispatchMessage的消息,并实现写-什么-在哪里原语。
结合上面描述的信息泄露,这可以转变为更灵活和可重复的写入原语,并最终实现任意读/写原语。
参考文献
- 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