DF-0733 / 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 | /* * DF-0733 โ deterministic userspace transcription (PRIMARY proof). * * Bug: sys/netproto/802_11/wlan_acl/ieee80211_acl.c * acl_check() (:161-176) calls _find_acl() (:171/:173) WITHOUT taking * ACL_LOCK(as). _find_acl() (:136-148) does: * * LIST_FOREACH(acl, &as->as_hash[hash], acl_hash) { * if (IEEE80211_ADDR_EQ(acl->acl_macaddr, macaddr)) * return acl; * } * * LIST_FOREACH expands (sys/sys/queue.h:456/458) to: * * for (acl = LIST_FIRST(&as->as_hash[hash]); acl != NULL; * acl = LIST_NEXT(acl, acl_hash)) // = acl->acl_hash.le_next * * Meanwhile acl_remove() (:222-239) / acl_free_all() (:241-255) DO take * ACL_LOCK and call _acl_free() (:150-159): * * ACL_LOCK_ASSERT(as); * TAILQ_REMOVE(&as->as_list, acl, acl_list); * LIST_REMOVE(acl, acl_hash); // does NOT clear le_next * IEEE80211_FREE(acl, M_80211_ACL); * * RACE: the lockless foreach in acl_check parks its cursor on entry E * (after the ADDR_EQ compare, before the implicit LIST_NEXT read of * E->acl_hash.le_next). A concurrent acl_remove()/_acl_free() under the * lock runs LIST_REMOVE(E) then IEEE80211_FREE(E). The foreach then reads * E->acl_hash.le_next from FREED memory ==> Use-After-Free read. * * With INVARIANTS (default GENERIC) the freed slab is poisoned * (WEIRD_ADDR 0xdeadc0de / debug.use_malloc_pattern=1 => 0xFE fill), so * le_next resolves to a wild pointer and the next iteration dereferences * it -> panic. This transcription reproduces that exact chain: the freed * victim is poisoned (0xde), the foreach reads the poison le_next (UAF * read), then dereferences it -> SIGSEGV (the userspace analogue of the * kernel panic). harness_mod.ko is the real-kernel confirmation. * * Reachability: acl_check == iac_check, called from the UNAUTHENTICATED 802.11 * RX path in ieee80211_hostap.c: * :1801 PROBE_REQ (hostap_recv_mgmt, before any auth) * :1886 AUTH seq-1 (hostap_recv_mgmt, before any auth) * wh->i_addr2 is fully attacker-controlled, so a remote WiFi peer triggers the * lockless _find_acl at will while a local admin edits the ACL. The RX path * needs a wifi radio (absent on this KVM guest), so this transcription is the * PRIMARY proof; harness_mod.ko is the real-kernel object-level confirmation. * * Modes: * cc -O2 -pthread -o harness harness.c # BUGGY (faithful) * cc -O2 -pthread -DFIXED -o harness_fixed harness.c # FIXED (lock around _find_acl) * * BUGGY -> "UAF CONFIRMED" (poison le_next read + wild deref / clean report). * FIXED -> "NO UAF (serialized)". */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <setjmp.h> #include <signal.h> #include <pthread.h> /* ---------- faithful transcription of the kernel data structures ---------- */ #define IEEE80211_ADDR_LEN 6 #define ACL_HASHSIZE 32 #define LIST_ENTRY(type) struct { struct type *le_next; struct type **le_prev; } #define LIST_HEAD(name, type) struct name { struct type *lh_first; } struct acl { LIST_ENTRY(acl) acl_hash; uint8_t acl_macaddr[IEEE80211_ADDR_LEN]; }; struct aclstate { pthread_mutex_t as_lock; /* ACL_LOCK = lockmgr LK_EXCLUSIVE */ int as_policy; uint32_t as_nacls; LIST_HEAD(, acl) as_hash[ACL_HASHSIZE]; }; #define ACL_LOCK(as) pthread_mutex_lock(&(as)->as_lock) #define ACL_UNLOCK(as) pthread_mutex_unlock(&(as)->as_lock) #define ACL_HASH(addr) (((const uint8_t *)(addr))[IEEE80211_ADDR_LEN - 1] % ACL_HASHSIZE) static inline int addr_eq(const uint8_t *a, const uint8_t *b) { return memcmp(a, b, IEEE80211_ADDR_LEN) == 0; } /* queue primitives transcribed verbatim from sys/sys/queue.h */ static inline void list_insert_head(struct acl **headp, struct acl *elm) { if ((elm->acl_hash.le_next = *headp) != NULL) (*headp)->acl_hash.le_prev = &elm->acl_hash.le_next; elm->acl_hash.le_prev = headp; *headp = elm; } static inline void list_remove(struct acl *elm) { /* LIST_REMOVE */ if (elm->acl_hash.le_next != NULL) elm->acl_hash.le_next->acl_hash.le_prev = elm->acl_hash.le_prev; *elm->acl_hash.le_prev = elm->acl_hash.le_next; /* NOTE: faithful to the kernel โ le_next is NOT cleared. */ } /* ---------- poisoned allocator (models INVARIANTS free-poisoning) ---------- */ /* kern_slaballoc.c poisons freed slab with WEIRD_ADDR 0xdeadc0de; with * debug.use_malloc_pattern=1 freed allocations are filled with 0xFE. We * model the former: every byte of the freed object reads 0xde, so * le_next == 0xdededededededede (non-NULL, wild, unmapped). */ #define POISON_BYTE 0xde static void poisoned_free(struct acl *p) { memset(p, POISON_BYTE, sizeof(*p)); } /* ---------- the vulnerable function, transcribed verbatim ---------- */ /* The 'park' hook lets the test deterministically interleave the remover * exactly between the ADDR_EQ compare and the LIST_NEXT read of the victim. * It returns the cursor the foreach should advance with (so we can return the * poison pointer and let the loop deref it, or NULL to stop cleanly). */ static struct acl *(*park_hook)(struct aclstate *, struct acl *, int *uaf_read); static struct acl * _find_acl(struct aclstate *as, const uint8_t *macaddr) { struct acl *acl; int hash = ACL_HASH(macaddr); for (acl = as->as_hash[hash].lh_first; acl != NULL; ) { if (addr_eq(acl->acl_macaddr, macaddr)) return acl; if (park_hook) { int uaf = 0; struct acl *next = park_hook(as, acl, &uaf); if (uaf) { /* the hook already recorded the UAF read; advance to the * (wild) value it returned so the loop faithfully derefs it. */ acl = next; continue; } acl = next; continue; } acl = acl->acl_hash.le_next; } return NULL; } static int acl_check(struct aclstate *as, const uint8_t *mac /* wh->i_addr2 */) { switch (as->as_policy) { case 1: /* ACL_POLICY_ALLOW */ #ifdef FIXED { struct acl *r; ACL_LOCK(as); r = _find_acl(as, mac); ACL_UNLOCK(as); return r != NULL; } #else return _find_acl(as, mac) != NULL; /* BUG: no lock */ #endif case 2: /* ACL_POLICY_DENY */ #ifdef FIXED { struct acl *r; ACL_LOCK(as); r = _find_acl(as, mac); ACL_UNLOCK(as); return r == NULL; } #else return _find_acl(as, mac) == NULL; /* BUG: no lock */ #endif } return 0; } /* ---------- deterministic interleaving harness ---------- */ static struct aclstate g_as; static struct acl *victim; static volatile int remover_ready, remover_done; static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t cv_park = PTHREAD_COND_INITIALIZER; static pthread_cond_t cv_resume = PTHREAD_COND_INITIALIZER; /* captured UAF evidence */ static uintptr_t observed_le_next; static int observed_was_poison; static int uaf_read_happened; /* SIGSEGV/SIGBUS handler: the BUGGY foreach, after reading the poison le_next, * returns it as the next cursor; the loop then evaluates acl != NULL (true) * and dereferences acl->acl_macaddr at the unmapped poison address โ the * userspace analogue of the kernel panic in _find_acl. We catch it and * report UAF CONFIRMED. */ static sigjmp_buf uaf_jmp; static void uaf_sighandler(int sig) { (void)sig; siglongjmp(uaf_jmp, 1); } /* park hook: runs in checker thread with cursor == victim. Wakes remover, * waits for it to free victim, then reads victim->acl_hash.le_next (the UAF * read) and returns it as the next cursor. */ static struct acl * park_on_victim(struct aclstate *as, struct acl *cur, int *uaf_read) { (void)as; if (cur != victim) return cur->acl_hash.le_next; /* advance normally to next entry */ /* cursor is parked on the victim: deterministically interleave the remover * between this ADDR_EQ compare and the LIST_NEXT read of le_next. */ pthread_mutex_lock(&mtx); remover_ready = 1; pthread_cond_signal(&cv_park); while (!remover_done) pthread_cond_wait(&cv_resume, &mtx); pthread_mutex_unlock(&mtx); /* ---- THE UAF READ ---- : victim has been LIST_REMOVE'd + poisoned_free'd. * Reading victim->acl_hash.le_next touches freed, poisoned memory. */ uintptr_t v = (uintptr_t)victim->acl_hash.le_next; observed_le_next = v; uint8_t *p = (uint8_t *)&v; observed_was_poison = 1; for (int i = 0; i < (int)sizeof(uintptr_t); i++) if (p[i] != POISON_BYTE) { observed_was_poison = 0; break; } *uaf_read = 1; uaf_read_happened = 1; /* return the (wild) value so the loop derefs it โ faithful to the kernel * where the next iteration reads acl->acl_macaddr at the poison address. */ return (struct acl *)v; } struct remover_arg { uint8_t mac[IEEE80211_ADDR_LEN]; }; static void * remover_thread(void *v) { struct remover_arg *ra = v; struct acl *found; #ifdef FIXED /* FIXED: the checker holds ACL_LOCK for the whole _find_acl, so there is * no race window to interleave into. Just contend for the lock; we will * block until the checker releases it, then free victim โ exactly the * serialized behaviour the fix guarantees. */ remover_ready = 1; /* unblock main so it can run check */ #else /* BUGGY: wait for the precise interleaving point (cursor parked on victim) * before freeing, to make the race deterministic. */ pthread_mutex_lock(&mtx); while (!remover_ready) pthread_cond_wait(&cv_park, &mtx); pthread_mutex_unlock(&mtx); #endif /* acl_remove() transcribed: ACL_LOCK, _find_acl, _acl_free, ACL_UNLOCK */ ACL_LOCK(&g_as); { int h = ACL_HASH(ra->mac); found = NULL; for (struct acl *a = g_as.as_hash[h].lh_first; a != NULL; a = a->acl_hash.le_next) if (addr_eq(a->acl_macaddr, ra->mac)) { found = a; break; } } if (found != NULL) { /* _acl_free */ list_remove(found); poisoned_free(found); g_as.as_nacls--; } ACL_UNLOCK(&g_as); pthread_mutex_lock(&mtx); remover_done = 1; pthread_cond_signal(&cv_resume); pthread_mutex_unlock(&mtx); return NULL; } int main(void) { uint8_t mac_victim[6] = { 0x00,0x11,0x22,0x33,0x44,0x05 }; /* bucket 5 */ uint8_t mac_lookup[6] = { 0xff,0xff,0xff,0xff,0xff,0x05 }; /* bucket 5, no match */ struct acl *e1, *e2, *e3; int h = ACL_HASH(mac_victim); pthread_mutex_init(&g_as.as_lock, NULL); for (int i = 0; i < ACL_HASHSIZE; i++) g_as.as_hash[i].lh_first = NULL; g_as.as_policy = 1; /* ACL_POLICY_ALLOW */ g_as.as_nacls = 0; e1 = calloc(1, sizeof(*e1)); memcpy(e1->acl_macaddr,(uint8_t[]){0,0,0,0,0,5},6); e2 = calloc(1, sizeof(*e2)); memcpy(e2->acl_macaddr, mac_victim, 6); e3 = calloc(1, sizeof(*e3)); memcpy(e3->acl_macaddr,(uint8_t[]){0xaa,0,0,0,0,5},6); list_insert_head(&g_as.as_hash[h].lh_first, e1); list_insert_head(&g_as.as_hash[h].lh_first, e2); list_insert_head(&g_as.as_hash[h].lh_first, e3); victim = e2; g_as.as_nacls = 3; park_hook = park_on_victim; #ifdef FIXED park_hook = NULL; /* FIXED: lock serializes, no interleaving */ #endif printf("=== DF-0733 deterministic UAF transcription ===\n"); #ifdef FIXED printf("MODE: FIXED (acl_check takes ACL_LOCK around _find_acl)\n"); #else printf("MODE: BUGGY (acl_check calls _find_acl with NO lock โ the bug)\n"); #endif printf("bucket=%d victim=%p lookup matches nothing (forces full walk)\n\n", h, (void *)victim); struct remover_arg ra; memcpy(ra.mac, mac_victim, 6); pthread_t rt; remover_ready = remover_done = 0; pthread_create(&rt, NULL, remover_thread, &ra); /* install SIGSEGV/SIGBUS handler to catch the wild-pointer deref that * follows the UAF read in BUGGY mode (faithful panic analogue). */ struct sigaction sa, oldsa, oldbus; memset(&sa, 0, sizeof(sa)); sa.sa_handler = uaf_sighandler; sa.sa_flags = 0; sigemptyset(&sa.sa_mask); sigaction(SIGSEGV, &sa, &oldsa); sigaction(SIGBUS, &sa, &oldbus); int wild_deref = 0; int rc; if (sigsetjmp(uaf_jmp, 1) == 0) { rc = acl_check(&g_as, mac_lookup); } else { /* the foreach dereferenced the poison pointer -> SIGSEGV caught */ wild_deref = 1; rc = -1; } sigaction(SIGSEGV, &oldsa, NULL); sigaction(SIGBUS, &oldbus, NULL); pthread_join(rt, NULL); printf("acl_check rc=%d uaf_read_happened=%d wild_deref=%d\n", rc, uaf_read_happened, wild_deref); if (uaf_read_happened) printf("victim->acl_hash.le_next (read from FREED memory) = %p poisoned=%s\n", (void *)observed_le_next, observed_was_poison ? "YES (0xde fill)" : "no"); printf("\n"); #ifdef FIXED if (!uaf_read_happened) { printf("NO UAF (serialized): ACL_LOCK in acl_check made the remover\n"); printf("block until the foreach completed; victim was never freed\n"); printf("under the cursor. le_next read from LIVE memory.\n"); printf("RESULT: FIXED โ UAF NOT TRIGGERED\n"); return 0; } else { printf("UNEXPECTED: UAF read happened under FIXED โ FIX IS BROKEN\n"); return 1; } #else if (uaf_read_happened && observed_was_poison) { printf("UAF CONFIRMED: _find_acl's LIST_FOREACH read victim->acl_hash.le_next\n"); printf("from FREED memory (poison byte 0x%02x repeated). ", POISON_BYTE); if (wild_deref) printf("The subsequent deref of the wild pointer faulted โ the\n" "userspace analogue of the kernel panic in _find_acl/acl_check.\n"); printf("On a real kernel with INVARIANTS+use_malloc_pattern this wild\n"); printf("le_next => panic; on noinv it is a slab-groom candidate\n"); printf("(controlled le_next => arbitrary r/w primitive).\n"); printf("RESULT: UAF CONFIRMED\n"); return 0; } else { printf("UNEXPECTED: le_next was not poisoned โ race did not interleave\n"); printf("RESULT: UAF NOT OBSERVED\n"); return 1; } #endif } |