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
编译出来的bzImage
在arch
下面
~/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
最后一行设置的启动的用户uid
是1000
如果需要以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
结构,这个结构保存了该进程的权限信息,包括uid
、gid
等
如果修改了这一结构,就能够达到提权的目的
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
的系统中,利用一个空指针解引用的漏洞只需要三步
- 一个恶意程序将恶意代码映射到0地址处
- 触发一个内核的空指针漏洞
- 内核从0地址开始执行恶意代码
kallsyms
这其实不算是一个内核保护机制吧
/proc/kallsyms
存储着内核符号的地址信息
如果这个文件的读取权限没有严格保护起来,低权限的用户就可以轻松读到其中
prepare_kernel_cred
以及commit_creds
两个函数的地址
接下来要构建exploit就很简单了
未完待续
其他安全机制学到了、学会怎么绕了慢慢加
目前已经知道名字的有
- KALSR
- PXN
- PaX
- kGuard
- SMAP
- ......
如何把一个大象放进冰箱
作为安全研究者,关注的更多可能是漏洞利用和漏洞挖掘的方法
看到宾州州立的Yueqi Chen之前讲过的talk,将内核漏洞的利用过程抽象成了三步
觉得这种总体上的思路可能对于刚开始学习内核漏洞利用的新手会很有帮助
以下的图片都是来自于Yueqi Chen的演讲PPT
UAF
利用一个内核Use After Free的漏洞只需要
- free掉存在漏洞的vulnerable object;
- 利用堆喷射申请到之前vulnerable object的位置,并且改掉其中的一个函数指针为shellcode的地址;
- 利用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越界写的一个漏洞又该如何利用呢
- 申请存在越界写漏洞的vulnerable object;
- 申请一个包含函数指针的vitim object,设法让这个object能够紧挨着vulnerable object,并且vitim object中的函数指针要在vulnerable object越界写的距离范围内;
- 利用漏洞越界写修改掉victim object的函数指针,改成要执行的恶意代码的地址;
- dereference掉这个函数指针(即调用这个函数,触发这个漏洞);
总体的流程听上去很简单,但是实际操作起来难度很大
这个过程就像是把大象放到冰箱里面一样,也只需要开门、放进去、关门三步
之后的博客随着学习的深入尽量慢慢能够把这个过程的具体操作讲清楚