DF-0285 / layout_proof.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 | /* * layout_proof.c - DF-0285 structural / overflow-reach proof * * This is a *userspace layout replica* of the relevant slice of * `struct ieee80211_node` (the "11s state" block from * sys/netproto/802_11/ieee80211_node.h:199-208) together with an exact * reproduction of `struct callout` (sys/sys/callout.h:77-82) and the * separately-allocated `struct _callout` (sys/sys/callout.h:54-75) it points at. * * The real header cannot be included verbatim in userspace (it depends on a * large forest of kernel-only macros / locks), so we replicate ONLY the field * types and order, which is what governs the C ABI layout. All types are * stdint / pointer types with identical size/alignment on x86_64 to the kernel * originals. We assert this at runtime with sizeof/alignof checks. * * Purpose: prove, with offsetof() arithmetic, that * * memcpy(ni->ni_meshid, ie+2, ie[1]) [ieee80211_mesh.c:3460] * * with an attacker-controlled ie[1] in (32 .. 255] overflows the fixed 32-byte * ni_meshid array into the subsequent fields, AND in particular into * ni_mltimer (a struct callout whose first member is a pointer, `toc`). * * IMPORTANT CORRECTION TO THE FINDING CLAIM: * The finding says ni_mltimer "is a callout containing a function pointer". * That is imprecise. `struct callout` (the field ni_mltimer) does NOT itself * contain a function pointer; it contains a POINTER `toc` to a * separately-allocated `struct _callout`, and the function pointers * (`rfunc`/`qfunc`) live inside that `_callout`. So the overflow corrupts the * `toc` pointer, giving an indirect / type-confused function-pointer-control * primitive (controlled pointer deref -> attacker-chosen qfunc), not a * direct function-pointer overwrite. The program below prints the exact byte * index at which each field (and ni_mltimer.toc specifically) is clobbered. * * Build: see build.sh * Run: see run.sh (prints the layout table + overflow-reach analysis) */ #include <stdint.h> #include <stddef.h> #include <stdio.h> #include <string.h> #define IEEE80211_MESHID_LEN 32 /* sys/netproto/802_11/ieee80211.h:200 */ /* ---- exact replica of sys/sys/callout.h:54-75 ------------------------------ */ struct _callout { /* spinlock, exis, tailq_entry are kernel-internal; their sizes are not * relevant to THIS proof because _callout is the *pointed-to* object, not * part of the contiguous ieee80211_node overflow region. We only need its * members that matter for the function-pointer-control claim. */ void *spin_skip[3]; /* spinlock + exis + TAILQ_ENTRY (placeholder) */ void *verifier; uint32_t flags; uint32_t lineno; void *lk; const char *ident; void *rsc; void *rarg; void (*rfunc)(void *); /* <--- function pointer #1 */ int rtick; uint32_t unused01; void *qsc; void *qarg; void (*qfunc)(void *); /* <--- function pointer #2 (the one fired) */ int qtick; int waiters; }; /* ---- exact replica of sys/sys/callout.h:77-82 ------------------------------ */ struct callout { struct _callout *toc; /* opaque internal pointer <-- FIRST FIELD */ struct lock_stub *lk; /* callout_init() copy data */ uint32_t flags; /* callout_init() copy data */ uint32_t unused01; }; struct lock_stub; /* opaque ptr type, size = 8 */ /* ---- exact replica of the "11s state" block: ieee80211_node.h:199-208 ------ */ enum ieee80211_mesh_mlstate { ML_DUMMY = 4 }; /* plain enum -> int (4 bytes) */ struct node_11s_block { uint8_t ni_meshidlen; /* :199 */ uint8_t ni_meshid[IEEE80211_MESHID_LEN]; /* :200 */ enum ieee80211_mesh_mlstate ni_mlstate; /* :201 */ uint16_t ni_mllid; /* :202 */ uint16_t ni_mlpid; /* :203 */ struct callout ni_mltimer; /* :204 */ uint8_t ni_mlrcnt; /* :205 */ uint8_t ni_mltval; /* :206 */ struct callout ni_mlhtimer; /* :207 */ uint8_t ni_mlhcnt; /* :208 */ }; static void check(const char *what, int ok) { printf(" %-42s %s\n", what, ok ? "OK" : "*** MISMATCH ***"); } int main(void) { struct node_11s_block n; printf("== ABI sanity (must all be OK) ==\n"); check("sizeof(void*) == 8", sizeof(void*) == 8); check("sizeof(struct callout) == 24", sizeof(struct callout) == 24); check("sizeof(enum) == 4", sizeof(enum ieee80211_mesh_mlstate) == 4); check("IEEE80211_MESHID_LEN == 32", IEEE80211_MESHID_LEN == 32); printf("\n== Field layout (offsets relative to start of ni_meshid) ==\n"); /* base address of ni_meshid in this replica object */ char *base = (char *)&n.ni_meshid; #define REL(f) ((long)((char *)&(n.f) - base)) printf(" ni_meshid[0] rel %+4ld (memcpy dst, attacker data starts here)\n", REL(ni_meshid[0])); printf(" ni_meshid[31] rel %+4ld (last legal byte)\n", REL(ni_meshid[31])); printf(" ni_mlstate rel %+4ld enum (peering FSM state)\n", REL(ni_mlstate)); printf(" ni_mllid rel %+4ld uint16 link-local id\n", REL(ni_mllid)); printf(" ni_mlpid rel %+4ld uint16 link peer id\n", REL(ni_mlpid)); printf(" ni_mltimer rel %+4ld struct callout\n", REL(ni_mltimer)); printf(" ni_mltimer.toc rel %+4ld *** POINTER to _callout (qfunc) ***\n",REL(ni_mltimer.toc)); printf(" ni_mltimer.lk rel %+4ld pointer\n", REL(ni_mltimer.lk)); printf(" ni_mltimer.flags rel %+4ld\n", REL(ni_mltimer.flags)); printf(" ni_mlrcnt rel %+4ld\n", REL(ni_mlrcnt)); printf(" ni_mltval rel %+4ld\n", REL(ni_mltval)); printf(" ni_mlhtimer rel %+4ld struct callout (2nd callout clobbered)\n", REL(ni_mlhtimer)); printf(" ni_mlhtimer.toc rel %+4ld *** 2nd POINTER clobbered ***\n", REL(ni_mlhtimer.toc)); printf(" ni_mlhcnt rel %+4ld\n", REL(ni_mlhcnt)); /* end of the clobberable region we model (next field would be 11n state) */ long region_end = REL(ni_mlhcnt) + 1; printf("\n== Overflow-reach analysis for ie[1]=N (attacker length byte) ==\n"); printf(" memcpy copies N bytes starting at ni_meshid[0] (rel 0).\n"); printf(" Bytes at rel >= 32 are PAST the legal ni_meshid[] array -> heap OOB.\n"); printf("\n %-10s %-12s %-12s %-12s\n", "ie[1]", "OOB_bytes", "reaches_toc?", "reaches_2nd_toc?"); int sizes[] = { 33, 48, 64, 128, 200, 255 }; for (size_t i = 0; i < sizeof(sizes)/sizeof(sizes[0]); i++) { int N = sizes[i]; int oob = N - IEEE80211_MESHID_LEN; /* bytes past ni_meshid */ int last_rel = N - 1; /* last byte written, rel */ int reach_toc = (last_rel >= REL(ni_mltimer.toc)); int reach_toc2 = (last_rel >= REL(ni_mlhtimer.toc)); printf(" %-10d %-12d %-12s %-12s\n", N, oob, reach_toc ? "YES" : "no", reach_toc2 ? "YES" : "no"); } printf("\n== Worst case ie[1]=255 ==\n"); { int N = 255; int oob = N - IEEE80211_MESHID_LEN; /* 223 */ int last_rel = N - 1; /* 254 */ printf(" Overflow past ni_meshid: %d bytes\n", oob); printf(" Last byte written at rel %d (region we model ends at rel %ld)\n", last_rel, region_end); printf(" -> clobbers ni_mlstate, ni_mllid, ni_mlpid, ni_mltimer (incl .toc ptr),\n"); printf(" ni_mlrcnt, ni_mltval, ni_mlhtimer (incl .toc ptr), ni_mlhcnt, AND\n"); printf(" continues %ld bytes past ni_mlhcnt into the 11n HT-state fields.\n", (long)(last_rel - region_end + 1)); } printf("\n== Verdict ==\n"); printf(" The memcpy at ieee80211_mesh.c:3460 with attacker ie[1] in (32..255]\n"); printf(" performs an up-to-223-byte heap OOB write into ni_mlstate/ni_mllid/\n"); printf(" ni_mlpid/ni_mltimer(+.toc ptr)/ni_mlrcnt/ni_mltval/ni_mlhtimer(+.toc ptr)/\n"); printf(" ni_mlhcnt and on into HT state. Two struct callout `toc` pointers are\n"); printf(" attacker-controlled -> indirect function-pointer-control primitive\n"); printf(" (forge toc -> forged _callout -> chosen qfunc). Real bug, real RCE\n"); printf(" surface; NOT directly a function-pointer field overwrite (correction\n"); printf(" to the finding's wording).\n"); return 0; } |