DirtyCred漏洞分析

Last updated on 4 months ago

DirtyCred漏洞分析

DirtyCred is a kernel exploitation concept that swaps unprivileged kernel credentials with privileged ones to escalate privilege. Instead of overwriting any critical data fields on kernel heap, DirtyCred abuses the heap memory reuse mechanism to get privileged. Although the concept is simple, it is effective. See the Blackhat presentation or CCS paper for more details.

基础知识

DirtyCred利用堆破坏内核漏洞交换进程或文件的非特权和特权凭据,从而达到越权执行或写入。该方法可以绕过KASLR、CFI、SMEP/SMAP和KPTI 等内核保护和漏洞缓解措施。

DirtyCred需要将已有内核漏洞的功能转向对凭据对象交换有用的功能,因为不同类型的漏洞在内存损坏中提供不同的功能。其次,DirtyCred需要严格控制启动对象交换的时间窗口,因为可利用的宝贵时间窗口很短,如果没有一个切实可行的机制来延长时间窗口,利用将是不稳定的。第三,DirtyCred需要找到一种有效的机制,允许无特权用户以主动的方式分配特权凭证,因为如果没有这种能力,将导致无法主动触发凭证对象交换,影响利用。

DirtyCred将任何基于堆的漏洞转换为以无效方式释放凭据对象的能力,并利用三个不同的内核特性——userfaultfd、FUSE和文件锁来延长对象交换所需的时间窗口,从而达成稳定利用。同时DirtyeCred利用各种内核机制从用户空间和内核空间产生高特权线程,从而主动分配特权对象。

Credentials in Linux kernel

Linux中凭据会引用一些包含特权信息的内核属性。通过这些属性,Linux内核可以检查用户的访问权限。在Linux内核中,凭据被实现为携带特权信息的内核对象。这些对象包括credfileinode。因为inode对象只能在文件系统上创建新文件时分配,这没有为内存操作(成功利用程序的关键操作)提供足够的利用空间,所以只考虑credfile对象来设计利用链。

每个Linux进程都包含一个指向cred对象的指针。cred对象包含UID字段,表示进程权限。例如,GLOBAL_ROOT_UID表示任务具有root权限。当一个进程试图访问一个资源(例如一个文件)时,内核检查进程中cred对象中的UID,确定是否可以授予访问权限。除了UID,cred还包含capability.。该功能指定进程细粒度特权。例如,CAP_NET_BIND_SERVICE表示任务可以将套接字绑定到internet域特权端口。每个进程的凭证都是可配置的,在修改任务凭据时,内核遵循copy-and-replace方法。它会先复制凭据再修改副本,然后将进程中的cred指针更改为新修改的副本。在Linux中,每个进程只能更改自己的凭证。

在Linux内核中,每个文件都有其所有者的UID和GID以及其他用户的访问权限和能力。对于可执行文件,它们还具有SUID/SGID标志,指示允许其他用户以所有者的特权运行的特殊权限。在Linux内核实现中,每个文件都绑定到一个链接到凭证的inode对象。当一个进程试图打开一个文件时,内核调用函数inode_permission会在授予文件访问权之前检查inode和相应的权限。打开文件后,内核断开与inode对象的凭据链接并将它们附加到file对象。除了维护凭证之外,file对象还包含文件的读/写权限。通过file对象,内核可以索引到cred对象,从而检查特权。此外,它还可以检查读写权限,从而确保进程不会向以只读模式打开的文件写入数据。

Kernel Heap Memory Management

Linux内核专门设计了内存分配器来管理小内存分配以提高性能和防止碎片。尽管Linux内核中有三个不同的内存分配器,但它们拥有相同的设计方案。具体地说,它们都使用缓存来维护相同大小的内存。对于每个缓存,内核会分配内存页并将内存划分为大小相同的多个块,每个块是用于承载对象的内存片段。当缓存的内存页用完时,内核会为缓存分配新的页。如果缓存不再使用内存页,即内存页上的所有对象都被释放,内核将相应地回收内存页。

Linux内核中有两种主要的缓存,如下所述。

Generic Caches

Linux内核有不同的通用缓存来分配不同大小的内存。当从通用缓存分配内存时,内核将首先取整请求的大小并找到与大小请求匹配的缓存,然后从相应的缓存分配一个内存片段。在Linux内核中,如果分配请求没有指定它从哪些类型的缓存进行分配,那么分配在默认情况下发生在通用缓存上。对于属于相同通用缓存的分配,它们可以共享相同的内存地址,因为它们可以维护在相同的内存页面上。

Dedicated Caches

Linux内核为性能和安全目的创建专用缓存。由于有些对象在内核中经常被使用,为这些对象专用缓存可以减少分配它们的时间,从而提高系统性能。专用缓存与通用缓存不共享相同的内存页,因此在通用缓存中分配的对象与专用缓存中的对象不相邻。它可以被视为缓存级隔离,从而减轻了来自通用缓存中的溢出影响。

Threat Model

首先假设一个无特权用户拥有对Linux系统的本地访问权限,想要达成的效果是利用内核中的堆内存破坏漏洞本地提权。假设Linux启用了内核版本5.15中可用的所有攻击缓解和内核保护机制。这些机制包括KASLR, SMAP, SMEP, CFI, KPTI等。此时内核地址是随机的,且内核在执行期间不能直接访问用户空间内存,并且它的控制流完整性得到了保证。

DirtyCred利用

CVE-2021-4154为例展示DirtyCred是如何实际利用的。

CVE-2021-4154是由于类型混淆错误,文件对象被fs_context结构体中的指针错误引用。在Linux内核中,文件对象的生命周期是通过计数机制来维护的。当引用计数变为零时,该文件对象将自动释放,这意味着该文件对象不再被使用。然而,通过触发该漏洞,即使该文件仍在使用中,内核也将非法释放文件对象。

image-20221007220556314

如图所示,DirtyCred首先打开一个可写文件/tmp/x,这将在内核中分配一个可写文件对象。通过触发该漏洞,结构体中的指针将指向对应缓存中的文件对象,然后,DirtyCred尝试将内容写入打开的文件/tmp/x。在实际写内容之前,Linux内核会检查当前文件是否有写权限、位置是否可写等信息。通过内核检查后,DirtyCred会继续执行这个实际的文件写入操作,并进入第二步。在这一步中,DirtyCred触发fs_context的free操作来释放文件对象,从而使文件对象变成一个已经释放的内存块。然后在第三步中,DirtyCred打开一个只读文件/etc/passwd,这会让内核为/etc/passwd分配文件对象。如图所示,新分配的文件对象分到了之前被释放的内存。在此之后,DirtyCred将继续之前的写入操作,内核将执行实际的内容写入。因为文件对象已经交换,所以要写入的内容将被重定向到只读文件/etc/passwd。假设写入/etc/password的内容是hacker:x:0:0:root:/:/bin/sh,则攻击者就可以使用该方案注入root帐户,从而实现提权。

漏洞patch:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/kernel/cgroup/cgroup-v1.c b/kernel/cgroup/cgroup-v1.c
index ee93b6e895874..527917c0b30be 100644
--- a/kernel/cgroup/cgroup-v1.c
+++ b/kernel/cgroup/cgroup-v1.c
@@ -912,6 +912,8 @@ int cgroup1_parse_param(struct fs_context *fc, struct fs_parameter *param)
opt = fs_parse(fc, cgroup1_fs_parameters, param, &result);
if (opt == -ENOPARAM) {
if (strcmp(param->key, "source") == 0) {
+ if (param->type != fs_value_is_string)
+ return invalf(fc, "Non-string source");
if (fc->source)
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;

上面的例子只是说明了DirtyCred如何使用文件对象进行完整利用。如前所述,除了file外,cred也可以作为凭据。就像文件交换一样,攻击者也可以使用类似的方法来交换凭据,从而实现提权。DirtyCred Demos

根据CVE-2021-4154的现实利用来看,DirtyCred本身并不改变控制流,而是利用内核内存管理的特性来操作内存中的对象。因此,许多防止控制流篡改的现有防御措施并不影响DirtyCred的利用。虽然最近的一些研究工作通过重新设计内存管理机制(例如AUTOSLAB)来实现内核防御,但依然无法防范DirtyCred,因为新提出的内存管理方法仍然是粗粒度的,不足以阻碍所需要的内存操作。

技术细节

虽然上面的示例演示了DirtyCred如何实现利用并提权,但仍有许多技术细节需要进一步分析和解决。

DirtyCred需要一个非法释放低特权对象(例如,具有写权限的文件对象)的能力,然后重新分配一个高特权对象(例如,具有只读权限的文件对象)。实际上,内核漏洞可能并不总是提供这样的功能。例如,漏洞可能只提供越界写,而不是直接针对凭据对象提供非法释放。因此,对于不同类型的漏洞,DirtyCred需要对应的方法来利用。

在完成权限检查之后,在文件对象交换之前,DirtyCred需要保持真实文件写入。然而,保持写入的过程是很困难的。在Linux内核中,权限检查和实际的内容写入是并行的。如果没有一个切实可行的方案来准确地控制文件对象交换的发生,那么利用难度将大大提高。因此DirtyCred需要一系列有效的机制,以确保文件对象交换能够在所需的时间窗口发生。

DirtyCred中最关键的步骤之一是使用高特权凭据替换低特权凭据。为此,DirtyCred分配高特权对象,接管释放的内存块。但是,对于低权限用户来说,分配高权限凭据也不容易。虽然简单地等待特权用户自己分配可能会奏效,但这种被动策略极大地影响了利用的稳定性。首先,DirtyCred不知道什么时候可以回收所需的内存块并继续利用它。其次,DirtyCred无法控制新分配的对象,接管所需内存块的对象可能并没有所需的特权级别。所以DirtyCred需要一个用户空间机制和一个内核空间方案来解决这个问题。

PIVOTING VULNERABILITY CAPABILITY

如CVE-2021-4154所示,内核漏洞为DirtyCred提供了非法释放文件对象的能力。然而在实践中,其他内核漏洞可能没有这种能力。例如,double-free或use-after-free(UAF)可能不会直接指向凭据对象。而一些漏洞如越界(OOB)访问没有非法free的能力。为此,DirtyCred需要调整不同漏洞的利用链。

Pivoting OOB & UAF Write

给定能够覆盖内存中数据的OOB漏洞或UAF漏洞,DirtyCred首先识别共享相同内存的结构体(即利用对象),其中包含一个引用凭据对象的指针。然后,它利用SLAKE或其他堆利用技术在发生覆盖的内存区域分配对象。如图所示,为了利用一个OOB漏洞,受害对象需要刚好位于可控对象之后。DirtyCred通过越界写进一步修改对象包含的指针。更具体地说,DirtyCred将覆盖引用凭据对象的指针零到最后两个字节。

image-20221007234651485

众所周知,内存是在连续的页上管理的。在Linux内核中,内存页的地址始终保持最后一个字节为零。在新缓存中分配对象时会从内存页的开头开始。因此,上面的零字节覆写将使指针指向内存页的开头。例如,如图(b)所示,在使引用凭据对象的指针的最后两个字节为空之后,该指针将指向保存另一个凭据对象的内存页的开头。因此,在指针操作之后,DirtyCred将获得对新内存页第一个对象的非法引用。因为内核可以正常释放对象内存并将被攻击对象中的指针遗留为野指针,DirtyCred就可以通过堆喷用高特权凭据对象占据释放的位置,从而实现提权。

CVE-2022-2588复现

内核调试环境搭建

使用gef调试

1
2
3
4
5
6
sudo sed -i "s@http://.*archive.ubuntu.com@https://mirrors.tuna.tsinghua.edu.cn@g" /etc/apt/sources.list
sudo sed -i "s@http://.*security.ubuntu.com@https://mirrors.tuna.tsinghua.edu.cn@g" /etc/apt/sources.list
sudo apt-get update && sudo apt-get install python3-pip
pip3 install capstone unicorn keystone-engine ropper
git clone https://github.com/hugsy/gef.git
echo source `pwd`/gef/gef.py >> ~/.gdbinit

清华源下载 Linux kernel 压缩包并解压编译,漏洞分析使用5.16内核版本:

1
2
3
4
5
wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.16.tar.xz
unxz linux-5.16.tar.xz
tar xf linux-5.16.tar
cd linux-5.16
make menuconfig

在内核编译选项中,开启如下选项(推荐同时关闭KALSR和开启其他debug信息):

  • Kernel hacking -> Compile-time checks and compiler options -> Compile the kernel with debug info
  • Kernel hacking -> Generic Kernel Debugging Instruments -> KGDB: kernel debugger
image-20220928201252558

以上配置完成后会在当前目录生成 .config 文件,我们可以使用 grep 进行验证:

1
2
grep CONFIG_DEBUG_INFO .config
CONFIG_DEBUG_INFO=y

将 .config 中的 CONFIG_SYSTEM_TRUSTED_KEYS和CONFIG_SYSTEM_REVOCATION_KEYS内容置空。

image-20220928201737453

编译内核,编译完成后,会在当前目录下生成vmlinux,这个在 gdb 的时候需要加载,用于读取 symbol 符号信息,包含了所有调试信息,所以比较大。

1
2
sudo apt install libncurses-dev dwarves
make -j$(nproc) bzImage

在编译成功后,我们一般主要关注于如下的文件

  • bzImage:arch/x86/boot/bzImage
  • vmlinux:源码所在的根目录下。

此外,这里给出常见内核文件的介绍。

  • bzImage:目前主流的 kernel 镜像格式,即 big zImage(即 bz 不是指 bzip2),适用于较大的(大于 512 KB) Kernel。这个镜像会被加载到内存的高地址,高于 1MB。bzImage 是用 gzip 压缩的,文件的开头部分有 gzip 解压缩的代码,所以我们不能用 gunzip 来解压缩。
  • zImage:比较老的 kernel 镜像格式,适用于较小的(不大于 512KB) Kernel。启动时,这个镜像会被加载到内存的低地址,即内存的前 640 KB。zImage 也不能用 gunzip 来解压缩。
  • vmlinuz:vmlinuz 不仅包含了压缩后的 vmlinux,还包含了 gzip 解压缩的代码。实际上就是 zImage 或者 bzImage 文件。该文件是 bootable 的。 bootable 是指它能够把内核加载到内存中。对于 Linux 系统而言,该文件位于 /boot 目录下。该目录包含了启动系统时所需要的文件。
  • vmlinux:静态链接的 Linux kernel,以可执行文件的形式存在,尚未经过压缩。该文件往往是在生成 vmlinuz 的过程中产生的。该文件适合于调试。但是该文件不是 bootable 的。
  • vmlinux.bin:也是静态链接的 Linux kernel,只是以一个可启动的 (bootable) 二进制文件存在。所有的符号信息和重定位信息都被删除了。生成命令为:objcopy -O binary vmlinux vmlinux.bin
  • uImage:uImage 是 U-boot 专用的镜像文件,它是在 zImage 之前加上了一个长度为 0x40 的 tag 而构成的。这个 tag 说明了这个镜像文件的类型、加载位置、生成时间、大小等信息。

Linux系统启动阶段,boot loader加载完内核文件vmlinuz后,内核紧接着需要挂载磁盘根文件系统,但如果此时内核没有相应驱动,无法识别磁盘,就需要先加载驱动。而驱动又位于/lib/modules,得挂载根文件系统才能读取,这就陷入了一个两难境地,系统无法顺利启动。于是有了initramfs根文件系统,其中包含必要的设备驱动和工具,bootloader加载initramfs到内存中,内核会将其挂载到根目录/,然后运行/init脚本,挂载真正的磁盘根文件系统。

这里借助BusyBox构建极简initramfs,提供基本的用户态可执行程序,可以从busybox官网地址下载最新版本。

1
2
3
4
wget https://busybox.net/downloads/busybox-1.35.0.tar.bz2
tar xvf busybox-1.35.0.tar.bz2
cd busybox-1.35.0
make menuconfig

在 menuconfig 页面中,

  • Setttings 选中 Build static binary (no shared libs), 使其编译成静态链接的文件(因为 kernel 不提供 libc)
  • 在 Linux System Utilities 中取消选中 Support mounting NFS file systems on Linux < 2.6.23 (NEW)
  • 在 Networking Utilities 中取消选中 inetd
1
2
sudo apt-get install libc6-dev
make && make install

编译完成后将生成文件夹_install,该目录将成为我们的 rootfs。

接下来在 _install 文件夹下执行以创建一系列文件:

1
2
cd _install
mkdir -p proc sys dev etc/init.d

之后,在 rootfs 下(即 _install 文件夹下)编写以下 init 挂载脚本:

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
echo "{==DBG==} 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 "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
setsid /bin/cttyhack setuidgid 1000 /bin/sh

最后设置 init 脚本的权限,并将 rootfs 打包:

1
2
3
4
5
chmod +x ./init
# 打包命令
find . | cpio -o --format=newc > ../../rootfs.img
# 解包命令
# cpio -idmv < rootfs.img

busybox的编译与安装在构建 rootfs 中不是必须的,但还是强烈建议构建 busybox,因为它提供了非常多的有用工具来辅助使用 kernel。

安装QEMU

1
sudo apt install qemu qemu-utils qemu-kvm virt-manager libvirt-daemon-system libvirt-clients bridge-utils texinfo

QEMU启动调试内核

1
qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -initrd ./rootfs.img -append "nokaslr"
  • -kernel ./arch/x86/boot/bzImage:指定启用的内核镜像;
  • -initrd ./rootfs.img:指定启动的内存文件系统;
  • -append "nokaslr console=ttyS0" :附加参数,其中 nokaslr 参数必须添加进来,防止内核起始地址随机化,这样会导致 gdb 断点不能命中;
image-20220928212832142

漏洞分析

漏洞patch:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/net/sched/cls_route.c b/net/sched/cls_route.c
index a35ab8c27866e..3f935cbbaff66 100644
--- a/net/sched/cls_route.c
+++ b/net/sched/cls_route.c
@@ -526,7 +526,7 @@ static int route4_change(struct net *net, struct sk_buff *in_skb,
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);

- if (fold && fold->handle && f->handle != fold->handle) {
+ if (fold) {
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);