用户态缺页处理
pwn uffd机制
用户态缺页处理
本文档阐述了,如何利用 Linux 的 userfaultfd 机制,在用户空间处理缺页异常(page fault)。
1. 整体概述
代码主要流程如下:
- 内存映射
在main()中使用mmap分配了 2 页内存(每页 4KB)。 - 注册 userfaultfd
调用register_uffd()将映射区域注册到 userfaultfd,使得对这些区域的访问触发缺页异常。 - 缺页处理线程
启动一个专门处理缺页事件的线程(fault_handler_thread),该线程通过poll()、read()捕获缺页事件,然后利用UFFDIO_COPY将预先准备的数据填充到缺页区域。 - 触发缺页
在main()中通过读取映射内存触发缺页事件,最终观察到填充的数据。
2. main() 函数解析
int main() {
void *page;
page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
fatal("mmap");
printf("[+] mmap two pages at 0x%llx\n", (long long unsigned int)page);
register_uffd(page, 0x2000);
char buf[0x100];
puts("[*] reading from page#1");
strcpy(buf, (char *)(page));
printf("[+] 0x0000: %s\n", buf);
puts("[*] reading from page#2");
strcpy(buf, (char *)(page + 0x1000));
printf("[+] 0x1000: %s\n", buf);
puts("[*] reading from page#1");
strcpy(buf, (char *)(page));
printf("[+] 0x0000: %s\n", buf);
puts("[*] reading from page#2");
strcpy(buf, (char *)(page + 0x1000));
printf("[+] 0x1000: %s\n", buf);
getchar();
return 0;
}主要步骤
- 内存映射
使用mmap分配了 0x2000 字节(2 页)的内存,设置了读写权限。映射类型为匿名且私有。 - 打印映射地址
打印分配内存的起始地址,便于调试。 - 注册 userfaultfd
调用register_uffd(page, 0x2000)将分配的 2 页内存区域注册到 userfaultfd。 - 触发缺页
通过对映射区域内存的读取(使用strcpy拷贝数据到缓冲区),第一次访问将触发缺页异常。代码中读取两次每个页面,借此观察缺页处理线程的填充行为(根据缺页次数返回不同的字符串)。 - 暂停程序
使用getchar()阻塞程序退出,以便观察程序运行过程中的输出。
3. register_uffd() 函数解析
int register_uffd(void *addr, size_t len) {
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
long uffd;
pthread_t th;
puts("[*] registering userfaultfd");
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
fatal("userfaultfd");
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
fatal("ioctl(UFFDIO_API)");
uffdio_register.range.start = (unsigned long)addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
fatal("UFFDIO_REGISTER");
puts("[*] spawning a fault handler thread");
if (pthread_create(&th, NULL, fault_handler_thread, (void *)uffd))
fatal("pthread_create");
return 0;
}主要步骤
- 创建 userfaultfd
通过syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK)创建一个 userfaultfd 文件描述符,设置了O_CLOEXEC(子进程不继承)和O_NONBLOCK(非阻塞模式)。 - 初始化 API
使用ioctl(uffd, UFFDIO_API, &uffdio_api)初始化 userfaultfd,设置 API 版本为UFFD_API,不启用额外特性(features = 0)。 - 注册内存区域
设置uffdio_register结构体,指定要监控的内存区域起始地址和长度,模式设置为UFFDIO_REGISTER_MODE_MISSING(只对缺页页面进行处理)。调用ioctl将内存区域注册到 userfaultfd。 - 启动缺页处理线程
使用pthread_create创建一个新的线程,线程函数为fault_handler_thread,并将 userfaultfd 文件描述符作为参数传递给线程。
4. fault_handler_thread() 函数解析
static void *fault_handler_thread(void *arg) {
char *dummy_page;
static struct uffd_msg msg;
struct uffdio_copy copy;
struct pollfd pollfd;
long uffd;
static int fault_cnt = 0;
uffd = (long)arg;
puts("[t][*] mmaping one dummy page");
dummy_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (dummy_page == MAP_FAILED)
fatal("mmap(dummy)");
puts("[t][*] waiting for page fault");
pollfd.fd = uffd;
pollfd.events = POLLIN;
while (poll(&pollfd, 1, -1) > 0) {
if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
fatal("poll");
// block until triggered
if (read(uffd, &msg, sizeof(msg)) <= 0)
fatal("read(uffd)");
assert(msg.event == UFFD_EVENT_PAGEFAULT);
puts("[t][+] caught page fault");
printf("[t][+] uffd: flag=0x%llx, addr=0x%llx\n", msg.arg.pagefault.flags, msg.arg.pagefault.address);
// craft data and copy
puts("[t][*] writing hello world into dummy page");
if (fault_cnt++ == 0)
strcpy(dummy_page, "Hello, world! (1)");
else
strcpy(dummy_page, "Hello, world! (2)");
puts("[t][*] copying data from dummy page to faulted page");
copy.src = (unsigned long)dummy_page;
copy.dst = (unsigned long)msg.arg.pagefault.address & ~0xfff;
copy.len = 0x1000;
copy.mode = 0;
copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, ©) == -1)
fatal("ioctl(UFFDIO_COPY)");
}
return NULL;
}主要步骤
- 线程初始化
- 接收参数,将
arg转换为 userfaultfd 文件描述符。 - 定义相关变量:
dummy_page用于存放数据,msg用于接收缺页事件信息,copy用于UFFDIO_COPY操作,pollfd用于事件监听,fault_cnt用于统计缺页次数。
- 分配临时页面
- 使用
mmap分配一个大小为 0x1000(4KB)的临时页面dummy_page,用于在缺页事件时填充数据。
- 等待缺页事件
- 设置
pollfd监听 userfaultfd 文件描述符的可读事件(POLLIN)。 - 进入
while循环,使用poll()阻塞等待缺页事件。
- 处理缺页事件
- 当
poll()返回后,通过read()读取 userfaultfd 的事件消息,并使用assert确认事件类型为UFFD_EVENT_PAGEFAULT。 - 打印出缺页的标志和地址信息。
- 填充数据
- 根据
fault_cnt判断缺页次数:
- 第一次填充
"Hello, world! (1)" - 后续填充
"Hello, world! (2)"
- 将数据写入
dummy_page。
- 复制数据到缺页区域
- 配置 copy结构体:
copy.src:设置为dummy_page的地址(数据来源)。copy.dst:将缺页地址按页对齐(使用& ~0xfff)。copy.len:复制 0x1000 字节(即 4KB,一整页)。
- 使用
ioctl(uffd, UFFDIO_COPY, ©)将dummy_page中的数据复制到触发缺页的内存页面,从而完成缺页处理。
- 退出线程
- 当没有更多缺页事件时,线程将退出并返回
NULL。
5. 总结
- 初始化与注册
main()中调用mmap分配了 2 页内存,并通过register_uffd()注册到 userfaultfd。
- 缺页事件处理
- 注册后,对内存的读取触发缺页,缺页处理线程
fault_handler_thread()捕获并响应,通过UFFDIO_COPY将预设数据写入缺页区域。
- 效果验证
- 通过重复读取内存页面,可以观察到缺页处理线程根据缺页次数填充了不同的字符串数据,从而验证 userfaultfd 机制的工作效果。
这种机制常用于延迟加载数据、实现用户态内存管理和内存调试等场景。上述的处理函数中,穿插的我们自己的处理代码,就可以帮助实现条件竞争。
[+] mmap two pages at 0x7f0ed2616000
[*] registering userfaultfd
[*] spawning a fault handler thread
[t][*] mmaping one dummy page
[t][*] waiting for page fault
[*] reading from page#1
[t][+] catched page fault
[t][+] uffd: flag=0x0, addr=0x7f0ed2616000
[t][*] writing hello world into dummy page
[t][*] copying data from dummy page to faulted page
[+] 0x0000: Hello, world! (1)
[*] reading from page#2
[t][+] catched page fault
[t][+] uffd: flag=0x0, addr=0x7f0ed2617000
[t][*] writing hello world into dummy page
[t][*] copying data from dummy page to faulted page
[+] 0x1000: Hello, world! (2)
[*] reading from page#1
[+] 0x0000: Hello, world! (1)
[*] reading from page#2
[+] 0x1000: Hello, world! (2)poc
#define _GNU_SOURCE
#include <assert.h>
#include <fcntl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <unistd.h>
void fatal(const char *msg) {
perror(msg);
exit(1);
}
static void *fault_handler_thread(void *arg) {
char *dummy_page;
static struct uffd_msg msg;
struct uffdio_copy copy;
struct pollfd pollfd;
long uffd;
static int fault_cnt = 0;
uffd = (long)arg;
puts("[t][*] mmaping one dummy page");
dummy_page = mmap(NULL, 0x1000, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (dummy_page == MAP_FAILED)
fatal("mmap(dummy)");
puts("[t][*] waiting for page fault");
pollfd.fd = uffd;
pollfd.events = POLLIN;
while (poll(&pollfd, 1, -1) > 0) {
if (pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
fatal("poll");
// block until triggered
if (read(uffd, &msg, sizeof(msg)) <= 0)
fatal("read(uffd)");
assert(msg.event == UFFD_EVENT_PAGEFAULT);
puts("[t][+] caught page fault");
printf("[t][+] uffd: flag=0x%llx, addr=0x%llx\n", msg.arg.pagefault.flags, msg.arg.pagefault.address);
// craft data and copy
puts("[t][*] writing hello world into dummy page");
if (fault_cnt++ == 0)
strcpy(dummy_page, "Hello, world! (1)");
else
strcpy(dummy_page, "Hello, world! (2)");
puts("[t][*] copying data from dummy page to faulted page");
copy.src = (unsigned long)dummy_page;
copy.dst = (unsigned long)msg.arg.pagefault.address & ~0xfff;
copy.len = 0x1000;
copy.mode = 0;
copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, ©) == -1)
fatal("ioctl(UFFDIO_COPY)");
}
return NULL;
}
int register_uffd(void *addr, size_t len) {
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
long uffd;
pthread_t th;
puts("[*] registering userfaultfd");
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
fatal("userfaultfd");
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
fatal("ioctl(UFFDIO_API)");
uffdio_register.range.start = (unsigned long)addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
fatal("UFFDIO_REGISTER");
puts("[*] spawning a fault handler thread");
if (pthread_create(&th, NULL, fault_handler_thread, (void *)uffd))
fatal("pthread_create");
return 0;
}
int main() {
void *page;
page = mmap(NULL, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (page == MAP_FAILED)
fatal("mmap");
printf("[+] mmap two pages at 0x%llx\n", (long long unsigned int)page);
register_uffd(page, 0x2000);
char buf[0x100];
puts("[*] reading from page#1");
strcpy(buf, (char *)(page));
printf("[+] 0x0000: %s\n", buf);
puts("[*] reading from page#2");
strcpy(buf, (char *)(page + 0x1000));
printf("[+] 0x1000: %s\n", buf);
puts("[*] reading from page#1");
strcpy(buf, (char *)(page));
printf("[+] 0x0000: %s\n", buf);
puts("[*] reading from page#2");
strcpy(buf, (char *)(page + 0x1000));
printf("[+] 0x1000: %s\n", buf);
getchar();
return 0;
}
<sript>1</sript>