漏洞简介
漏洞编号: CVE-2021-4154
漏洞产品: linux kernel - cgroup1 - cgroup1_parse_param
影响范围: ~ linux kernel 5.14.0
利用条件: linux普通用户可执行代码
利用效果: 本地提权
该漏是Zhenpeng Lin 博士在blackhat 的议题:"Cautious! A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe"中的演示漏洞之一。漏洞存在于cgroup1 文件系统中的cgroup1_parse_param 函数在处理key 为"source"的情况时默认命令类型为fs_value_is_string,然而实际可能是其他类型,造成可以错误关闭已经打开的文件描述符。使用漏洞作者的dirty cred方法可以完成不依赖内核版本(特定地址)的本地提权。和之前的dirty pipe 系列利用方法一样,实现无地址依赖的本地提权攻击。
环境搭建
没有什么特殊的需要的编译选项,直接就可以断住关键函数,不过复现exp的话需要有比较大的内存。
ubuntu 20.04 中利用效果:
漏洞原理
漏洞补丁
首先查看漏洞补丁:
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=3b0462726e7ef281c35a7a4ae33e93ee2bc9975b
在key 为"source"的情况下添加了一条限制分支,但这么看看不出什么,我们需要从头开始分析fsconfig 的逻辑,才能理解该漏洞的逻辑。
fsconfig 原理
首先看fsconfig入口:
fs\fsopen.c : SYSCALL_DEFINE5(fsconfig
SYSCALL_DEFINE5(fsconfig,
int, fd,//[1] 打开的文件系统fd
unsigned int, cmd,//命令编号
const char __user *, _key,//命令对应的key 和value
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,//[2] 设置默认状态
};
if (fd < 0)
return -EINVAL;
switch (cmd) {//[3] 根据命令进行不同的合法性判断
case FSCONFIG_SET_FLAG:
if (!_key || _value || aux)
return -EINVAL;
break;
case FSCONFIG_SET_STRING:
if (!_key || !_value || aux)
return -EINVAL;
break;
case FSCONFIG_SET_BINARY:
if (!_key || !_value || aux <= 0 || aux > 1024 * 1024)
return -EINVAL;
break;
case FSCONFIG_SET_PATH:
case FSCONFIG_SET_PATH_EMPTY:
if (!_key || !_value || (aux != AT_FDCWD && aux < 0))
return -EINVAL;
break;
case FSCONFIG_SET_FD:
if (!_key || _value || aux < 0)
return -EINVAL;
break;
case FSCONFIG_CMD_CREATE:
case FSCONFIG_CMD_RECONFIGURE:
if (_key || _value || aux)
return -EINVAL;
break;
default:
return -EOPNOTSUPP;
}
f = fdget(fd); //[4] 获得打开文件系统的struct fd
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)//[4] 传入文件系统文件描述符必须是文件系统的文件描述符
goto out_f;
fc = f.file->private_data;//[4] 获取文件系统上下文结构体
if (fc->ops == &legacy_fs_context_ops) {
··· ···
}
}
if (_key) {//[5] 从参数中设置key
param.key = strndup_user(_key, 256);
if (IS_ERR(param.key)) {
ret = PTR_ERR(param.key);
goto out_f;
}
}
switch (cmd) {//[5] 设置其他参数
case FSCONFIG_SET_FLAG:
param.type = fs_value_is_flag;
break;
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;//设置type
param.string = strndup_user(_value, 256);//string 为value
if (IS_ERR(param.string)) {
ret = PTR_ERR(param.string);
goto out_key;
}
param.size = strlen(param.string);
break;
··· ···
case FSCONFIG_SET_FD:
param.type = fs_value_is_file;//设置type
ret = -EBADF;
param.file = fget(aux);// file 为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);//[6] 下一步操作
mutex_unlock(&fc->uapi_mutex);
}
··· ···
}
[1] fsconfig 传入5个参数,分别是
-
fd:打开的文件系统的文件描述符,必须有
-
cmd:需要的命令操作,对应下面几种,具体每项命令的含义可以不用太纠结
FSCONFIG_SET_FLAG = 0 FSCONFIG_SET_STRING = 1 FSCONFIG_SET_BINARY = 2 FSCONFIG_SET_PATH = 3 FSCONFIG_SET_PATH_EMPTY = 4 FSCONFIG_SET_FD = 5 FSCONFIG_CMD_CREATE = 6 FSCONFIG_CMD_RECONFIGURE = 7
-
key 和value:可以理解为对应命令的参数
- aux:补充参数
[2] 在参数结构体中设置默认状态,参数结构体如下
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;
};
[3] 根据命令进行不同的合法性判断,这里并没有需要绕过的点
[4] 获得打开文件系统的struct fd,传入文件系统文件描述符必须是文件系统的文件描述符,一切正确后获取文件系统上下文结构体和后续处理
[5] 提取参数补全fs_parameter参数结构体 param,注意其中联合体的部分:
- FSCONFIG_SET_STRING 命令时该字段是字符串,对应value值
- FSCONFIG_SET_FD 命令时该字段为file,对应aux(为一文件描述符)对应的struct file文件结构体
[6] 进入下一步
接下来处理的是vfs_fsconfig_locked 函数:
fs\fsopen.c : vfs_fsconfig_locked
static int vfs_fsconfig_locked(struct fs_context *fc, int cmd,
struct fs_parameter *param)
{
struct super_block *sb;
int ret;
··· ···
switch (cmd) {
case FSCONFIG_CMD_CREATE:
··· ···
case FSCONFIG_CMD_RECONFIGURE:
··· ···
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;
}
没什么需要关注的,这里在漏洞利用中直接走的默认分支,然后到下一步vfs_parse_fs_param:
fs\fs_context.c : vfs_parse_fs_param
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_param 函数,则调用对应的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.
*/
if (strcmp(param->key, "source") == 0) {//这里是正确的逻辑
if (param->type != fs_value_is_string)//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);
该函数中直接检查对应文件系统上下文有没有定义parse_param函数,如果有则调用对应的parse_param 函数进行参数处理,否则会进行一个简单处理,这里就会调用我们的漏洞函数cgroup1_parse_param:
kernel\cgroup\cgroup-v1.c : 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) {
if (strcmp(param->key, "source") == 0) {//对比补丁和上面简要处理部分,发现少了一个判断分支
if (fc->source)//缺少判断type 必须是fs_value_is_string
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;//将联合体部分赋值给source
param->string = NULL;
return 0;//然后返回,后面无需关注了
}
··· ···
}
··· ···
··· ···
}
漏洞触发
看完流程可能没感觉,可以关注一下合法和非法用法,首先合法用法下,这里type应该是必须是fs_value_is_string,那么是正确的,而type是在syscall入口的时候对传入命令不同而设置的,包括对参数的提取和堆param结构体的设置
if (_key) {//[5] 从参数中设置key
param.key = strndup_user(_key, 256);
if (IS_ERR(param.key)) {
ret = PTR_ERR(param.key);
goto out_f;
}
}
switch (cmd) {//[5] 设置其他参数
case FSCONFIG_SET_FLAG:
param.type = fs_value_is_flag;
break;
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;//设置type
param.string = strndup_user(_value, 256);//string 为value
if (IS_ERR(param.string)) {
ret = PTR_ERR(param.string);
goto out_key;
}
param.size = strlen(param.string);
break;
··· ···
case FSCONFIG_SET_FD:
param.type = fs_value_is_file;//设置type
ret = -EBADF;
param.file = fget(aux);// file 为aux对应的文件描述符对应的文件结构体
if (!param.file)
goto out_key;
break;
default:
break;
}
可以看出,假如按照如下方式调用fsconfig
fsconfig(fs_fd, FSCONFIG_SET_FD, "source", 0, any_file_fd)
那么提取参数和设置param结构体之后,param是如下内容:
struct fs_parameter {
const char *key = "source";
enum fs_value_type type = fs_value_is_file;
union {//在不同命令模式中不同的功能
char *string;
void *blob;
struct filename *name;
struct file *file = any_file_fd;
};
size_t size;
int dirfd;
};
那么传入漏洞函数的时候:
if (strcmp(param->key, "source") == 0) {//满足key == "source"
if (fc->source)
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;//将联合体部分赋值给source,由于是联合体,这里其实是把file结构体复制给了source
param->string = NULL;
这里把file 结构体当做string 类型的string 复制给了fc->source,这会造成什么后果呢?
非法释放任意文件结构体
上文说到可以通过漏洞将一个struct file结构体复制给fc->source,fc本身是struct fs_context 类型结构体,结构体具体结构我们先不关注,我们看在关闭一个文件系统文件描述符的时候是如何操作的,首先释放文件系统结构使用的是fscontext_release 函数:
fs\fsopen.c : 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);//直接调用put_fs_context 释放struct 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结构:
fs\fs_context.c : 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);//调用kfree 释放source
kfree(fc);
}
前面的部分都不用关注,后面直接调用kfree 释放fc->source,如果这里是上文中设置的一个文件描述符对应的struct file结构体,就会在这里被释放。但该文件描述符还是会对应这个struct file结构体,也就是文件描述符指向了一个被释放的内存区域,这时可以打开其他文件覆盖该文件描述符指向的struct file结构体,该操作高度契合Dirty Cred利用方法。
漏洞利用
漏洞利用过程比较简单,具体dirty cred 原理请见[kernel exploit] Dirty Cred: 一种新的无地址依赖漏洞利用方案,这里直接分析操作过程:
由于该漏洞直接可以对一个struct file 进行一次非法释放,可以说完美适配dirty cred。完全不需要像cve-2022-2588一样还要cross cache attack之类的麻烦操作。
- 直接对一个文件进行一次非法释放,这样该文件描述符就会指向一个被释放的struct file 结构体,不过该结构体虽然被释放了,但里面的内容只有8个字节会被改变,还是不影响正常使用的
- 然后A进程对该文件进行大量写入,inode会将其上锁
- C进程使用已经被释放的文件描述符尝试写入任意内容,会先进行权限校验,通过,然后等待A进程写入结束
- B进程喷射大量passwd文件,会覆盖之前释放的struct file内存区域。
- 等C进程等待结束,就会写入到passwd之中。
利用效果: