In late February 2024, I decided to perform some vulnerability research on VirtualBox. Even though I found two vulnerabilities that I reported through ZDI, the actual research experience was very painful. With many hours wasted in debugging efforts, it ultimately pushed me away from VirtualBox after only about two weeks of actual research.
In this blog post, I'll go through this experience, and cover one of the two vulnerabilities, as the other vulnerability is still a 0-day at this point in time.
Disclaimer
I have not looked at VirtualBox at all for ~1.5 years now, so a lot of the content in this blog post is written from memory, which means some of it could be inaccurate.
Table of Contents
Research Environment Setup
I documented how to set up a research environment using Ubuntu 22.04 extensively on a Github repo that you can find here:
The instructions were made for VirtualBox 7.0.14, while the latest VirtualBox version is 7.2.2, so they're actually a little bit out of date by now.
However, unless Oracle has updated their own build instructions (I haven't checked), my notes above will still most likely work better than theirs.
My notes also cover how to attach GDB to the VirtualBox process for debugging purposes.
The Vulnerabilities
Both vulnerabilities I found were buffer overreads that could be used for information leaks. ZDI accepted one of the vulnerabilities but rejected the other one.
Funnily enough, the vulnerability that ZDI did accept was practically useless, while the rejected vulnerability can easily be exploited to leak a large amount of heap memory, which makes it really powerful when combined with another vulnerability to achieve a VM escape.
So why was it rejected? Well, it required 3D acceleration to be turned on in the VM, and since it's not turned on by default, ZDI rejected my submission.
Bug 1 - 4-byte Stack Buffer Overread in EHCI
This is the bug that was accepted by ZDI. It lead to a 4-byte stack buffer overread. https://www.zerodayinitiative.com/advisories/ZDI-24-1034/
The emulated EHCI device in VirtualBox essentially reads Isochronous Transmit Descriptors (ITDs) from the guest VM. These ITDs are then processed by the ehciR3ServiceITD()
function. The relevant code is shown below:
static void ehciR3ServiceITD(PPDMDEVINS pDevIns, PEHCI pThis, PEHCICC pThisCC,
RTGCPHYS GCPhys, EHCI_SERVICE_TYPE enmServiceType, const unsigned iFrame)
{
RT_NOREF(enmServiceType);
bool fAnyActive = false;
EHCI_ITD_PAD PaddedItd;
PEHCI_ITD pItd = &PaddedItd.itd;
if (ehciR3IsTdInFlight(pThisCC, GCPhys))
return;
/* Read the whole ITD */
ehciR3ReadItd(pDevIns, GCPhys, &PaddedItd); // [ 1 ]
Log2((" ITD: %RGp={Addr=%x EndPt=%x Dir=%s MaxSize=%x Mult=%d}\n", GCPhys, pItd->Buffer.Misc.DeviceAddress, pItd->Buffer.Misc.EndPt, (pItd->Buffer.Misc.DirectionIn) ? "in" : "out", pItd->Buffer.Misc.MaxPacket, pItd->Buffer.Misc.Multi));
/* Some basic checks */
for (unsigned i = 0; i < RT_ELEMENTS(pItd->Transaction); i++)
{
if (pItd->Transaction[i].Active)
{
fAnyActive = true;
if (pItd->Transaction[i].PG >= EHCI_NUM_ITD_PAGES) // [ 2 ]
{
/* Using out of range PG value (7) yields undefined behavior. We will attempt
* the last page below 4GB (which is ROM, not writable).
*/
LogRelMax(10, ("EHCI: Illegal page value %d in iTD at %RGp.\n", pItd->Transaction[i].PG, (RTGCPHYS)GCPhys));
}
Log2((" T%d Len=%x Offset=%x PG=%d IOC=%d Buffer=%x\n", i, pItd->Transaction[i].Length, pItd->Transaction[i].Offset, pItd->Transaction[i].PG, pItd->Transaction[i].IOC,
pItd->Buffer.Buffer[pItd->Transaction[i].PG].Pointer));
}
}
/* We can't service one transaction every 125 usec, so we'll handle all 8 of them at once. */
if (fAnyActive)
ehciR3SubmitITD(pDevIns, pThis, pThisCC, pItd, GCPhys, iFrame);
else
Log2((" ITD not active, skipping.\n"));
}
The EHCI_ITD_PAD
and PEHCI_ITD
structures can be found here for reference.
In the above code, there are three issues. Let me explain some context first:
- At
[ 1 ]
, an ITD is read from the guest VM. This means that the contents ofPaddedItd
(and subsequently,pItd
) is fully controlled by an attacker. Note thatPaddedItd
is allocated on the stack. - The size of the
pItd->Buffer.Buffer
array isEHCI_NUM_ITD_PAGES == 7
. - The
PG
variable referenced at[ 2 ]
aspItd->Transaction[i].PG
is a 3-bit variable, meaning it can be between 0 and 7 inclusive.
Later on, this PG
variable is used as an index to access pItd->Buffer.Buffer
. Therefore, there are three separate bugs, two of which can be considered vulnerabilities:
- The check at
[ 2 ]
doesn't make sense.PG
is a 3-bit variable, and thus can never be greater thanEHCI_NUM_ITD_PAGES
. - The same check is also off-by-one. Since
PG
is used to accesspItd->Buffer.Buffer
, which has a size ofEHCI_NUM_ITD_PAGES
, ifPG == EHCI_NUM_ITD_PAGES
, thenpItd->Buffer.Buffer
will be accessed one index out of bounds. - Lastly, the check doesn't actually do anything, because all it does is output a log. No error is returned and the code does not exit early.
Accessing pItd->Buffer.Buffer
one byte out of bounds won't actually lead to a buffer overread, because the EHCI_ITD_PAD
structure that's in use here actually adds an extra element into this buffer array indirectly. You can see the details in the structure itself here.
However, when ehciR3SubmitITD()
is called later in the same function, it actually accesses pItd->Buffer.Buffer[pg + 1]
:
static bool ehciR3SubmitITD(PPDMDEVINS pDevIns, PEHCI pThis, PEHCICC pThisCC,
PEHCI_ITD pItd, RTGCPHYS ITdAddr, const unsigned iFrame)
{
// [ ... ]
for (unsigned i=0;i<RT_ELEMENTS(pItd->Transaction);i++)
{
RTGCPHYS GCPhysBuf;
if (pItd->Transaction[i].Active)
{
// [ ... ]
if (pItd->Transaction[i].Offset + pItd->Transaction[i].Length > GUEST_PAGE_SIZE)
{
// [ ... ]
// [ 1 ]
GCPhysBuf = pItd->Buffer.Buffer[pg + 1].Pointer << EHCI_BUFFER_PTR_SHIFT;
ehciPhysRead(pDevIns, GCPhysBuf, &pUrb->abData[curOffset + cb1], cb2);
}
else
ehciPhysRead(pDevIns, GCPhysBuf, &pUrb->abData[curOffset], pItd->Transaction[i].Length);
curOffset += pItd->Transaction[i].Length;
}
}
// [ ... ]
}
Since the attacker controls the entire pItd
structure, they can take the branch that leads to [ 1 ]
, where pItd->Buffer.Buffer[pg + 1].Pointer
is accessed. This access will be 1 index out of bounds.
This code effectively reads 4 bytes past the whole EHCI_ITD_PAD
structure on the stack, treats it as a pointer into guest memory, and reads some network related data from there.
And that is why this bug is useless - even though 4 bytes are read out of bounds, the bytes are treated as a pointer into guest memory and read from. There is practically no good way to use this bug in a real exploit.
In any case, here is the PoC trigger for this bug.
=================================================================
==73482==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ff9454b7584 at pc 0x7ff94ce74379 bp 0x7ff9454b7310 sp 0x7ff9454b7300
READ of size 4 at 0x7ff9454b7584 thread T28
#0 0x7ff94ce74378 in ehciR3SubmitITD /home/faith/VirtualBox-7.0.14/src/VBox/Devices/USB/DevEHCI.cpp:2880
Bug 2 - Controlled heap buffer overread in XXXX
Since ZDI rejected this submission, this ended up never being reported to VirtualBox, so it's still an unpatched 0-day right now.
Since it just leads to an information leak (and usually most bugs don't require separate information leaks in my experience) I'm ok with disclosing the following details:
- This bug requires 3D acceleration to trigger.
- It leads to a heap buffer overread of a controlled amount.
I personally have decided to just hold onto this bug for now. Maybe I'll come back to VirtualBox in the future and use this bug in combination with another bug to write a VM escape exploit.
In any case, here's a hash of the file that contains the explanation for this bug. If it ever gets fixed in the future, I'll share the contents of the file:
f1bdf47d6ca950c0733a4667940c4de4fb16c68b83c72d91034217c15107e023
The Pains of Debugging - Breakpoints Not Working
As I mentioned before, my research experience was very painful. There was one main reason behind it: Breakpoints just refuse to work sometimes.
When you install and run VirtualBox, there are two main components:
- The userland
VirtualBox
process - this is what you run VMs through. - The kernel
vboxdrv.ko
module - this is used by VirtualBox in certain cases when needing to perform actions that require higher privileges.
In the codebase, you can see that they make use of certain macros to determine whether some code is running in the host userland or the host kernel:
# if defined(IN_RING3) // Run in the host userland
# if defined(IN_RING0) // Run in the host kernel
As you may have noticed in my notes about building and debugging VirtualBox, I don't use a nested VM setup, which means that it's not possible for me to debug the code that runs in the host kernel.
I was already aware of this, and while doing my research, I stayed away from such code, and tried my best to only focus on code that ran in the host userland. And even in those cases, certain breakpoints in VirtualBox would refuse to work on GDB.
The most painful part of this was that adding print statements into the VirtualBox code and recompiling took forever. Honestly, it's a relief to me that the Linux kernel doesn't take so long to recompile after I add printk
statements to parts of the code.
A Potential Solution?
So why didn't I attempt a nested VM setup? I have two machines, one is a Windows 10 host while the other is an Ubuntu host. Here were the issues:
- The Ubuntu host (the one I did my research on) was a fairly old laptop, and the CPU just doesn't support nested virtualization.
- Windows 10 also does not support nested virtualization. For that, you need to use Windows 11.
Unfortunately for me, even though my Windows 10 machine has a CPU that supports nested virtualization, I simply could not figure out how to set it up at the time. Only after 1.5 years did I realize recently that I just needed to use Windows 11.
But here's the question: Would this actually have solved the breakpoints issue?
Honestly, I can't say for sure without trying. As I've said before, I was ensuring to only add breakpoints into code that was handled by the host userland, but maybe I made a mistake? Or maybe even that code could potentially be handled by the host kernel in some cases?
In any case, my suggestions to others (and to my future self if I ever go back to doing VirtualBox research):
- Please ensure you have a nested VM set up so you can debug the host kernel as well.
- If all else fails, you may have to heavily depend on adding print statements into the VirtualBox code and recompiling. This can waste a lot of time, so be mentally prepared for this (unlike old me 😅).
Conclusion
Overall, I'd say my research endeavour was somewhat of a success. I didn't achieve my goal of a VM escape exploit, but I did manage to find one good bug (that I unfortunately could not disclose in this post, sorry!).
Knowing what I know now, maybe I'll come back to this in the future and finally achieve my original goal.
Thanks for reading!