[toc]
漏洞编号: CVE-2022-0185
漏洞评分:
漏洞产品: linux kernel - fsconfig syscall
影响范围: linux kernel 5.1-rc1 ~ 5.16.2
利用条件: linux 本地; 具有CAP_SYS_ADMIN cap权限(可以unshare 直接获得,等于无限制)
利用效果: 本地提权;容器逃逸
源码获取: git clone git://kernel.ubuntu.com/ubuntu/ubuntu-focal.git -b Ubuntu-hwe-5.11-5.11.0-27.29_20.04.1 --depth 1
或 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/
5.X 内核编译环境docker :chenaotian/kernelcompile
漏洞分析docker:chenaotian/cve-2022-0185
准备了两个内核一个发行版,一个自己编译版
安装qemu、gdb、gdb-peda 等
漏洞相关在 /root/cve-2022-0185
boot_exp.sh 用于启动exp 验证调试环境,发行版5.11.0-44无符号内核boot_poc.sh 用于启动poc 验证环境,可以跑崩内核,但无法跑exp,自编译5.13 有符号内核exp 目录,exp 源码(作者: BitsByWill),直接编译exploit_fuse即可。qemu 环境:https://github.com/chenaotian/CVE-2022-0185/tree/main/qemuANDexp
ubuntu 20.04虚拟机exp 运行环境运行原作者exp
准备ubuntu20.04 虚拟机,然后更换内核:
apt-get install linux-image-5.11.0-44-generic
grep menuentry /boot/grub/grub.cfg
vim /etc/default/grub
#修改 GRUB_DEFAULT 选项为上面结果中想要启动内核的下标
update-grub
#如果不生效的话则直接进入/boot 目录将之前的内核相关文件(带之前内核编号的文件)全部删掉,然后启动时候报找不到内核,然后手动选择内核启动也可以
#编译exp
make fuse
./exploit
提权效果

漏洞发生的系统调用是fsconfig 中的 FSCONFIG_SET_STRING 操作选项,该系统调用用于对已经打开的文件系统上下文进行一些配置,需要的前提条件是具备CAP_SYS_ADMIN cap权限:
fsopen的主要目的就是创建一个文件系统上下文,然后把它和一个文件描述符挂钩,返回文件描述符。fsopen后面就是fsconfig,从字面意思应该可以猜到,我们上面通过fsopen创建了一个文件系统上下文,下面的fsconfig可能就是用来配置文件系统上下文里的内容的。事实上fsconfig确实主要是做这个配置工作的,除了文件系统上下文,同时它还支持其它的工作。
首先漏洞出现在 legacy_parse_param 函数中:
linux-5.11\fs\fs_context.c : 502 : legacy_parse_param
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct legacy_fs_context *ctx = fc->fs_private;
unsigned int size = ctx->data_size;
size_t len = 0;
··· ···
··· ···
switch (param->type) {
case fs_value_is_string:
len = 1 + param->size;
fallthrough;
··· ···
}
if (len > PAGE_SIZE - 2 - size) //此处边界检查有问题
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
memchr(param->string, ',', param->size)))
return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
param->key);
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); //在第一次时会分配一页大小
if (!ctx->legacy_data)
return -ENOMEM;
}
ctx->legacy_data[size++] = ',';
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
memcpy(ctx->legacy_data + size, param->string, param->size); //拷贝,可能越界
size += param->size;
}
ctx->legacy_data[size] = '\0';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
关键在于后面的memcpy,会将我们传入的param->string 拷贝到ctx->legacy_data 之中。而判断是否拷贝越界就在前面的(len > PAGE_SIZE - 2 - size) 判断,这里判断是有问题的,判断类型是size_t 也就是unsigned int,如果size > PAGE_SIZE - 2 则会发生整数溢出反转,造成len < PAGE_SIZE - 2 - size ,进而判断通过,后面拷贝的时候size 是大于 PAGE_SIZE - 2的,造成拷贝越界。
用到的一些数据结构:
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) */
};
struct legacy_fs_context {
char *legacy_data; /* Data page for legacy filesystems */
size_t data_size;
enum legacy_fs_param param_type;
};
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;
};
下面分析一下函数调用栈,首先入口肯定是 fsconfig 系统调用:
linux-5.11\fs\fsopen.c : 314 : SYSCALL_DEFINE5(fsconfig,...
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,
};
··· ···
f = fdget(fd);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;
fc = f.file->private_data; //设置fc
··· ···
switch (cmd) {
··· ···
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;
//初始化结构体中的联合体中的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);//设置size
break;
··· ···
··· ···
}
ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) {
ret = vfs_fsconfig_locked(fc, cmd, ¶m);
mutex_unlock(&fc->uapi_mutex);
}
··· ···
··· ···
}
在fsconfig 系统调用的入口中,先根据文件描述符fd 初始化文件系统上下文结构体fc,然后根据用户传入的参数设置param 结构体,该结构体变量就是后面在漏洞发生函数legacy_parse_param 中使用的param。接下来进入vfs_fsconfig_locked函数:
linux-5.11\fs\fsopen.c : 216 : vfs_fsconfig_locked
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;
}
首先调用finish_clean_context函数,这里会调用legacy_init_fs_context 函数来注册回调函数表,该回调函数表中就包括漏洞所在函数legacy_parse_param。
linux-5.11\fs\fs_context.c
int finish_clean_context(struct fs_context *fc)
{
··· ···
error = legacy_init_fs_context(fc);
··· ···
}
static int legacy_init_fs_context(struct fs_context *fc)
{
fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL);
if (!fc->fs_private)
return -ENOMEM;
fc->ops = &legacy_fs_context_ops; //注册回调函数表
return 0;
}
const struct fs_context_operations legacy_fs_context_ops = {
.free = legacy_fs_context_free,
.dup = legacy_fs_context_dup,
.parse_param = legacy_parse_param, //漏洞函数
.parse_monolithic = legacy_parse_monolithic,
.get_tree = legacy_get_tree,
.reconfigure = legacy_reconfigure,
};
注册结束之后,进入vfs_parse_fs_param 函数处理参数,这里会调用刚注册的回调函数,也就是漏洞函数。
linux-5.11\fs\fs_context.c : 98 : vfs_parse_fs_param
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param)
{
··· ···