DF-3460 / df3460_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 | /* * DF-3460 โ rssadapt_sysctl_interval() UAF across vap teardown * ============================================================= * * Target: sys/netproto/802_11/wlan/ieee80211_rssadapt.c:340-353 (handler) * sys/netproto/802_11/wlan/ieee80211_rssadapt.c:151-155 (rssadapt_deinit) * * Lifetime chain (all on this DragonFly tree): * * attach : iwm_llat... vap create -> ieee80211_ratectl_init() * (ieee80211_ratectl.c:119-135) -> rssadapt_init() * (ieee80211_rssadapt.c:128-149) kmalloc's vap->iv_rs AND * registers the world-readable sysctl oid * net.wlan.N.rssadapt_rate_interval (rssadapt_sysctlattach :356-362) * * teardown: iwm_vap_delete() (if_iwm.c:6486-6493) calls * ieee80211_ratectl_deinit(vap) -> rssadapt_deinit() -> * IEEE80211_FREE(vap->iv_rs, M_80211_RATECTL) [iv_rs NOT nulled] * ... and only LATER ieee80211_vap_detach() -> ... -> * ieee80211_sysctl_vdetach(vap) (ieee80211.c:827, the LAST detach) * removes the oid. * * During the whole window between the kfree and the sysctl-ctx teardown * (which spans ether_ifdetach, ieee80211_stop, task drains, node detach, * ...), the oid stays registered and world-readable, and its handler does * * struct ieee80211_rssadapt *rs = vap->iv_rs; // dangling * int msecs = ticks_to_msecs(rs->interval); // UAF READ * ... sysctl_handle_int(...) // 4 bytes -> user * rssadapt_setinterval(vap, msecs); // UAF WRITE (root) * * An unprivileged user polling `sysctl net.wlan.0.rssadapt_rate_interval` * while the interface is being destroyed reads 4 bytes out of a FREED * (and possibly reused) M_80211_RATECTL chunk; the write path (root) writes * 4 controlled bytes into the freed chunk. * * LATENT, same as DF-0730/DF-0731: no in-tree driver selects RSSADAPT * (default is AMRR) and the audit guest has no WiFi radio โ but the amrr * twin (ieee80211_amrr.c:390-403 amrr_sysctl_interval + SYSCTL_ADD_UINT * pointing directly into the freed chunk) is the DEFAULT algorithm and has * the identical deinit-order hole on any real WiFi system. * * ------------------------------------------------------------------------ * Harness: replicates the exact lifetime. malloc'd iv_rs, "kernel" free at * deinit (NOT nulled โ as master), immediate same-size reallocation with a * poison object (kmalloc reuses freed chunks), then the sysctl read handler * โ the value handed to the unprivileged reader is the POISON, i.e. bytes * from a different live object that reused the chunk. -DFIXED applies this * finding's fix.diff (deinit NULLs iv_rs, handler returns ENXIO). */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #define ENXIO_ 6 struct ieee80211_rssadapt { const void *vap; int interval; /* in ticks */ }; struct fake_vap { const char *name; struct ieee80211_rssadapt *iv_rs; }; /* poison object that reuses the freed chunk (any 16-byte kmalloc) */ struct poison { uint32_t marker0; uint32_t marker1; uint32_t marker2; uint32_t marker3; }; static int sysctl_out_val; /* what the unprivileged reader gets */ /* replica of rssadapt_sysctl_interval (read path), :341-353 */ static int rssadapt_sysctl_interval_read(struct fake_vap *vap) { struct ieee80211_rssadapt *rs = vap->iv_rs; int msecs, error; #ifdef FIXED if (rs == NULL) return (ENXIO_); /* fix: refuse before deref */ #endif msecs = rs->interval; /* ticks_to_msecs() in the kernel */ sysctl_out_val = msecs; error = 0; return error; } /* NOTE: built -O0 (see build.sh). At -O2 gcc treats the post-free * dereference as unreachable UB and CSEs the second load of rs->interval * with the pre-free one, hiding the chunk reuse. The kernel does no * such elision across the real sysctl dispatch. */ /* replica of rssadapt_setinterval write path (root-only) */ static int rssadapt_sysctl_interval_write(struct fake_vap *vap, int msecs) { struct ieee80211_rssadapt *rs = vap->iv_rs; #ifdef FIXED if (rs == NULL) return (ENXIO_); #endif /* rssadapt_setinterval: clamp + store */ if (msecs < 100) msecs = 100; rs->interval = msecs; /* UAF WRITE into freed chunk */ return 0; } int main(void) { struct fake_vap vap; struct poison *p; vap.name = "wlan0"; /* ---- vap attach: rssadapt_init ---- */ vap.iv_rs = malloc(sizeof(*vap.iv_rs)); vap.iv_rs->interval = 500; /* 500ms default */ printf("attach : iv_rs=%p interval=%d\n", (void *)vap.iv_rs, vap.iv_rs->interval); /* normal unprivileged read while alive */ rssadapt_sysctl_interval_read(&vap); printf("read (alive) : reader gets %d (ok)\n", sysctl_out_val); /* ---- vap destroy, step 1: iwm_vap_delete -> rssadapt_deinit ---- */ free(vap.iv_rs); /* NOTE: iv_rs NOT cleared (master) */ #ifdef FIXED vap.iv_rs = NULL; /* fix.diff: rssadapt_deinit NULLs it */ #endif printf("deinit : iv_rs freed, pointer now %p (%s)\n", (void *)vap.iv_rs, vap.iv_rs ? "dangling" : "cleared"); /* ---- the window: chunk gets reused by unrelated 16-byte alloc ---- */ p = malloc(sizeof(struct poison)); printf("window : chunk reused by poison obj at %p\n", (void *)p); if (p == NULL) { fprintf(stderr, "alloc failed\n"); return 2; } /* poison the WHOLE replacement object; struct ieee80211_rssadapt's * .interval lives at offset 8 (after the vap pointer) == marker2 */ memset(p, 0xBD, sizeof(*p)); p->marker2 = 0xDEADBEEF; /* ---- unprivileged sysctl read lands INSIDE the teardown window ---- */ if (rssadapt_sysctl_interval_read(&vap) == ENXIO_) { printf("read (window) : handler refused (fix active)\n"); printf("RESULT: FIXED โ no UAF read/write\n"); return 0; } printf("read (window) : unprivileged reader gets 0x%x from FREED " "chunk\n", sysctl_out_val); if (sysctl_out_val == 0xDEADBEEF) printf(" -> UAF READ confirmed: reader saw the replacement " "object's bytes (4-byte kernel-heap info leak)\n"); /* ---- root write path in the same window ---- */ rssadapt_sysctl_interval_write(&vap, 300); printf("write (window) : poison.marker2 (offset 8, = interval) " "now 0x%08x (was 0xDEADBEEF)\n", p->marker2); if (p->marker2 == 300) printf(" -> UAF WRITE confirmed: root sysctl write landed in " "the replacement object\n"); printf("RESULT: BUG REPRODUCED โ sysctl handler dereferences freed " "iv_rs across vap teardown\n"); return 0; } |