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内核中是作为携带特权信息的内核对象来实现的,这些对象主要包括cred
、file
和inode
对象。鉴于inode
对象仅在文件系统上创建新文件时分配,它提供的利用空间不足以支持内存操作(成功利用漏洞的关键步骤),因此,漏洞利用主要集中在cred
和file
对象上。
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 */
...
}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/passwd
的file
结构体)来占据这个内存块,使得待写入的数据被写入/etc/passwd
中,造成本地提权。
漏洞修补:
1 |
|
如上所示,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漏洞的利用相对更为复杂:
利用流程如下:
- 在受影响对象所在的缓存中大量分配对象,使其释放时机可控且至少占用一个内存页。这样做的目的是让某个内存页的回收时机可控,因为如果该页上的所有对象都被释放,则该空闲页会被回收。
- 尝试触发两次double free漏洞,以在一个被释放的内存块上留下两个悬挂指针。
- 释放该受影响对象所在内存页上的所有对象,使该页被回收并用于
credential
的内存分配,成为专用缓存。 - 在这个现已成为
credential dedicated cache
的内存页上大量分配credential
结构体,以占满该页内存。 - 注意到两个悬挂指针可能不与
credential object
对齐,需要利用其中一个悬挂指针来释放出一个credential object
的内存块。 - 分配新的
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:
- 大量执行Set-UID程序(如
sudo
),或频繁创建特权级守护进程(如sshd
),以此来创建特权credential结构体。 - 使用ReadOnly方式打开如
/etc/passwd
这类特权文件。
在内核层面,当内核创建新的kernel thread时,当前的kernel thread及其特权credential结构体会被复制。因此,只要找到稳定创建kernel thread的方法,Dirty Cred就能稳定地创建特权credential结构体。实现这一目标的方法包括:
- 向kernel workqueue中填充大量任务,动态创建新的kernel thread来执行这些任务。
- 调用usermode helper(一种允许内核创建用户模式进程的机制)。最常见的应用场景是加载内核模块到内核空间中。
1 |
|
内核在加载内核模块时,会在内核层执行modprobe程序,以搜索标准安装路径下的目标驱动。
EVALUATION
可利用的内核对象
在Linux 5.16.15版本中,DirtyCred利用的前提是内核对象中必须包含credential对象,且可以控制这些对象在内核堆上的分配时机。
分析结果如下:
- 几乎每个generic cache都至少有两个可利用对象。
- 各个可利用对象中credential的偏移量差异较大,这为Dirty
Cred的利用成功率提供了提升的可能性。
- 特别是对于OOB(越界写)漏洞,可覆写的偏移量可能相差甚远。
- 有五个可利用对象的credential相对偏移量为0,这意味着在内存破坏范围较小的情况下,Dirty Cred的利用成功率会更高。
满足评估条件的CVE漏洞
评估标准包括:
- 报告时间为2019年及以后的Linux内核漏洞。
- 能够在Linux堆上进行堆破坏。
- 触发条件不需要特定硬件支持。
- 能复现相应内核panic。
从上图可见,在所有缓解机制都启动的情况下,Dirty Cred的利用成功率为:16/24。其中:
- Double Free漏洞的利用成功率最高。
- OOB漏洞中,有些案例因为OOB write发生在虚拟内存而非kmalloc分配的内存,因此不可利用。
- UAF漏洞中,一些无法完成利用的案例是因为仅能进行UAF read,无法执行invalid-write;或者虽然可以执行invalid-write,但写入位置不在可利用对象的credential字段上。
Dirty Cred防护
Dirty Cred之所以能成功利用,核心原因在于内核的内存隔离是基于类型而非权限。
防护方法相对简单:将privileged credentials与其他unprivileged credentials隔离。
实现方式是使用vzalloc/kvfree
函数在虚拟内存中创建与释放privileged
credentials内存,从而实现privileged和unprivileged对象在memory
cache中的隔离。
选择虚拟内存的原因:
- 如果使用两个不同的kmalloc分配的memory cache,存在通过Linux内核重用机制将privileged和unprivileged所在页合并的风险,导致隔离失效。
- 虚拟内存区域内的内存是内核动态分配、虚拟连续的,位于VMALLOC_START至VMALLOC_END区域内,不会与直接映射的内存区域重叠。
需要隔离的credential结构体包括:
- UID为GLOBAL_ROOT_UID的struct cred(privileged credentials)。
- 打开方式中带有可写权限的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 |
|