Kernel Pwn的学习之路(一)

寒假里面学习了一些Linux Kernel相关的漏洞利用和漏洞挖掘的东西

主要看了一些学术圈大佬的一些talk,接着也做了一些实验

准备写一下从零开始学习的过程

环境配置

配置环境主要需要注意的其实就是两点,一是编译内核;二是编译文件系统

编译文件系统可以使用busybox,或者可以用syzkaller里面的create-images.sh脚本

syzkaller的文件系统各种包比较多,并且命令行是bash,比busybox还是功能多很多,但是缺点是文件比较大;

编译kernel

下载源码

$ curl -O -L https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.4.98.tar.xz
$ x linux-5.4.98.tar.xz

在漏洞复现时,可以直接去git clone下来linux的master分支

之后需要切换到版本使用git checkout v4.10 --force这样的方式切换到对应的tag即可

安装依赖

apt install libelf-dev flex

编译配置

make menuconfig

在menuconfig中启用调试相关选项

Kernel hacking->Compile-time checks and compiler options->KGDB: kernel debugger

修改完成之后保存退出

手动编辑.config找到CONFIG_SYSTEM_TRUSTED_KEYS将其设置为""

编译为bzImage

make -j2 bzImage

运行完毕之后主要需要关注两个文件

  • bzImage
  • vmlinux

编译出来的bzImagearch下面

~/linux-5.4.98 $  find . -name bzImage
./arch/x86/boot/bzImage
./arch/x86_64/boot/bzImage

源码根目录下有vmlinux文件

编译busybox

首先下载源码

$ wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2
$ x busybox-1.32.1.tar.bz2

编译配置

make menuconfig

开启Settings->Build static binary)编译静态链接的文件

编译

make
make install

安装完成之后会生成一个_install目录,在这个目录下是类似linux根目录的结构

不过由于缺少几个目录,还需要再创建几个

mkdir -p  proc sys dev etc/init.d

在根目录创建init文件

#!/bin/sh
echo "INIT SCRIPT"
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
mount -t tmpfs none /tmp
echo -e "Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh

最后一行设置的启动的用户uid1000

如果需要以root用户运行,就设置这里为0

使用cpio打包镜像

find . | cpio -o --format=newc > ../rootfs.img

cpio打包的镜像可以用下面的指令解包

cpio -idmv < rootfs.img

最后是启动qemu的脚本

#!/bin/sh
qemu-system-x86_64 \
    -m 64M \
    -nographic \
    -kernel ./bzImage \
    -initrd  ./rootfs.img \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kalsr" \
    -smp cores=2,threads=1 \
    -cpu kvm64

不知道为什么,这里目前运行不起来,kernel里面一直重启,不知道是启动参数的问题还是怎样

检查之后发现这里rootfs.img有些问题,导致没办法启动起来

之前执行的是find . |cpio -o --format=newc > rootfs.img导致生成的文件可能把自己也包含进去了

最终运行起来的状态

内核基础

这一节就作为持续更新的内容吧,目前也是在一点一点的学习中

只写一下目前学到的内容

关键数据结构

task_struct结构体

这个结构体是用于标识进程的结构体,其中有很多信息,这个结构体定义在include/linux/sched.h中,整个结构体代码有五六百行

624  struct task_struct {                                                                                               
   1 #ifdef CONFIG_THREAD_INFO_IN_TASK
   2     /*
   3      * For reasons of header soup (see current_thread_info()), this
   4      * must be the first element of task_struct.
   5      */
   6     struct thread_info      thread_info;
   7 #endif
   8     /* -1 unrunnable, 0 runnable, >0 stopped: */
   9     volatile long           state;
  10 
  11     /*
  12      * This begins the randomizable portion of task_struct. Only
  13      * scheduling-critical items should be added above here.
          */   
//...
1287 }

在内核漏洞利用中,比较重要的是其中这几项

 872     const struct cred __rcu     *ptracer_cred;                                                                     
 873 
 874     /* Objective and real subjective task credentials (COW): */
 875     const struct cred __rcu     *real_cred;
 876 
 877     /* Effective (overridable) subjective task credentials (COW): */
 878     const struct cred __rcu     *cred;
 879 
 880 #ifdef CONFIG_KEYS
 881     /* Cached requested key. */
 882     struct key          *cached_requested_key;
 883 #endif
 884 
 885     /*
 886      * executable name, excluding path.
 887      *
 888      * - normally initialized setup_new_exec()
 889      * - access it with [gs]et_task_comm()
 890      * - lock it with task_lock()
 891      */
 892     char                comm[TASK_COMM_LEN];
 893 
 894     struct nameidata        *nameidata;
 895 
 896 #ifdef CONFIG_SYSVIPC
 897     struct sysv_sem         sysvsem;
 898     struct sysv_shm         sysvshm;
 899 #endif

其中*cred是一个指向cred结构体的指针

thread_info

这个结构是存储当前线程的metadata的结构体,与内核栈存储在一起
在源码的include/linux/sched.h中定义了内核栈空间

union thread_union {
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

这两者在内核栈空间的状态类似与下图

thread_info则定义在arch/x86/include/asm/thread_info.h

struct thread_info {
    struct task_struct  *task;
    struct exec_domain  *exec_domain; 
    __u32                flags;
    __u32                status;
    __u32                cpu;
    int                  preempt_count;
    mm_segment_t         addr_limit;
    struct restart_block restart_block; 
    void __user          *sysenter_return; 
#ifdef CONFIG_X86_32 
    unsigned long        previous_esp;
    __u8                 supervisor_stack[0]; 
#endif
    int                  uaccess_err; 
};

cred结构体

每一个进程中都有一个cred结构,这个结构保存了该进程的权限信息,包括uidgid

如果修改了这一结构,就能够达到提权的目的

   93 /*
   94  * The security context of a task
   95  *
   96  * The parts of the context break down into two categories:
   97  *
   98  *  (1) The objective context of a task.  These parts are used when some other
   99  *      task is attempting to affect this one.
  100  *
  101  *  (2) The subjective context.  These details are used when the task is acting
  102  *      upon another object, be that a file, a task, a key or whatever.
  103  *
  104  * Note that some members of this structure belong to both categories - the
  105  * LSM security pointer for instance.
  106  *
  107  * A task has two security pointers.  task->real_cred points to the objective
  108  * context that defines that task's actual details.  The objective part of this
  109  * context is used whenever that task is acted upon.
  110  *
  111  * task->cred points to the subjective context that defines the details of how
  112  * that task is going to act upon another object.  This may be overridden
  113  * temporarily to point to another security context, but normally points to the
  114  * same context as task->real_cred.
  115  */
  116 struct cred {
  117         atomic_t        usage;
  118 #ifdef CONFIG_DEBUG_CREDENTIALS
  119         atomic_t        subscribers;    /* number of processes subscribed */
  120         void            *put_addr;
  121         unsigned        magic;
  122 #define CRED_MAGIC      0x43736564
  123 #define CRED_MAGIC_DEAD 0x44656144
  124 #endif
  125         uid_t           uid;            /* real UID of the task */
  126         gid_t           gid;            /* real GID of the task */
  127         uid_t           suid;           /* saved UID of the task */
  128         gid_t           sgid;           /* saved GID of the task */
  129         uid_t           euid;           /* effective UID of the task */
  130         gid_t           egid;           /* effective GID of the task */
  131         uid_t           fsuid;          /* UID for VFS ops */
  132         gid_t           fsgid;          /* GID for VFS ops */
  133         unsigned        securebits;     /* SUID-less security management */
  134         kernel_cap_t    cap_inheritable; /* caps our children can inherit */
  135         kernel_cap_t    cap_permitted;  /* caps we're permitted */
  136         kernel_cap_t    cap_effective;  /* caps we can actually use */
  137         kernel_cap_t    cap_bset;       /* capability bounding set */
  138 #ifdef CONFIG_KEYS
  139         unsigned char   jit_keyring;    /* default keyring to attach requested
  140                                          * keys to */
  141         struct key      *thread_keyring; /* keyring private to this thread */
  142         struct key      *request_key_auth; /* assumed request_key authority */
  143         struct thread_group_cred *tgcred; /* thread-group shared credentials */
  144 #endif
  145 #ifdef CONFIG_SECURITY
  146         void            *security;      /* subjective LSM security */
  147 #endif
  148         struct user_struct *user;       /* real user ID subscription */
  149         struct user_namespace *user_ns; /* cached user->user_ns */
  150         struct group_info *group_info;  /* supplementary groups for euid/fsgid */
  151         struct rcu_head rcu;            /* RCU deletion hook */
  152 };

这里面存储着uid、gid、suid等等值

这类值是标识进程特权级的,也就是说只要修改了这些都为0(即root),就可以提取到root

一个常见的提权方法是获取控制权之后设法调用

commit_cred(prepare_kernel_cred(0))

就可以获取到root权限

安全保护机制

内核除了用户态的安全机制,还有一些独有的保护机制

SMEP

Supervisor Mode Execution Protection
这一机制从Intel IvyBridge引入

常规的一个获取到root权限的思路是这样

  • 用户空间代码将某个函数指针改写为自己提权的函数
  • 触发内核执行这一函数
  • 内核控制流从内核空间跳转到用户空间来执行这一部分代码

SMEP的机制就是让这两部分发生隔离,内核空间无法执行用户空间的代码

mmap_min_addr

这个值是一个内核的参数,它指定了内核中允许进程mmap的最低虚拟地址
如果这个值被设置为0,那么有可能导致Null Pointer Dereference的漏洞能够被利用

正常情况下由于内核中比较低的虚拟地址是保留不能够分配的,如果由于程序的bug出现一个空指针,是无法直接访问0地址的,因此会报错;而如果这一地址可以被分配映射为特定的代码,则可能会由于一个简单的空指针带来更大的安全问题(例如权限提升等)

https://wiki.debian.org/mmap_min_addr

在没有设置mmap_min_addr的系统中,利用一个空指针解引用的漏洞只需要三步

  1. 一个恶意程序将恶意代码映射到0地址处
  2. 触发一个内核的空指针漏洞
  3. 内核从0地址开始执行恶意代码

Pasted image 20220123102018

kallsyms

这其实不算是一个内核保护机制吧

/proc/kallsyms存储着内核符号的地址信息

如果这个文件的读取权限没有严格保护起来,低权限的用户就可以轻松读到其中

prepare_kernel_cred以及commit_creds两个函数的地址

接下来要构建exploit就很简单了

未完待续

其他安全机制学到了、学会怎么绕了慢慢加

目前已经知道名字的有

  • KALSR
  • PXN
  • PaX
  • kGuard
  • SMAP
  • ......

如何把一个大象放进冰箱

作为安全研究者,关注的更多可能是漏洞利用和漏洞挖掘的方法

看到宾州州立的Yueqi Chen之前讲过的talk,将内核漏洞的利用过程抽象成了三步

觉得这种总体上的思路可能对于刚开始学习内核漏洞利用的新手会很有帮助

以下的图片都是来自于Yueqi Chen的演讲PPT

UAF

利用一个内核Use After Free的漏洞只需要

  1. free掉存在漏洞的vulnerable object;
  2. 利用堆喷射申请到之前vulnerable object的位置,并且改掉其中的一个函数指针为shellcode的地址;
  3. 利用UAF触发这个被修改了的函数指针,就可以执行任意地址的代码;

相关的名词解释

内核的内存管理是由一个SLAB/SLUB管理器分配的,可以先理解为按照对象object来申请释放内存的堆管理器,vulnerable object是我们发现的存在Use After Free的一个object

堆喷射的思路是,一口气申请大量的对象,因为一下子会占用很大内存,就有比较大的概率申请到原本free的那块位置。

可能有的同学要问了,为什么要搞这么麻烦呢?不能像User Mode里面直接申请一个相同大小的吗?

kernel和user mode的进程间的差异就是,用户模式的进程(CTF程序之类的)都只有一个交互的出口,但是内核不止在处理我们的请求,后台可能还有很多其他的模块、驱动、设备、进程等等在利用内核申请释放内存,这就没办法保证申请释放的顺序,只申请一个object就不一定能够准确的覆盖到想要的那块区域了。

这里堆喷射大量申请的vitim object需要满足一个特点,这个object中的很多data需要由用户可以控制,这样我们将这些data全部改成目标shellcode的指针,一口气申请一大堆这样的vitim object,就有很大概率覆盖修改掉victim中的某个函数指针了。

OOB

Out Of Boundary越界写的一个漏洞又该如何利用呢

  1. 申请存在越界写漏洞的vulnerable object;
  2. 申请一个包含函数指针的vitim object,设法让这个object能够紧挨着vulnerable object,并且vitim object中的函数指针要在vulnerable object越界写的距离范围内;
  3. 利用漏洞越界写修改掉victim object的函数指针,改成要执行的恶意代码的地址;
  4. dereference掉这个函数指针(即调用这个函数,触发这个漏洞);

总体的流程听上去很简单,但是实际操作起来难度很大

这个过程就像是把大象放到冰箱里面一样,也只需要开门、放进去、关门三步

之后的博客随着学习的深入尽量慢慢能够把这个过程的具体操作讲清楚