DF-0350 / harness.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 | /* * DF-0350 โ Code-level harness for the unbounded mesh route-table growth * + attacker-controlled lifetime in ieee80211_hwmp.c / ieee80211_mesh.c * * WHY A HARNESS (not a runtime PoC) * This KVM audit guest has NO WiFi radio: `ifconfig -l` shows only * `vtnet0 lo0`; no wlan vap, no ath/iwm/iwn kld bound to hardware, and no * netgraph 802.11 injection node (sys/netgraph* has no ng_80211/ng_wlan). * The runtime mesh RX path that reaches hwmp_recv_preq() therefore CANNOT * be exercised on this guest โ identical to the already-settled findings * DF-0393 (Mesh ID heap overflow), DF-0594 (TKIP RX underflow) and * DF-0616 (netmap RX overflow), all resolved via faithful in-process * harnesses because the live 802.11/netmap RX path is unreachable without * the relevant hardware. We follow that precedent. * * This harness embeds the VERBATIM allocation/insertion/lifetime code from: * - mesh_rt_add_locked() sys/netproto/802_11/wlan/ieee80211_mesh.c:194-228 * - ieee80211_mesh_rt_update() sys/netproto/802_11/wlan/ieee80211_mesh.c:266-303 * - the PREQ-originator handling in hwmp_recv_preq() * sys/netproto/802_11/wlan/ieee80211_hwmp.c:1055-1097 * and drives it with an attacker-shaped frame stream (distinct spoofed * originator MACs + maximal preq_lifetime) to prove the two claims: * (1) the route table grows WITHOUT ANY BOUND (no cap on ms_routes), and * (2) preq->preq_lifetime (attacker uint32) flows UNVALIDATED into * rt->rt_lifetime, letting a single PREQ pin ~150 B for ~49 days. * * FAITHFULNESS * - struct ieee80211_mesh_route is reconstructed field-for-field from * sys/netproto/802_11/ieee80211_mesh.h:420-440 (incl. the HWMP priv). * - struct ieee80211_mesh_state carries the TAILQ ms_routes * (ieee80211_mesh.h:525) and the route lock (a no-op in userspace). * - mesh_rt_add_locked() body is copied character-for-character, including * the `#if defined(__DragonFly__)` kmalloc path (modelled with malloc(3)). * - ieee80211_mesh_rt_update() body is copied character-for-character. * - The PREQ-originator block from hwmp_recv_preq() is modelled: find-or-add * the originator route, then update its lifetime with the attacker value. * * BUILD * cc -O2 -Wall -o harness harness.c # BUG PRESENT (unbounded) * cc -O2 -Wall -DFIXED -o harness_fixed harness.c # WITH CAP+CLAMP FIX * * RUN * ./harness [Nattack] # default Nattack=100000 distinct originators */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> /* ---- Constants reproduced from the kernel headers ---- */ #define IEEE80211_ADDR_LEN 6 #define IEEE80211_MESHID_LEN 32 /* ieee80211.h:200 */ #define M_80211_MESH_RT "M_80211_MESH_RT" #define M_INTWAIT 0x0000 /* placeholder: "wait ok" */ #define M_ZERO 0x0100 /* ieee80211_mesh.c uses M_ZERO */ /* ---- Fix constants โ MUST match fix.diff applied to sys/. The harness is * compiled with -DFIXED to demonstrate the fix closes the bug. ---- */ #define IEEE80211_MESH_RT_MAX 4096 /* per-vap route cap */ #define IEEE80211_MESH_RT_LIFETIME_MAX_MS (60*1000)/* clamp: 60 s (was 49 days) */ /* the harness references these as _FIX / _MAX_MS to avoid clashing with the * unpatched build; alias them: */ #define IEEE80211_MESH_RT_MAX_FIX IEEE80211_MESH_RT_MAX /* HWMP private route data (ieee80211_hwmp.c:139-146): hr_seq, hr_preqid, * hr_origseq (+ pad). mpp_privlen = sizeof(struct ieee80211_hwmp_route). */ struct ieee80211_hwmp_route { uint32_t hr_seq; uint32_t hr_preqid; uint32_t hr_origseq; }; /* ---- struct ieee80211_mesh_route reproduced VERBATIM (field order & sizes) * from sys/netproto/802_11/ieee80211_mesh.h:420-440 (x86_64 layout). * TAILQ_ENTRY(ieee80211_mesh_route) rt_next; // 2 ptrs = 16 B * struct ieee80211vap *rt_vap; // ptr 8 B * ieee80211_rte_lock_t rt_lock; // lock (modelled: int) * struct callout rt_discovery; // callout (modelled: int) * int rt_updtime; // 4 B * uint8_t rt_dest[6]; * uint8_t rt_mesh_gate[6]; * uint8_t rt_nexthop[6]; * uint32_t rt_metric; * uint16_t rt_nhops; * uint16_t rt_flags; * uint32_t rt_lifetime; * uint32_t rt_lastmseq; * uint32_t rt_ext_seq; * void *rt_priv; */ struct ieee80211_mesh_route { struct ieee80211_mesh_route *rt_next_le_next; /* TAILQ_ENTRY next */ struct ieee80211_mesh_route *rt_next_le_prev; /* TAILQ_ENTRY prev (TAILQ uses ptr-to-ptr; we model as simple list) */ void *rt_vap; int rt_lock_dummy; int rt_discovery_dummy; int rt_updtime; uint8_t rt_dest[IEEE80211_ADDR_LEN]; uint8_t rt_mesh_gate[IEEE80211_ADDR_LEN]; uint8_t rt_nexthop[IEEE80211_ADDR_LEN]; uint32_t rt_metric; uint16_t rt_nhops; uint16_t rt_flags; uint32_t rt_lifetime; uint32_t rt_lastmseq; uint32_t rt_ext_seq; void *rt_priv; /* -> appended HWMP priv */ }; #define IEEE80211_MESHRT_FLAGS_VALID 0x02 #define ALIGN(x) (((x) + sizeof(void *) - 1) & ~(sizeof(void *) - 1)) /* ---- struct ieee80211_mesh_state (the mesh vap state) modelled with a * simple singly-linked list standing in for TAILQ_HEAD ms_routes * (ieee80211_mesh.h:525). ms_ppath->mpp_privlen = sizeof(HWMP priv). */ struct ieee80211_mesh_proto_path_min { size_t mpp_privlen; }; struct ieee80211_mesh_state { struct ieee80211_mesh_route *ms_routes_head; /* TAILQ ms_routes */ int ms_routes_count; /* list length */ struct ieee80211_mesh_proto_path_min *ms_ppath; /* -> privlen provider*/ }; /* ---- The two macros referenced by the verbatim bodies ---- */ #define MESH_ROUTE_LIFETIME_MAX(a, b) ((a) > (b) ? (a) : (b)) /* ---- stubs for the kernel-internal helpers the verbatim bodies call. * `ticks` is a global counter; ticks_to_msecs is 1:1 here so the * lifetime arithmetic is faithful in units of "msec". */ static uint64_t g_ticks = 0; #define ticks g_ticks static uint64_t ticks_to_msecs(uint64_t t) { return t; } /* list helpers standing in for TAILQ (head/tail semantics irrelevant to the * growth/lifetime claims โ only membership & count matter) */ static void TAILQ_INSERT_TAIL_mesh(struct ieee80211_mesh_state *ms, struct ieee80211_mesh_route *rt) { rt->rt_next_le_next = NULL; if (ms->ms_routes_head == NULL) { ms->ms_routes_head = rt; rt->rt_next_le_prev = NULL; } else { struct ieee80211_mesh_route *p = ms->ms_routes_head; while (p->rt_next_le_next) p = p->rt_next_le_next; p->rt_next_le_next = rt; rt->rt_next_le_prev = p; } ms->ms_routes_count++; } static struct ieee80211_mesh_route * mesh_rt_find_locked(struct ieee80211_mesh_state *ms, const uint8_t dest[IEEE80211_ADDR_LEN]) { struct ieee80211_mesh_route *rt; for (rt = ms->ms_routes_head; rt != NULL; rt = rt->rt_next_le_next) if (memcmp(dest, rt->rt_dest, IEEE80211_ADDR_LEN) == 0) return rt; return NULL; } /* ============================================================================ * VERBATIM vulnerable code from sys/netproto/802_11/wlan/ieee80211_mesh.c:194-228 * mesh_rt_add_locked() โ the SINGLE choke point for all PREQ/PREP/RANN adders. * (ms_ppath->mpp_privlen == sizeof(struct ieee80211_hwmp_route) under HWMP.) * The ONLY change vs. the kernel: kmalloc() is modelled with malloc(3), and * under -DFIXED we insert the cap check BEFORE the kmalloc. * ============================================================================ */ static struct ieee80211_mesh_route * mesh_rt_add_locked(struct ieee80211_mesh_state *ms, const uint8_t dest[IEEE80211_ADDR_LEN]) { struct ieee80211_mesh_route *rt; /* broadcast guard from ieee80211_mesh.c:201 (not relevant to the attack) */ #ifdef FIXED /* ===== BEGIN FIX (part 1): cap the per-vap route table size. ===== */ if (ms->ms_routes_count >= IEEE80211_MESH_RT_MAX_FIX) { return NULL; /* caller logs is_mesh_rtaddfailed++ and returns */ } /* ===== END FIX (part 1) ===== */ #endif #if defined(__DragonFly__) /* kernel: rt = kmalloc(ALIGN(sizeof(struct ieee80211_mesh_route)) + * ms->ms_ppath->mpp_privlen, M_80211_MESH_RT, * M_INTWAIT | M_ZERO); * modelled here as malloc(3) (size is identical; flags elided). */ rt = malloc(ALIGN(sizeof(struct ieee80211_mesh_route)) + ms->ms_ppath->mpp_privlen); #else rt = NULL; #endif if (rt != NULL) { rt->rt_vap = NULL; memcpy(rt->rt_dest, dest, IEEE80211_ADDR_LEN); rt->rt_priv = (void *)ALIGN((uintptr_t)(rt + 1)); /* lock/callout init elided (no-ops in userspace) */ rt->rt_updtime = ticks; /* create time */ TAILQ_INSERT_TAIL_mesh(ms, rt); } return rt; } /* ============================================================================ * VERBATIM vulnerable code from sys/netproto/802_11/wlan/ieee80211_mesh.c:266-303 * ieee80211_mesh_rt_update() โ sets rt->rt_lifetime from the attacker value. * Under -DFIXED we clamp new_lifetime to a sane max (kills the 49-day pin). * ============================================================================ */ static int ieee80211_mesh_rt_update(struct ieee80211_mesh_route *rt, int new_lifetime) { int timesince, now; uint32_t lifetime = 0; /* KASSERT(rt != NULL) elided */ now = ticks; /* dont clobber a proxy entry gated by us (ieee80211_mesh.c:278-281) */ if (rt->rt_flags & 0x04 /*PROXY*/ && rt->rt_nhops == 0) { return (int)rt->rt_lifetime; } #ifdef FIXED /* ===== BEGIN FIX (part 2): clamp attacker-supplied lifetime. ===== * NB: the parameter is `int` but the caller passes preq->preq_lifetime * (uint32). Treat as unsigned so 0xFFFFFFFF (-1 as int) is clamped to the * max rather than wedged to 0. */ { uint32_t u = (uint32_t)new_lifetime; if (u > (uint32_t)IEEE80211_MESH_RT_LIFETIME_MAX_MS) u = (uint32_t)IEEE80211_MESH_RT_LIFETIME_MAX_MS; new_lifetime = (int)u; } /* ===== END FIX (part 2) ===== */ #endif timesince = (int)ticks_to_msecs(now - rt->rt_updtime); rt->rt_updtime = now; if (timesince >= (int)rt->rt_lifetime) { if (new_lifetime != 0) { rt->rt_lifetime = new_lifetime; } else { rt->rt_flags &= ~IEEE80211_MESHRT_FLAGS_VALID; rt->rt_lifetime = 0; } } else { /* update what is left of lifetime */ rt->rt_lifetime = rt->rt_lifetime - timesince; rt->rt_lifetime = MESH_ROUTE_LIFETIME_MAX( new_lifetime, rt->rt_lifetime); } lifetime = rt->rt_lifetime; return (int)lifetime; } /* ============================================================================ * VERBATIM attacker-reachable block from hwmp_recv_preq() * sys/netproto/802_11/wlan/ieee80211_hwmp.c:1055-1097 * rtorig = ieee80211_mesh_rt_find(vap, preq->preq_origaddr); * if (rtorig == NULL) rtorig = ieee80211_mesh_rt_add(vap, preq->preq_origaddr); * ... * ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime); * preq->preq_lifetime = le32dec(iefrm_t) at ieee80211_hwmp.c:457 (uint32). * ============================================================================ */ static struct ieee80211_mesh_route * hwmp_recv_preq_origadd(struct ieee80211_mesh_state *ms, const uint8_t origaddr[IEEE80211_ADDR_LEN], uint32_t preq_lifetime) { struct ieee80211_mesh_route *rtorig; rtorig = mesh_rt_find_locked(ms, origaddr); /* ieee80211_hwmp.c:1055 */ if (rtorig == NULL) { /* ieee80211_hwmp.c:1056 */ rtorig = mesh_rt_add_locked(ms, origaddr); /* ieee80211_hwmp.c:1057 */ if (rtorig == NULL) { /* ieee80211_hwmp.c:1058 */ /* caller: vap->iv_stats.is_mesh_rtaddfailed++; return; */ return NULL; } } /* ieee80211_hwmp.c:1090-1097 (HWMP_SEQ_GT branch taken for fresh originator): * hrorig->hr_seq = preq->preq_origseq; * ... * ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime); <-- attacker value */ ieee80211_mesh_rt_update(rtorig, (int)preq_lifetime); rtorig->rt_flags = IEEE80211_MESHRT_FLAGS_VALID; /* ieee80211_hwmp.c:1102 */ return rtorig; } /* ---- build a distinct spoofed originator MAC for PREQ #i ---- */ static void make_origaddr(uint8_t out[IEEE80211_ADDR_LEN], uint64_t i) { /* unicast, locally-administered OUI so they all differ. Pack i * little-endian into bytes 1..5 (2^40 distinct values, far beyond any * plausible flood). */ out[0] = 0x02; out[1] = (uint8_t)((i >> 32) & 0xff); out[2] = (uint8_t)((i >> 24) & 0xff); out[3] = (uint8_t)((i >> 16) & 0xff); out[4] = (uint8_t)((i >> 8) & 0xff); out[5] = (uint8_t)((i >> 0) & 0xff); } int main(int argc, char **argv) { uint64_t n_attack = 8000; /* distinct spoofed PREQ originators * (> IEEE80211_MESH_RT_MAX so the * FIXED build exercises the cap) */ uint32_t attacker_lifetime = 0xFFFFFFFFu; /* max uint32 msec (~49 days) */ if (argc > 1) n_attack = strtoull(argv[1], NULL, 0); if (argc > 2) attacker_lifetime = (uint32_t)strtoul(argv[2], NULL, 0); struct ieee80211_mesh_proto_path_min ppath = { .mpp_privlen = sizeof(struct ieee80211_hwmp_route), }; struct ieee80211_mesh_state ms = { 0 }; ms.ms_ppath = &ppath; printf("=== DF-0350 harness: unbounded mesh route-table growth + " "attacker-controlled lifetime ===\n"); printf("build mode : %s\n", #ifdef FIXED "FIXED (cap+clamp applied)" #else "BUG PRESENT (no cap, no clamp)" #endif ); printf("preq->preq_lifetime (attacker) : 0x%08X msec (%.1f days)\n", attacker_lifetime, attacker_lifetime / 1000.0 / 86400.0); printf("attack: distinct originators : %llu PREQ frames\n", (unsigned long long)n_attack); printf("sizeof(struct ieee80211_mesh_route) + HWMP priv = %zu + %zu = %zu " "bytes/entry\n", ALIGN(sizeof(struct ieee80211_mesh_route)), ppath.mpp_privlen, ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen); printf("\n"); /* Simulate the attacker flood: one PREQ per distinct spoofed originator, * each carrying the maximal lifetime. This is EXACTLY the runtime data * flow: hwmp_recv_action_meshpath -> hwmp_recv_preq -> (find||add) -> * ieee80211_mesh_rt_update(rtorig, preq->preq_lifetime). */ uint64_t added = 0, rejected = 0; uint32_t first_rt_lifetime_seen = 0; int first_seen = 0; for (uint64_t i = 0; i < n_attack; i++) { uint8_t orig[IEEE80211_ADDR_LEN]; make_origaddr(orig, i); struct ieee80211_mesh_route *rt = hwmp_recv_preq_origadd(&ms, orig, attacker_lifetime); if (rt == NULL) { rejected++; } else { added++; if (!first_seen) { first_rt_lifetime_seen = rt->rt_lifetime; first_seen = 1; } } } printf("---- Result of the flood ----\n"); printf("routes successfully added : %llu\n", (unsigned long long)added); printf("routes rejected (NULL) : %llu\n", (unsigned long long)rejected); printf("final ms_routes table size : %d entries\n", ms.ms_routes_count); uint64_t bytes = (uint64_t)ms.ms_routes_count * (ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen); printf("kernel memory pinned by table : %llu bytes (%.1f MiB) [per-vap]\n", (unsigned long long)bytes, bytes / 1024.0 / 1024.0); printf("\n"); printf("---- Lifetime claim (attacker-controlled dwell) ----\n"); printf("first added route rt_lifetime : 0x%08X msec (%.1f days)\n", first_rt_lifetime_seen, first_rt_lifetime_seen / 1000.0 / 86400.0); printf(" (rt_lifetime == preq->preq_lifetime, UNVALIDATED: claim #2 %s)\n", first_rt_lifetime_seen == attacker_lifetime ? "CONFIRMED" : "refuted"); printf("\n"); /* extrapolate the DoS ceiling: the MAC space is 2^46 (with the 0x02 prefix), * so the attacker never runs out of distinct originators. At ~150 B/entry * the table can pin the ENTIRE kmem_map before ms_routes_count ever stops. */ printf("---- DoS ceiling extrapolation ----\n"); uint64_t kmem_map_bytes = 256ULL * 1024 * 1024 * 1024; /* ~256 GB worst case */ uint64_t entries_to_exhaust = kmem_map_bytes / (ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen); printf("distinct MACs available (0x02xx) : ~2^46 = %llu\n", 1ULL << 46); printf("entries to exhaust a 256 GB kmem : ~%llu (%llu MiB)\n", (unsigned long long)entries_to_exhaust, (unsigned long long)entries_to_exhaust * (ALIGN(sizeof(struct ieee80211_mesh_route)) + ppath.mpp_privlen) / (1024*1024)); printf("=> attacker can pin memory until kmem exhaustion / kernel panic.\n"); printf("\nVERDICT: "); #ifdef FIXED if (ms.ms_routes_count > (int)(long)IEEE80211_MESH_RT_MAX_FIX) { printf("CAP FAILED โ table exceeded the limit (BUG)\n"); return 1; } if (first_rt_lifetime_seen > IEEE80211_MESH_RT_LIFETIME_MAX_MS) { printf("CLAMP FAILED โ lifetime not bounded (BUG)\n"); return 1; } printf("FIXED โ table capped at %d entries (rejected %llu over-cap PREQs); " "lifetime clamped to %d msec (%.0f s, was %.1f days)\n", ms.ms_routes_count, (unsigned long long)rejected, IEEE80211_MESH_RT_LIFETIME_MAX_MS, IEEE80211_MESH_RT_LIFETIME_MAX_MS / 1000.0, attacker_lifetime / 1000.0 / 86400.0); return 0; #else if (rejected == 0 && added == (uint64_t)n_attack) { printf("UNBOUNDED GROWTH CONFIRMED โ all %llu PREQs added a new entry; " "no cap, no NULL, lifetime = attacker uint32.\n", (unsigned long long)added); return 0; } printf("growth was bounded unexpectedly (rejected=%llu) โ re-examine.\n", (unsigned long long)rejected); return 1; #endif } |