... Loading ...

SSD Secure Disclosure

Disclosing vulnerabilities responsibly since 2007

SSD Advisory – VirtualBox VRDP Guest-to-Host Escape

Vulnerability Summary
VirtualBox has a built-in RDP server which provides access to a guest machine. While the RDP client sees the guest OS, the RDP server runs on the host OS. Therefore, to view the guest OS the RDP client will make a connection to the host OS IP address rather than the guest OS IP address.
The VRDP server is composted of two parts: a high level, which is open source and residing in the VirtualBox source tree, and is responsible for the display management, and a low level shipped with Extension Pack which is the RDP server which conforms to RDP specifications.
The vulnerability is in the high level part. The vulnerability can be triggered when a connection to a Windows guest OS is closed, i.e. when we close the window of the RDP client application like rdesktop or Microsoft Remote Desktop.
While the crashing bug was reported to the VirtualBox tracker (https://www.virtualbox.org/ticket/16444), it was never considered a security vulnerability, and is not marked as one. This ticket is 15 months old at the time of writing this post and still marked as unresolved.
Prerequisites to exploit the vulnerability:

  • VirtualBox Extension Pack installed on a host. It’s required to enable VRDP server
  • VRDP server enabled
  • 3D acceleration enabled
  • Windows 10 as a guest

The vulnerability can probably be triggered from other guest OS due to the fact the the vulnerable code resides inside the Guest Additions driver.
An independent security researcher, Sergey Zelenyuk, has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.

Affected systems
VirtualBox version 5.2.10
Vendor response
We reported this vulnerability to Oracle, the latest update from them is that they are still looking into it, while in fact the latest version of Oracle VirtualBox version 5.2.18 has silently introduced a patch without giving credit or mentioning of the vulnerability report. We do not know at this time if this fix was intentional (to fix our report) or done for some other reason, the change log does mention: “VRDP: fixed VM process termination on RDP client disconnect if 3D is enabled for the virtual machine”.
Vulnerability Analysis
General analysis
The vulnerability consists of two parts: a type confusion and a UAF. It’s not clear which of them is a bug and which one was the developer’s intention. We will discuss them separately later in subsection Root Cause Analysis.
Starting from the end, when RDP connection is being closed we gain control at the following place in /VirtualBox-5.2.8/src/VBox/Main/src-client/ConsoleVRDPServer.cpp file, line 1994:

The corresponding assembly is in VBoxC.so library:

Root Cause Analysis
Stopping at ConsoleVRDPServer::H3DORVisibleRegion we get a stack trace (here we use binaries with symbols compiled by us rather than those downloaded from VirtualBox website):

Frames #22 – #14 are a generic handler of VDMA requests including calls to Shared OpenGL Service (Chromium Service) from a guest or a host. Frames #13 – #9 do preparations for the following creation or close of displays. (Display is a part of screen sent to a client. There may be several displays, one of them may represents an entire screen and another may be a little rectangle as an update for the screen.) At frames #8 – #7 we reach the point where a type confusion occurs.
Type Confusion
Frame #7 is of the following function crPMgrFbDisconnectDisplay:

The second argument is an object of CrFbDisplayBase class. This class has the following subclasses: CrFbDisplayComposite, CrFbDisplayWindow, CrFbDisplayWindowRootVr, CrFbDisplayVrdp. In our case the type of pDp object is not CrFbDisplayBase but CrFbDisplayVrdp so its virtual table pointer references CrFbDisplayVrdp table. Please note this.
When pDp->getContainer() is called the call goes to the base class’ method getContainer because only CrFbDisplayBase implements it. The return value of this method is an object of type CrFbDisplayComposite. It’s strange because in our case the object is actually of type CrFbDisplayVrdp.
This allows to pass the check and to call CrFbDisplayComposite::remove() method (frame #6). This method calls CrFbDisplayBase::setFramebuffer, which has another interesting line:

We can assume that the code was written with an intention to call fbCleanup() on an object of CrFbDisplayBase type, but the current object type is CrFbDisplayVrdp (remember the virtual table pointer). Hence, instead of call to CrFbDisplayBase::fbCleanup() we call the CrFbDisplayVrdp::fbCleanup() function.
CrFbDisplayVrdp::fbCleanup() calls method fbCleanupRemoveAllEntries() which is implemented in the base class only so we’ve arrived to CrFbDisplayBase::fbCleanupRemoveAllEntries() which is the root of UAF and the entire vulnerability.

The loop iterates through all displays and calls EntryRemoved() for each display, where HCR_FRAMEBUFFER_ENTRY is a structure pointer represents a single display. Again, EntryRemoved() is called using CrFbDisplayVrdp virtual table rather than one of CrFbDisplayBase. Skipping an analysis of how the deletion is performed, let’s analyze what happens when CrFbVisitCreatedEntries is called.

The first argument is the container of all displays, the second is a callback called for each display, and the third is an argument for the callback. This procedure iterates through all the displays and calls the callback. Now look at the callback itself.

Not diving deeper, EntryDestroyed() is actually CrFbDisplayVrdp::EntryRemoved() which removes a display and frees its memory. Now you can see what’s wrong: in just one iteration of the loop of fbCleanupRemoveAllEntries() all displays are deleted and freed, and the second iteration will use already freed memory.
Controlled Memory Analysis
Each display (HCR_FRAMEBUFFER_ENTRY) has a hash table where a value is a pointer to a structure describing coordinates of the display. For each display there is usually only one entry in the hash.

This is a “glue” between the high level of VRDP Server and the rest of VirtualBox. While the hash table holds just void pointers, when they are passed to ConsoleVRDPServer::* methods they are casted as H3DORInstance.
Back to the assembly, let’s look what memory is referenced in method ConsoleVRDPServer::H3DORVisibleRegion when it’s called during normal conditions.

$rax-0x10 is a malloc_chunk of size 0x30 and $rax points to a H3DORInstance. You can see “w” (width) field is 0x400 and “h” (height) is 0x298 – it’s the resolution of our RDP client display. Let’s break on this place when RDP session is being closed.

Memory being referenced is a freed chunk stored in fastbins. The first two qwords at $rax was replaced with malloc_chunk* fd and malloc_chunk* bk, respectively. The code takes the first qword, dereferences it and again dereferences at 0x320 offset. We need to switch to binaries compiled by ourself with symbols and disabled optimization to show what’s really pointed by the first qword at this moment.
The next snippet is a list of displays and corresponding H3DORInstance-s at the first iteration of the loop, when no displays were freed yet.

They look fine. Now lets pass one iteration to free all the displays and dump the display structures again.

We can see that the first qword of an entry (freed) is a pointer to a previous entry (also freed) minus 0x10, i.e. is a pointer to a malloc_chunk of a previous entry. Next, we continue to break on our crucial code which is occurred in the current iteration (remember we are using unoptimized binaries compiled by us at the moment).

As you can see, $rax holds a pointer to the second H3DORInstance from the bottom (0x7fb79cf983c0) and the first qword is a pointer to malloc_chunk of another freed H3DORInstance (0x7fb79d12dc50-0x10).
The main thing required to exploit this vulnerability is to be able to create an arbitrary number of H3DORInstance-s and to spray the heap around it to point [[$rax]+0x320] to an executable code we control.
Source Code
You can find the full source code of the exploit here: VirtualBox VRDP Guest-to-Host Escape.
The exploit contains three parts:

  • Guest usermode executable launcher (vrdpexploit_launcher.exe)
  • Guest usermode library to inject in dwm.exe process (hostid_hijacker.dll)
  • Guest kernelmode driver (vrdpexploit.sys)

The exploit requires an elevated privileges to load the driver. In theory, on OSes other than Windows 10 the privileges may not be required.
Exploitation Algorithm

  1. An attacker runs vrdpexploit_launcher.exe with elevated privileges.
  2. Stage 1: escalation
    1. The launcher loads the driver.
    2. The driver escalates privileges of the launcher process and dwm.exe process to SYSTEM.
  3. Stage 2: hijacking
    1. The launcher injects the library to dwm.exe process and hijacks an identifier required to successfully spray the host heap later.
    2. The hijacked identifier is returned to the launcher.
  4. Stage 3: exploitation
    1. The launcher suspends dwm.exe process to stop any guest-host communication related to a display updating. The display is “freezed”.
    2. The driver connect to the Chromium service on the host via HGSMI (Host-Guest Shared Memory Interface).
    3. The drivers sends a Chromium command to make an information leak and obtain host addresses.
    4. The driver sends commands to the host to spray the heap.
    5. The driver writes a shellcode to video memory. VRAM is shared between the guest and the host, on the host side a mapped VRAM region has RWX attributes set.
    6. The driver reverts dwm.exe privileges back.
  5. Final stage
    1. An attacker closes RDP connection to trigger an execution of the shellcode in VRAM on the host to spawn /usr/bin/xterm.
    2. On the guest, the loader continues dwm.exe process and exits itself. The display is “unfreezed”, the VM continues to work.

Stage 1: Escalation
The launcher (vrdpexploit_launcher.exe) loads the driver (vrdpexploit.sys) and sends IOCTL_ESCALATE request. The driver finds EPROCESS of System, the launcher, and dwm.exe processes. Then it saves an access token of dwm.exe process to revert it back after the exploitation, and replaces tokens of the launcher and dwm.exe with a token of System.
Stage 2: Hijacking
Reflective DLL Injection tool by Stephen Fewer is used to simplify an injection. When the library (hostid_hijacker.dll) is injected into dwm.exe it patches the following code to jump to a shellcode.

The patch modifies the code right after VBoxD3DIfSurfGet call:

At startup time, before patching, the library modifies 0x4141414141414141 with an address of the shellcode.

The shellcode takes pSrcSurfIf, the value returned by VBoxD3DIfSurfGet, and goes through several structures to get Host ID. After that the shellcode restores an original bytes at the place of the jumper. This process of patching, hijacking, and restoring is repeated for 4 times. It’s because there are several Host ID and we must not accidentally take the lowest. For more details, see the HostIdHijacker.c file (not included in this blog post, for details on how to obtain it see the bottom of this post).
After Host ID is gathered it’s returned to the launcher process via WriteProcessMemory.
Stage 3: Exploitation
dwm.exe process is suspended using PsSuspend tool by Sysinternals.
The launcher sends IOCTL_EXPLOIT command to the driver. The driver initializes HGSMI interface to communicate with the host.
ASLR Bypass

To bypass ASLR we need an additional vulnerability, ideally an information leak. There is such vulnerability in a handler of CR_GETCHROMIUMPARAMETERVCR_EXTEND_OPCODE Chromium command. The handler allocates a buffer on the stack and then reads it with length specified in the command, without a boundaries check. This way we able to obtain addresses inside VBoxSharedCrOpenGL.so and VBoxDD.so.

DEP Bypass
Not so difficult protection for this day. It might be enough to make a ROP chain but we have a quite scarce control of registers at the time of the vulnerable call [rax+320h], so I decided to search for another ways. It turned out that a host VirtualBox process has a memory region corresponding to guest video memory (VRAM) and its protection is RWX. If there is a pointer to VRAM in the host process, we could leak it using the information leak bug described above and transfer the control to VRAM where a shellcode written by our guest driver will be residing.
Indeed, there is a global variable in /src/VBox/HostServices/SharedOpenGL/crserverlib/server_main.c that stores an address of VRAM:

Moreover, server_main.c is a part of VBoxSharedCrOpenGL.so library which address can be easily leaked, and the variable itself is placed at the fixed offset from the libary.
Thus to bypass DEP we leak VBoxSharedCrOpenGL.so address, add a fixed offset to it to obtain a pointer to g_pvVRamBase variable, and then force the host process to place the pointer in such way that later our ROP code will read a VRAM address from the pointer and will transfer the control to video memory. As we’ll see soon, just one ROP gadget is enough for that.
Heap Spray
The last step is to spray the heap. We need to create many H3DORInstance-s following by a chunks with content controlled by us. To create a display I send VBOXCMDVBVA_FLIP command, as it does the WDDM driver. To allocate chunks of arbitrary content I send CR_PROGRAMNAMEDPARAMETER4DVNV_EXTEND_OPCODE command. This command accepting a buffer as an argument allocates memory and copies the buffer content to it, but doesn’t deallocates it even if the command is failed. I use this “feature” to pass the buffer of the following content:

As you can see, our buffers contain only two values. The first, a pointer to the rop gadget, is placed at addresses modulo 16, so one of them will be used in the vulnerable call command. Remember:

The second, a pointer to g_pvVRamBase, is placed at addresses not modulo 16, and one of them will be used in the ROP gadget:

Summarizing, the heap layout at the time of the vulnerable call will be like that:

Here is the main part of the heap sprayer. It creates 64 displays and sprays 16384 chunks of size 0x2F8 holding the buffer shown above for each display.

Shellcode and Process Continuation
When the call to the rop gadget is performed we are jumping into the shellcode residing in mapped VRAM:

The shellcode does fork+execve to spawn xterm in the child process. To continue execution of the virtual machine, we must configure RBP and RSP in such way that the RET instruction will return us back to svcHostCallPerform function where the RDP connection closure was initiated. In order to do so, one adds 0x2B0 to RBP and 0x1D0 to RSP.
Oracle has fixed the vulnerability by moving the function call causing the double free out of the loop:

Print Friendly, PDF & Email

Leave a Reply