Kernel PWN从入门到入土-Kernel UAF

一些前置知识请点这里

这里以CISCN 2017 babydriver为例

qemu

可以看到没有给出.ko驱动文件,我们自己来提取一下。

railgun@Kernel:~/ciscn_babydriver/core$ mv rootfs.cpio rootfs.cpio.gz
railgun@Kernel:~/ciscn_babydriver/core$ ls
rootfs.cpio.gz
railgun@Kernel:~/ciscn_babydriver/core$ gunzip ./rootfs.cpio.gz
railgun@Kernel:~/ciscn_babydriver/core$ sudo cpio -idmv < rootfs.cpio
.
etc
etc/init.d
etc/passwd
etc/group
.
.
.
tmp
linuxrc
home
home/ctf
5556 blocks
railgun@Kernel:~/ciscn_babydriver/core$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr

前置知识已经说过了,一般ctf不会让选手直接pwn掉kernel,一般是可加载核心模块(LKM)也可称为内核模块,这个东西是个elf,可以IDA分析的。

接下来尝试启动一下qemu,使用给出的boot.sh,后面调试的时候可以适当修改

###boot.sh###

#!/bin/bash

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

这是上一篇x86下运行的结果,Google了一番也是没能解决,问了一下其他师傅,他们都是在x86_64系统下跑的,那我又重新搭了一个环境。

#Ubuntu 18.04 x64 
#qemu 
#Kernel 4.4.72
#busybox 1.19.4

接下来起qemu,用的给出的boot.sh、bzImage、rootfs.cpio:

init:

前面已经说过的babydriver.ko,就是我们需要分析的内核模块。

ko分析

首先Shift+F9看一下结构体:

双击babydevice_t这个结构体:

然后分析一下主要函数:

babyioctl()

首先定义的0x1001这个command,具体功能是,kfree掉device_buf,然后重新kmalloc一个我们指定大小的空间,地址存储在device_buf上,最后设置device_buf_len为我们指定的size。

babyopen()

申请一块0x40大小的空间,地址存储在device_buf上,并设置device_buf_len为0x40。

babyrelease()

释放device_buf上存储的地址的空间。

babywrite()

若device_buf上存有地址并且size小于device_buf_len,则把buffer上的数据copy到device_buf存储的地址中。

babyread()

与上面babywrite()大体相同,只是将device_buf上地址存储的数据copy到buffer中。

还有babydriver_init()和babydriver_exit()完成了/dev/babydev 设备的初始化和清理。

思路:存在条件竞争引起的UAF,如果我们同时打开两个设备,因为babydev_struct是全局的,那么第二次会覆盖第一次申请的空间,因此释放一个可以用另一个来修改内存。

cred提权

接下来考虑如何提权,之前的前置知识说过了cred这个结构体,其中 4.4.72 的 cred 结构体的定义如下:

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 */
};

其中比较重要的uid、gid,只要将他们改为0,即可提权到root。那么我们的具体利用过程如下:

  1. 打开两次设备。
  2. 通过babyioctl()修改device_buf存储的大小改为cred的大小。
  3. 释放其中一个并fork 一个新进程,新进程的 cred 的空间就会和之前释放的空间重叠。
  4. 通过另一个设备的文件描述符写free掉的空间,修改uid、gid。

需要确定 cred 结构体的大小,有了源码,大小就很好确定了。计算一下是 0xa8(注意使用相同内核版本的源码),可以写个简单的Module来计算大小。

接下来编写exp:

railgun@Kernel:~/ciscn_babydriver$ gcc getroot.c -o exploit -static

注意:之前说过kernel没有libc,因此要静态编译。

railgun@Kernel:~/ciscn_babydriver$ sudo cp exploit ./core/tmp/
railgun@Kernel:~/ciscn_babydriver$ cd core/
railgun@Kernel:~/ciscn_babydriver/core$ find . | cpio -o --format=newc > rootfs.cpio

重新生成文件系统(也可以写个脚本比较方便),然后起qemu:

如上,已经提权成功。

当然这题也可以ROP来做,下篇来说,下面来调试一下整个过程。

exp调试

首先用extract-vmlinux提取出带符号的源码

railgun@Kernel:~$ sudo ln extract-vmlinux.sh /usr/bin/extract-vmlinux

railgun@Kernel:~/ciscn_babydriver$ extract-vmlinux bzImage > vmlinux

然后启动gdb,之前环境搭建没安装pwndbg等插件,请自行安装,pip报超时就改一下源

gdb vmlinux -q

pwndbg> add-symbol-file ./core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
add symbol table from file "./core/lib/modules/4.4.72/babydriver.ko" at
.text_addr = 0xffffffffc0000000
Reading symbols from ./core/lib/modules/4.4.72/babydriver.ko…done.

导入符号表,两个参数分别是.ko路径以及.text段地址。

/tmp # lsmod
babydriver 16384 4 - Live 0xffffffffc0000000 (OE)

/tmp # find / -name *.ko
/lib/modules/4.4.72/babydriver.ko

然后修改boot.sh,加上- gdb tcp::1234,然后起qemu并且gdb attach

pwndbg> target remote 127.0.0.1:1234

三个关键函数下断点:

然后运行exploit:

可以看到断点下载了babyopen处,单步跟进:

走到这里刚好是babyopen中对device_buf以及device_buf_len赋值的地方。

pwndbg> p $rip+0x2473                    <---device_buf
$1 = (void ()()) 0xffffffffc00024c9 
pwndbg> p $rip+0x2470                    <---device_buf_len
$2 = (void ()()) 0xffffffffc00024c6

当然还没有执行mov指令,device_buf必然为空:

执行完mov指令后:

有点错位,调整一下如下,分别是device_buf以及device_buf_len:

然后继续跑,到第二次babyopen断下,此时device_buf的内容:

可以看到device_buf已经被写成了我们新打开的device,接着走:

断在了babyioctl,同样赋值完后看一下device_buf:

这里可以看到,device_buf_len已经是我们的0xa8。

接着走,断在了babywrite,在此之前已经将fd1释放掉并且fork完成:

此时的device_buf如上,应该是子进程的cred部分,运行至write结束:

可以看到前28个字节被改成0,即uid、gid改成了0,提权root成功。

Leave a Reply

Your email address will not be published. Required fields are marked *

7 − six =