[漏洞分析] CVE-2021-4154 cgroup1 fsconfig UAF内核提权

27 篇文章 4 订阅
17 篇文章 11 订阅

漏洞简介

漏洞编号: 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, &param);//[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之中。

利用效果:

在这里插入图片描述

参考

Markakd/CVE-2021-4154

Cautious: A New Exploitation Method! No Pipe but as Nasty as Dirty Pipe - Black Hat USA 2022 | Briefings Schedule

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值