CVE-2020-8871: Privilege Escalation in Parallels Desktop via VGA Device

May 21, 2020 | Lucas Leong

Parallels Desktop for Mac is one of the most popular virtualization programs for macOS, but there hasn’t been much public vulnerability research regarding this product. Last November, Reno Robert (@renorobertr) reported multiple bugs in Parallels to the ZDI, one of which could allow a local user on the guest OS to escalate privileges and execute code on the host. This bug was patched in May with version 15.1.3 (47255) and was assigned CVE-2020-8871 (ZDI-20-292). This blog takes a deeper look at that vulnerability and the code change Parallels made to fix it.

Initial Analysis

All the following analysis is based on version 15.1.2. As tested, the guest virtual machine was configured with the default options.

The original report was short and was found by simple fuzzing. Here is the relevant code from the Proof-of-Concept (POC):

Basically, this randomly and infinitely writes words to I/O ports 0x3C4 and 0x3C5. If you run the POC on an affected version of Parallels, it will crash the prl_vm_app process on the host OS. Each virtual machine on the system is represented by a separate prl_vm_app process.

Through a bit of research, we find 0x3C4 and 0x3C5 are the VGA sequencer index register and sequencer data register respectively. At first glance, it appears there is an Out-Of-Bounds (OOB) write bug in the VGA device. As mentioned, the POC is triggered by fuzzing, and the original report did not provide a detailed analysis. It’s time to go deeper.

Investigating the Root Cause

The crash is in a large function called sub_100185DA0. The related part is simplified and commented as followed.

The vga_context structure is allocated during the initialization of a VGA device. It keeps the status and variables for the VGA device. This function attempts to write vga_context->buf buffer sequentially with three loops. The total length is evaluated as vga_context->h * vga_context->w * sizeof(DWORD) bytes. Then, it performs an OOB write and crashes within the loops due to the invalid length.

The first step in our investigation is to determine the source of the vga_context->buf buffer contents.

This rather large 64MB buffer is a screen buffer, which is configured through the guest VM configuration (Hardware->Graphics->Memory). It seems to imply that vga_context->h and vga_context->w are the height and width for the guest VM screen resolution.

Next, we need to determine the source of the vga_context->h and vga_context->w buffer contents. We can get this answer from our debugger and find it is from vga_state in sub_100184F90.

But again, what’s the source of the vga_state object?

We find it is shared memory. In this instance, it is shared between host ring0 and host ring3. It is updated by the VGA I/O port handler in ring0 and, later, the ring3 video worker thread (located in sub_100183610) will use it.

According to the pseudo-code above, port 0x3C4 acts as a selector to control what goes on in port 0x3C5. One of the features for port 0x3C5 is that it can set an arbitrary 16-bit value to vga_state->h and vga_state->w. When the ring3 video worker thread gets the new height and width for the screen, it attempts to update the whole screen buffer (vga_context->buf). However, it does not validate the new height and width which leads to the overflow on the screen buffer.

Additionally, the length of the overflow is controllable. The value of the overflow is partially controllable through port 0x3C9 (see vga_context->array). As a result, we identified it is likely exploitable.

Evaluating the Patch

After the patch was released, I did some binary diffing between version 15.1.2 and 15.1.3 to determine how they chose to fix this bug. By checking the diff carefully, the patch did a very tiny change with the caller of sub_100185DA0.

One of the if branches has changed. The patch moved the flaggg from vga_state to vga_context.

What is flaggg?

As we can see in sub_100184F90, flaggg has to be TRUE to get the controlled height and width from vga_state. However, the flaggg has to be FALSE in order to go into the crashing function. These two constraints are conflicted.

How can we satisfy these constraints?

As we stated earlier when looking at the root cause, vga_state is shared memory between ring0 and ring3. The flaggg function can be configured through port 0x3C5. Therefore, it is possible to flip the flaggg and make the double fetch in the ring3 video worker thread between these two constraints.

What the patch actually did is to move flaggg from vga_state to vga_context. This improves the situation since vga_context is a heap allocation in ring3 and is not vulnerable to double fetch. Therefore, it will never trigger the path to the OOB write.

Conclusion

This submission provides a nice example of what it takes to go through the workflow and the root cause analysis for a virtual device in Parallels Desktop. While the vendor lists the patches as Low severity, considering the overall CVSS scores and the opportunity to escalate from guest to host, you should consider the patch to be Important in severity and apply it as soon as you can. We don’t see a lot of bugs in Parallels Desktop submitted to the program, but maybe this blog will encourage others to take a look. If you do end up finding some vulnerabilities, we’d certainly be interested in seeing them.

You can find me on Twitter at @_wmliang_, and be sure to follow the team for the latest in exploit techniques and security patches.