Hunting for bugs in VirtualBox (First Take)

26 July 2020

Introduction

Recently, I managed to discover several memory corruption bugs affecting VirtualBox. I would like to share with you my experience, methodology of discovery and some information about VirtualBox internals.
I think VirtualBox is a really nice target for vulnerability research:

During this first attempt I managed to find two DoS bugs with fuzzing (patched in April CPU 2020) and I also identified a double-free vulnerability that possibly leads to guest-to-host escape (patched in July CPU 2020) by manually reviewing the code.

Despite the fact that this article is full of links and references to unofficial GitHub mirror of VirtualBox, I would you recommend downloading source code of corresponding version from official website, if you want to read the code though by yourself. I made these links only for the sake of readability, because I don’t want to mix a lot of enormous code pastes with the text.

Recon

When starting vulnerability research, the first thing everyone should do is to try to find resources about previously discovered bugs. For me it is hard to overestimate value of writeups by Reno Robert. From his blog you can learn about global architecture of the project, how source tree is organized, interesting types of discovered bugs and many other things. My personal favorite bug described in his blog so far is the one, which was introduced by compiler optimization.

Another very valuable set of resources is provided to us by Niklas Baumstark. He made a very interesting talk (video, slides), in which he covers multiple attack surfaces, security issues he found and opportunities for further research. He also wrote an article in phoenhex team blog about VirtualBox 3D acceleration bugs he found.

If you have already watched Niklas’s talk then you should know that there are also several informative reports from Google Project Zero members (blogpost by James Forshaw and report by Jann Horn). They tell us that VirtualBox bugs don’t narrow down to Guest-To-Host escape via memory corruption bugs, but there is also some potential for EoP on the host system, if you can find a way access VirtualBox host driver. I really recommend watching Niklas’s talk, because in it he provides interesting details about this attack vector.

Last but not least, there is a research made by Phạm Hồng Phi about bug in Intel PRO/1000 MT Desktop (82540EM) device emulation code. This network card device usually goes by the nickname E1000 or e1k. It is a default networking (emulated) device, when VM is started with networking adapter in NAT mode. Lately he also made an interesting article, which covers the process of fuzzing e1k device code.

That’s pretty a lot of information, but if you successfully managed to read it though, then you should be able now to choose your path start your journey into the code. Personally, I decided to stick with memory corruption bugs for now and to look for some Guest-to-Host escapes in networking emulation code with default VM settings.

To explore this attack surface I would assume that attacker has enough privileges in guest system to be able to communicate with emulated devices and/or open privileged raw sockets. In most cases that means attacker has root privileges inside guest OS.

Networking overview

The process of sending networking packet inside emulated environment is completely transparent to the guest operating system. It communicates with virtualized e1k device by I/O memory or I/O ports the same way it would communicate with real device. Emulated device reconstructs network packet and incapsulates it to the next layer – NAT, in our case. Since both guest and host machines have their own IP addresses inside NAT network, every packet sent by the guest to the global network has to be parsed and edited. Updated packet has to contain updated values of source and destination IP, and new checksum. This process is implemented in the “slirp”, which is a TCP/IP emulation library. Depending on the protocol being used slirp can send packet to the outer world or fully emulate network communication, like it does with DHCP or ARP protocols.

From various articles we already know that on each of those level there were previously found vulnerabilities, and maybe there are some of them left. To find these bugs we probably will have to utilize some new methodology or to look for more obscure bugs very carefully.

A hunting we will go

One of the main function we are going to be exploring, where the whole TCP/IP emulation starts, is void slirp_input(PNATState pData, struct mbuf *m, size_t cbBuf) (code). Each processed network packet passed to this function is wrapped in a special mbuf structure, which deserves some special attention. struct mbuf is a relic, which was inherited from BSD systems by the most TCP/IP stacks. I should say that there are several implementations of slirp on the internet and most of them use that strange artifact. In example, TCP/IP stack of QEMU is based on libslirp public library makes use of mbuf structure as well. But as you can see from the copyright notice in VirtualBox sources, their personal edition of slirp is based on FreeBSD implementation. Thus, we will have to understand layout of this structure and mbuf management interface to be able to read through the code. If you want to introduce yourself to mbuf interface, I would recommend you reading through this page.

As we know from blog of Reno Robert there were several vulnerabilities previously found in slirp module, but I had no evidence that anyone has utilized coverage-guided fuzzing to try to find vulnerabilities there. In this case, there is only one way to check it – to fuzz slirp and see, what we would find. My personal favorite fuzzer at the moment is libfuzzer and it was so easy to fuzz slirp_input with it. To write the simplest target function we just have to use API provided to us to create mbuf, which would contain our data. Target function turned out to be quite simple, the hardest part was compiling slirp by itself without the rest of VirtualBox and patching the source code, so it would be okay with clang.

This was my first scratch of LLVMFuzzerTestOneInput function that I wrote:

        
        1234567891011121314151617181920212223
        int LLVMFuzzerTestOneInput(char *d, size_t s)
{
    int rc;
    if (s < 10)
        return 0;

    if (!is_slirp_init) {
        rc = slirp_init(&pnatstate, 0x2000a, 0xffffff00,
            true, false, 0, 0x64, NULL);
        is_slirp_init = 1;
    }

    char *buf = NULL;
    size_t bufsize;

    struct mbuf *m = slirp_ext_m_get(pnatstate, s, &buf, &bufsize);

    buf = mtod(m, char *);
    memcpy(buf, d, s);

    slirp_input(pnatstate, m, s);
    return 0;
}

      

A careful reader would notice that this target function is far from perfect:

Later I was able to significantly improve target function, but even this toy example, which I used to check if fuzzing works at all, managed to find two vulnerabilities.

CVE-2020-2951

First bug that was found is unprivileged DoS by null pointer dereference in DHCP module and it is pretty straight forward (diff). In function dhcp_decode_request (code) on line 431 BOOTPClient *bc variable, which would be important to us, is initialized.

To trigger the vulnerability, we have to achieve following conditions:

Due to the fact that REBINDING value is not handled in a switch-case statement, we would hit default case and break out of this statement to the function call of dhcp_send_ack (on line 585), where bc variable would be dereferenced, while it is still equal to NULL.

Interesting thing about this vulnerability is that there is no need of superuser privileges to trigger it, only permissions to open UDP socket are required. In my opinion, this might have been one of a very few vulnerabilities that were still triggerable with normal user permissions in normal environment.

CVE-2020-2929

Next bug turned out to be my first bug collision ever, and it is a privileged DoS by accessing out-of-bounds value (diff). This bug is pretty simple as well.
Deeper in VirtualBox slirp module there is the “libalias” library being used to manage multiple IP addresses per one interface. At some point function ip_input is making a call to LibAliasIn (code). It is done right after initial checks of ip header are made, so it means that no checks regarding TCP/UDP layer data have been made. LibAlias in its turn makes false assumption that such header (udp in our case) is well formed.

LibAlias would determine the protocol and pass a packet to the UdpAliasIn function (code). After that it would determine protocol of the higher level and pass it to appropriate protocol handler (see find_handler function). In our case the vulnerability itself is located in handler of netbios protocol (code). This function assumes that end of the packet is at pmax = (char *)uh + ntohs(uh->uh_ulen); (line 886) and no checks of uh->uh_ulen value are made, so it could point outside the ip packet, meanwhile, uh->uh_ulen is a uint16_t value controlled by remote user. Due to this out of bound read will occur in AliasHandle(Question|Resource) (in example, there) leading to a DoS, because unallocated memory is being accessed outside of region allocated for mbufs.

To trigger this bug access to raw sockets is required, because we need to trick the kernel into passing UDP packet with invalid header to a network card. There several questions about this bug you can ask:

Preliminary results

These results tell us that slirp module hasn’t been fuzzed to death so far. You may try to find some tricky vulnerabilities missed by me and other researchers.

I can tell that my final fuzzer isn’t perfect as well, I didn’t have time to fully solve the problem of fuzzing IP reassembly, but after reading the code couple of times I didn’t find any security related issues.

By this time, you should be questioning yourself you even you bother reading another article written by fuzzkiddy. So here comes the main part, which is exactly the reason, why I decided to write this blogpost.

CVE-2020-14713

The bug

First of all, I need to say that this vulnerability can only be triggered with host OS being non-Windows system, since ICMP is handled in Windows differently.

While performing manual review of ICMP emulation I stumbled upon interesting part of code inside icmp_input function (code). This part of code is responsible for handling ICMP echo requests and sending them to the outer network. First let’s take a moment to take a look at a few finishing states of this function: there is an error state called end_error_free_m, which frees the allocated mbuf and done state, which is supposed to be a finishing state, in case there is no need to free our buffer, because it has already been freed.

Take a closer look at the case, when sendto transmission would fail. In this case icmp_error function would get called, then the control flow would reach the end of switch-case and we would result in end_error_free_m. But if you peek into the icmp_error function, you would be able to see that it actually always frees the second argument after the end. Even the comment above this function says that. It seems that there is a missing goto done; after icmp_error call. As a result our mbuf is freed for the first time in icmp_error and for the second time, when we would reach m_freem(pData, m); in icmp_input. It totally looks like a vulnerability! But to be sure we have to look into the source of allocation algorithm.

As we can learn from DrvNAT.cpp, the function slirp_ext_m_get (code) is responsible for allocating mbuf, that will be later passed into the slirp_input. You can see from slirp_ext_m_get there are several types of buffers that could be allocated for us depending on the size. The allocation process later continues in m_getjcl function, which is a function from mbuf interface (code). In this function we are able to see that there are multiple allocation “zones”. Structure mbuf is allocated from the global zone_mbuf (line 598) and buffer with raw data will be allocated in another zone depending on its size (lines 605-607). Inside uma_zalloc_arg function we can see that is does some locking on the zone and makes a call to zone->pfAlloc and zone->pfCtor. By applying dynamic analysis (haha gdb go brrr) I know that these values should be equal to the following:

        
        123456
        pfCtor = <mb_ctor_clust>,
pfDtor = <mb_dtor_clust>,
pfInit = NULL,
pfFini = NULL,
pfAlloc = <slirp_uma_alloc>,
pfFree = <slirp_uma_free>,

      

Finally, function slirp_uma_alloc contains the real allocation algorithm based of free-lists, which looks very simple comparing to modern allocators. In most cases call to this function will perform simple unlinking from the current list (line 180), insertion into the used-list (line 181) and that’s it. The free-list has been already filled during initialization stage in call to uma_zone_set_max (code) from mbuf_init function.

Deallocation goes almost just the opposite way as you can see from slirp_uma_free function. It performs unlinking from the current list and links it into the free-list.

That means after one mbuf has been freed twice, there is no chance we would be able to allocate the same buffer two times in a row. During the first call to m_freem chunk will be placed for used-list to free-list, and on the second call it would be unlinked from free-list and linked back to the free-list again. That’s by design protection from double-free! Is this a game over?

Well, actually yes, but no

Of course, my first thought was that it is not a bug, so I gave up on this and decided to move on and review some code of e1k device emulation. After reading through some code drvNATNetworkUp_SendBuf function caught my eye. This function is called from e1kTransmitFrame as callback, when e1k emulator has reconstructed a network packet and ready to present it to slirp. But this function doesn’t really call slirp_input or processes a packet in any way. Instead it creates a function call request, places it into the request queue named hSlirpReqQueue and another thread called TaskSet0 will process this request. But the process of buffer allocation actually happens during call to drvNATNetworkUp_AllocBuf from e1kXmitAllocBuf (code), which happens on the same NAT thread without any function call request. That means that the process of buffer allocation and call to slirp_input happen on the different threads.

This allows us to race NAT and TaskSet0 threads, so a new allocation would occur during first and second call to m_freem.

18702bd0.png

During first free on T2 our mbuf m will be placed on the top of a free-list, and during next allocation (T3) the same mbuf will be picked from the top of free-list and placed into the used-list again. Then the second free will occur in slirp thread (T4), that will place m again in free-list and allow us to allocate the same mbuf again on T6.

This means that at least we would be able to change data of the network packet on the fly while it is being processed. I created a fake e1k Linux driver that was feeding packets to the emulated device in a special way to land this race.

Exploitation

Exploitation of this issue is really hard since we have to nail down this race in microseconds. I have spent some time trying to write full exploit for issue, but I didn’t manage fully construct leak + code execution chain. Since exploitation of this bug requires winning a race – it is very hard to test it, so eventually I decided that I should not concentrate that much on trying to create a code execution exploit. Instead I can spend this time to try to find more security related issues.
I would be very happy if someone eventually would be able to create a full Guest-to-Host escape exploit.

Conclusion

I hope this article was entertaining enough for you and maybe you have discovered something new, because “you know, I learned something today” for sure.
Stupid bugs may slip though the QA process. It is possible that they don’t do a lot of fuzzing in Oracle QA, so I would expect more bugs to be found with fuzzing shortly. I have also tried to fuzz e1k device with libfuzzer and didn’t manage to succeed due to many issues during compilation. At the moment, I think that approach presented by Phạm Hồng Phi in his article may be one of the best solutions to overcome all the issues.

I wouldn’t be surprised that there are bugs still present in the slirp or e1k. Source code diffing can show us that there used to be vulnerabilities just a couple of months ago.

My communication with Oracle went very smooth, so big thanks to them.