DF-0617 / uaf_ng_ether.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 | /* * DF-0617 โ Code-level harness proving the Use-After-Free in * ng_ether_rcv_upper() (sys/netgraph7/ether/ng_ether.c:640-666). * * The vulnerability: bridge_input_p() is called as a statement-expression * (return value discarded) at ng_ether.c:658. bridge_input() returns NULL * when it consumes/frees the mbuf (IFF_MONITOR, packet-for-bridge-MAC, BPDU, * bridge_forward, ether_reinput_oncpu, sender's-own-MAC โ see * sys/net/bridge/if_bridge.c:2646-2662, 2804-2805, 2936-2960, 2973-2989). * * Because the return value is discarded, the local variable `m` is never * updated and the subsequent `if (m == NULL) return (0);` at line 659 is * dead code. Execution falls through to ether_demux_oncpu(ifp, m) at line * 664, which dereferences the freed mbuf: * M_ASSERTPKTHDR(m) โ reads m->m_flags (if_ethersubr.c:992) * KASSERT(m->m_len >= ...) โ reads m->m_len (if_ethersubr.c:993) * eh = mtod(m, ...) โ dereferences m->m_data (if_ethersubr.c:996) * * The canonical correct pattern at sys/net/if_ethersubr.c:1252 is: * m = bridge_input_p(ifp, m); * if (m == NULL) return; * * This harness replicates the EXACT control flow of ng_ether_rcv_upper() * with a poisoned-freed-memory allocator: when bridge_input_p() frees the * mbuf, its backing memory is overwritten with 0xDE bytes. The UAF is then * observable: ether_demux_oncpu() reads the poison pattern from the freed * mbuf. Two modes are provided: * * --buggy : replicates the current kernel code (return value discarded) * --fixed : replicates the one-line fix (m = bridge_input_p(ifp, m)) * * Build: cc -O2 -o uaf_ng_ether uaf_ng_ether.c * Run: ./uaf_ng_ether --buggy (shows UAF: freed mbuf accessed) * ./uaf_ng_ether --fixed (shows fix: early return, no access) * ./uaf_ng_ether (runs both, summary) * * NOTE: This is a code-level proof. A live runtime trigger requires the * netgraph7 ng_ether + if_bridge topology on a bridged NIC, which causes * guest instability on this QEMU/vtnet0 guest (the ng_ether input-orphan * hooks interfere with normal bridge traffic processing, hanging the guest * before the upper-hook injection path can be exercised). The control flow * replicated here is an exact transcription of the kernel source. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <assert.h> /* ---- Simulated kernel structures (minimal fields for the UAF path) ---- */ struct ether_header { uint8_t ether_dhost[6]; uint8_t ether_shost[6]; uint16_t ether_type; } __attribute__((packed)); #define ETHER_HDR_LEN 14 #define ETHER_ADDR_LEN 6 #define M_PKTHDR 0x00000002 /* m_flags: packet header present */ #define POISON_BYTE 0xDE struct mbuf { /* The fields accessed by the vulnerable code path, in access order: */ uint32_t m_flags; /* M_ASSERTPKTHDR checks M_PKTHDR */ int m_len; /* KASSERT(m_len >= ETHER_HDR_LEN) */ void *m_data; /* mtod() dereferences this */ int m_pkthdr_len; /* ng_ether_rcv_upper line 646 */ struct { int len; /* m->m_pkthdr.len */ void *rcvif; /* m->m_pkthdr.rcvif (line 654) */ } m_pkthdr; /* padding so the allocation is a realistic size */ uint8_t pad[224]; }; struct ifnet { int if_flags; void *if_bridge; /* non-NULL => bridge member */ }; /* IFF_MONITOR on the bridge interface */ #define IFF_MONITOR 0x00040000 #define IFF_RUNNING 0x00000040 /* ---- Poisoned allocator: freed memory is overwritten with 0xDE ---- * * Uses a STATIC pool (not malloc/free) so the compiler cannot optimize * away the poison write as a "dead store before free" (which -O2 does * with real malloc/free due to the UB of accessing freed memory). * * In the real kernel, m_freem() returns the mbuf to the slab allocator; * the backing page may be reused immediately for a different object. * Our static pool simulates this: freed mbufs are poisoned with 0xDE * and flagged, and the poison persists so the UAF is observable. */ static int g_freed_count = 0; #define POOL_SIZE 4 static struct mbuf m_pool[POOL_SIZE]; static int m_pool_used[POOL_SIZE]; /* 0=free, 1=allocated */ static struct mbuf * mock_m_gethdr(int len) { for (int i = 0; i < POOL_SIZE; i++) { if (!m_pool_used[i]) { struct mbuf *m = &m_pool[i]; m_pool_used[i] = 1; memset(m, 0, sizeof(*m)); m->m_flags = M_PKTHDR; m->m_len = len; m->m_pkthdr.len = len; m->m_pkthdr.rcvif = NULL; /* m_data points to a simulated packet buffer right after the header */ m->m_data = (void *)((uint8_t *)m + sizeof(*m) - 64); /* Write a recognizable Ethernet header into the data area */ struct ether_header *eh = (struct ether_header *)m->m_data; memset(eh->ether_dhost, 0xAA, ETHER_ADDR_LEN); memset(eh->ether_shost, 0xBB, ETHER_ADDR_LEN); eh->ether_type = 0x0800; return m; } } fprintf(stderr, "mbuf pool exhausted\n"); abort(); return NULL; } static void mock_m_freem(struct mbuf *m) { /* Simulate m_freem(): poison the mbuf's backing memory with 0xDE. * In the real kernel, the slab allocator would record the free and * the page could be reclaimed for a different object type. The poison * pattern makes the subsequent UAF access observable. */ if (m) { /* volatile sink prevents the compiler from eliding the write */ volatile unsigned char *p = (volatile unsigned char *)m; size_t n = sizeof(*m); while (n--) *p++ = POISON_BYTE; g_freed_count++; } } /* ---- Simulated bridge_input() โ the IFF_MONITOR NULL-return path ---- * * Transcribed from sys/net/bridge/if_bridge.c:2615-2662: * * static struct mbuf * * bridge_input(struct ifnet *ifp, struct mbuf *m) * { * struct bridge_softc *sc = ifp->if_bridge; * ... * if (sc == NULL) * return m; // line 2631-2632 * ... * if ((bifp->if_flags & IFF_RUNNING) == 0) * goto out; // line 2637-2638 -> returns m * ... * if (bifp->if_flags & IFF_MONITOR) { // line 2646 * ... * m_freem(m); // line 2660 * m = NULL; // line 2661 * goto out; // line 2662 -> returns NULL * } * ... * } * * When IFF_MONITOR is set on the bridge, bridge_input() ALWAYS frees the * mbuf and returns NULL, regardless of the packet content. */ static struct mbuf * mock_bridge_input(struct ifnet *ifp, struct mbuf *m) { /* ifp->if_bridge is the bridge softc; non-NULL means we're a member */ if (ifp->if_bridge == NULL) return m; /* not a bridge member โ return untouched */ /* Simulate bifp (the bridge interface) having IFF_MONITOR set */ int bifp_flags = IFF_RUNNING | IFF_MONITOR; if ((bifp_flags & IFF_RUNNING) == 0) return m; /* bridge not running โ return untouched */ if (bifp_flags & IFF_MONITOR) { /* IFF_MONITOR path (if_bridge.c:2660-2662): * m_freem(m); * m = NULL; * goto out; // return NULL */ mock_m_freem(m); return NULL; /* mbuf consumed/freed, return NULL */ } return m; /* not consumed โ return original mbuf */ } /* Function pointer matching the kernel's bridge_input_p */ static struct mbuf *(*bridge_input_p)(struct ifnet *, struct mbuf *) = mock_bridge_input; /* ---- Simulated ether_demux_oncpu() โ the UAF sink ---- * * Transcribed from sys/net/if_ethersubr.c:985-996: * * void ether_demux_oncpu(struct ifnet *ifp, struct mbuf *m) * { * M_ASSERTPKTHDR(m); // line 992 * KASSERT(m->m_len >= ETHER_HDR_LEN, ...); // line 993 * eh = mtod(m, struct ether_header *); // line 996 * ... * } */ static int g_uaf_detected = 0; static void mock_ether_demux_oncpu(struct ifnet *ifp, struct mbuf *m) { /* M_ASSERTPKTHDR(m) โ reads m->m_flags (if_ethersubr.c:992) */ uint32_t flags_read = m->m_flags; /* KASSERT(m->m_len >= ETHER_HDR_LEN) โ reads m->m_len (if_ethersubr.c:993) */ int len_read = m->m_len; /* eh = mtod(m, struct ether_header *) โ dereferences m->m_data (line 996) */ struct ether_header *eh = (struct ether_header *)m->m_data; /* Detect UAF: if the mbuf was freed and poisoned, ALL fields are 0xDE. * m_flags would be 0xDEDEDEDE and m_len would be (int)0xDEDEDEDE. */ int poisoned = (flags_read == 0xDEDEDEDEU); printf(" ether_demux_oncpu: m->m_flags=0x%08x m->m_len=%d m->m_data=%p\n", flags_read, len_read, m->m_data); if (poisoned) { printf(" *** UAF DETECTED: mbuf was freed (all fields poisoned to 0x%02x) " "but still dereferenced! ***\n", POISON_BYTE); g_uaf_detected++; } else if (flags_read & M_PKTHDR) { printf(" ether_demux_oncpu: valid mbuf (m_flags has M_PKTHDR), " "ether_type=0x%04x โ normal processing\n", eh ? eh->ether_type : 0); } else { printf(" ether_demux_oncpu: mbuf in unexpected state (m_flags=0x%08x)\n", flags_read); } (void)ifp; } /* ---- ng_ether_rcv_upper โ EXACT transcription of the kernel code ---- * * sys/netgraph7/ether/ng_ether.c:639-666 * * static int * ng_ether_rcv_upper(node_p node, struct mbuf *m) * { * const priv_p priv = NG_NODE_PRIVATE(node); * struct ifnet *ifp = priv->ifp; * * // Check length and pull off header * if (m->m_pkthdr.len < sizeof(struct ether_header)) { // line 646 * NG_FREE_M(m); * return (EINVAL); * } * ... * m->m_pkthdr.rcvif = ifp; // line 654 * * // Pass the packet to the bridge, it may come back to us * if (ifp->if_bridge) { // line 657 * bridge_input_p(ifp, m); <-- BUG: return value discarded (line 658) * if (m == NULL) <-- DEAD CODE (line 659) * return (0); * } * * // Route packet back in * ether_demux_oncpu(ifp, m); <-- UAF SINK (line 664) * return (0); * } */ static int ng_ether_rcv_upper_BUGGY(struct ifnet *ifp, struct mbuf *m) { /* line 646: length check */ if (m->m_pkthdr.len < ETHER_HDR_LEN) { mock_m_freem(m); return -1; /* EINVAL */ } /* line 650-652: m_pullup (omitted โ m_len is already >= ETHER_HDR_LEN) */ /* line 654: m->m_pkthdr.rcvif = ifp */ m->m_pkthdr.rcvif = ifp; /* line 657-661: THE BUG */ if (ifp->if_bridge) { /* BUGGY: bridge_input_p(ifp, m); โ return value DISCARDED */ bridge_input_p(ifp, m); /* line 658: m NOT updated */ if (m == NULL) /* line 659: DEAD โ m is never NULL */ return 0; } /* line 664: UAF SINK โ m was freed inside bridge_input_p but the local * pointer still points at the freed/poisoned memory */ printf(" [BUGGY] Falling through to ether_demux_oncpu with freed mbuf %p\n", m); mock_ether_demux_oncpu(ifp, m); return 0; } static int ng_ether_rcv_upper_FIXED(struct ifnet *ifp, struct mbuf *m) { /* line 646: length check */ if (m->m_pkthdr.len < ETHER_HDR_LEN) { mock_m_freem(m); return -1; /* EINVAL */ } /* line 654: m->m_pkthdr.rcvif = ifp */ m->m_pkthdr.rcvif = ifp; /* line 657-661: THE FIX โ capture the return value */ if (ifp->if_bridge) { /* FIXED: m = bridge_input_p(ifp, m); โ return value CAPTURED */ m = bridge_input_p(ifp, m); /* line 658: m IS updated */ if (m == NULL) /* line 659: NOW FIRES */ return 0; /* early return โ no UAF */ } /* line 664: only reached if bridge did NOT consume the mbuf */ printf(" [FIXED] Falling through to ether_demux_oncpu with valid mbuf %p\n", m); mock_ether_demux_oncpu(ifp, m); return 0; } /* ---- Main: run both modes and report ---- */ int main(int argc, char **argv) { int run_buggy = 1, run_fixed = 1; if (argc > 1) { if (strcmp(argv[1], "--buggy") == 0) { run_fixed = 0; } else if (strcmp(argv[1], "--fixed") == 0) { run_buggy = 0; } else { fprintf(stderr, "Usage: %s [--buggy|--fixed]\n", argv[0]); return 2; } } /* Topology: ifp is a bridge member (if_bridge != NULL), bridge has * IFF_MONITOR set โ the most common deterministic NULL-return path. */ struct ifnet ifp_obj; memset(&ifp_obj, 0, sizeof(ifp_obj)); ifp_obj.if_bridge = (void *)0xCAFE0000; /* non-NULL: we are a bridge member */ printf("=== DF-0617: ng_ether_rcv_upper bridge_input UAF harness ===\n"); printf("Topology: ifp->if_bridge=%p (bridge member), bridge IFF_MONITOR set\n", ifp_obj.if_bridge); printf("bridge_input() IFF_MONITOR path: m_freem(m) + return NULL\n\n"); int buggy_uaf = 0, fixed_uaf = 0; if (run_buggy) { printf("--- BUGGY mode (current kernel: ng_ether.c:658 discards return value) ---\n"); g_freed_count = 0; g_uaf_detected = 0; struct mbuf *m = mock_m_gethdr(60); printf(" Allocated mbuf %p (m_flags=0x%08x, m_len=%d)\n", m, m->m_flags, m->m_len); ng_ether_rcv_upper_BUGGY(&ifp_obj, m); printf(" Result: freed_count=%d, uaf_detected=%d\n\n", g_freed_count, g_uaf_detected); buggy_uaf = g_uaf_detected; } if (run_fixed) { printf("--- FIXED mode (m = bridge_input_p(ifp, m); โ return value captured) ---\n"); g_freed_count = 0; g_uaf_detected = 0; struct mbuf *m = mock_m_gethdr(60); printf(" Allocated mbuf %p (m_flags=0x%08x, m_len=%d)\n", m, m->m_flags, m->m_len); ng_ether_rcv_upper_FIXED(&ifp_obj, m); printf(" Result: freed_count=%d, uaf_detected=%d\n\n", g_freed_count, g_uaf_detected); fixed_uaf = g_uaf_detected; } /* ---- Summary ---- */ printf("=== SUMMARY ===\n"); if (run_buggy) { printf("BUGGY: %s โ %s\n", buggy_uaf ? "UAF CONFIRMED" : "no UAF", buggy_uaf ? "freed mbuf dereferenced in ether_demux_oncpu" : "(unexpected)"); } if (run_fixed) { printf("FIXED: %s โ %s\n", fixed_uaf ? "UAF STILL PRESENT" : "UAF ELIMINATED", fixed_uaf ? "freed mbuf still dereferenced" : "early return before ether_demux_oncpu"); } if (run_buggy && run_fixed) { printf("\nVerdict: %s\n", (buggy_uaf && !fixed_uaf) ? "REPRODUCED โ the one-line fix (m = bridge_input_p(...)) " "eliminates the UAF" : (buggy_uaf && fixed_uaf) ? "FIX FAILED โ UAF present in both modes" : "UNEXPECTED โ investigate"); } return (run_buggy && buggy_uaf) ? 0 : 1; } |