FUSE for Linux Exploitation 101

During the past few weeks, my friend @kiks and I started to develop an exploit for CVE-2022-2602: it’s an io_uring UAF.

We already completed the exploit using the userfaultfd technique to pause a kernel thread. Unfortunately, this technique is dead (thank you, vm.unprivileged_userfaultfd :/ ), so we started searching a good replacement for userfaultfd.

While reading @chompie’s blogpost about io_uring, I stumbled upon FUSE.

In this blogpost I’ll try to present to you my take on FUSE for Linux Exploitation :)

TLDR: FUSE filesystem allows users to pause a kernel thread, implementing a blocking read/write FUSE operation.

What is FUSE?

FUSE (Filesystem in USErspace) is a feature that allows users (also unprivileged users) to create fileystems in userspace.

FUSE architecture is composed of 3 main parts:

A simple FUSE filesystem

In order to write a simple filesystem in userspace, we need to use the fuse.h header in C programs.

Note: make sure you have libfuse-dev package installed on your system :)

Inside fuse.h, the struct fuse_operations is defined.

It contains all the function pointers to callbacks that the kernel module will use to handle different operations on FUSE files.

Another important element inside fuse.h is the fuse_main function.

Its signature is: fuse_main(int argc, char **argv, const struct fuse_operations *op, void *private_data).

This is the entry point of the filesystem: it takes the arguments from the main() function and also a pointer to the user-defined struct fuse_operations. It will take care of initializing the communication with the kernel module fuse.ko and it will wait for callbacks.

When an user issues a write to a FUSE file, the generic kernel write() interface will determine the filesystem of the file. If it’s a FUSE file, it will forward the write request to the kernel module fuse.ko, which will use the callback we defined to complete the write request.

Image

So, in order to create a minimal FUSE filesystem is required to:

And that’s it!

Goodbye userfaultfd, welcome FUSE!

With userfaultfd it’s possible to pause a kernel thread during the execution of a copy_from_user, using an mmap()ed chunk of memory with demand-zero paging¹.

With FUSE, an unprivileged user can create its own filesystem…how could it be exploited to pause a kernel thread?

The idea is that when a file is mmap()ed in memory, it’s mmap()ed using demand-zero paging as well. This means that the first read or write to that memory will cause a read() of the file from disk, in order to be copied into memory.

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <assert.h>
#include <string.h>
int main(){
	int fd = open("fuse_dir/lol", O_RDWR);
	void *addr = mmap(0x1000, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
	// mmap()ed the file in demand-zero paging
	printf("No read done from FUSE\n");
	assert(addr != -1);
	printf("Triggering read from FUSE\n");
	//THIS will trigger the call to FUSE read
	printf("%s\n", (char *)addr);
}

Well, since we have to handle all file operations to files in the FUSE filesystem from userspace, this will trigger the read() we defined in the FUSE filesystem.

We control what read() does, so we could, for example, simply call in a for loop sleep(). If a kernel thread executes a copy_from_user() on the FUSE mmap()ed file, this will pause the kernel thread.

#define FILE_TARGET "/lol"

static int FUSE_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi){
    if(strcmp(path, FILE_TARGET) == 0){
        for(;;){
            printf("[+] Pausing kernel thread...\n");
            sleep(100);
        }
    }
    return 0;
}

Conclusion

Building a FUSE filesystem it’s a good alternative to userfaultfd technique when developing kernel exploits, since userfaultfd is not enabled by default anymore for unprivileged users on almost all major linux distributions.

Full source code of the FUSE filesystem is here

CVE-2022-2602 writeup coming soon, stay tuned! :)