userfaultfd 的使用

概述

严格意义而言 userfaultfd 并非是一种利用手法,而是 Linux 的一个系统调用,简单来说,通过 userfaultfd 这种机制,用户可以通过自定义的 page fault handler 在用户态处理缺页异常

下面的这张图很好地体现了 userfaultfd 的整个流程:

userfaultfd.png

要使用 userfaultfd 系统调用,我们首先要注册一个 userfaultfd,通过 ioctl 监视一块内存区域,同时还需要专门启动一个用以进行轮询的线程 uffd monitor,该线程会通过 poll() 函数不断轮询直到出现缺页异常

  • 当有一个线程在这块内存区域内触发缺页异常时(比如说第一次访问一个匿名页),该线程(称之为 faulting 线程)进入到内核中处理缺页异常

  • 内核会调用 handle_userfault() 交由 userfaultfd 处理

  • 随后 faulting 线程进入堵塞状态,同时将一个 uffd_msg 发送给 monitor 线程,等待其处理结束

  • monitor 线程调用通过 ioctl 处理缺页异常,有如下选项:

    • UFFDIO_COPY:将用户自定义数据拷贝到 faulting page 上

    • UFFDIO_ZEROPAGE :将 faulting page 置 0

    • UFFDIO_WAKE:用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKEUFFDIO_ZEROPAGE_MODE_DONTWAKE 模式实现批量填充

  • 在处理结束后 monitor 线程发送信号唤醒 faulting 线程继续工作

以上便是 userfaultfd 这个机制的整个流程,该机制最初被设计来用以进行虚拟机/进程的迁移等用途,但是通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高,比如在如下的操作时:

如果在进入函数后,实际拷贝开始前线程被中断换下 CPU,别的线程执行,修改了 kptr 指向的内存块的所有权(比如 kfree 掉了这个内存块),然后再执行拷贝时就可以实现 UAF。这种可能性当然是比较小的,但是如果 user_buf 是一个 mmap 的内存块,并且我们为它注册了 userfaultfd,那么在拷贝时出现缺页异常后此线程会先执行我们注册的处理函数,在处理函数结束前线程一直被暂停,结束后才会执行后面的操作,大大增加了竞争的成功率。

使用方法

Linux man pagearrow-up-right 当中便已经为我们提供了 userfaultfd 的基本使用模板,我们只需要稍加修改便能直接投入到实战当中,下面笔者给出自用的为特定内存注册 userfaultfd monitor 的模板:

我们可以直接通过如下操作来为一块匿名的 mmap 内存注册 userfaultfd:

需要注意的是 handler 的写法,这里笔者直接照抄 Linux man page 改了改,可以根据个人需求进行个性化改动:

例题:QWB2021-notebook

这里以强网杯 2021 的 notebook 一题为例解释 userfaultfd 在条件竞争中的使用

分析

首先看一下启动脚本

append 时把 loglevel 开到了 3,建议把这个去掉,调试起来会好判断一点(可以看到驱动 printk 的内容)。

程序的流程比较简单,也没有去符号,这里就不分析了。程序主要的漏洞就是条件竞争造成的 UAF。

首先先说一下读写锁,其性质为

  • 当写锁被取走时,所有取锁操作被阻塞

  • 当读锁被取走时,取写锁的操作被阻塞

恰当的使用读写锁可以在提高程序性能的前提下保证线程同步。题目中的驱动程序在 noteedit 和 noteadd 操作中取了读锁,仅在 notedel 操作中取了写锁。其余操作都没有锁保护。而两个取读锁的操作实际上都有写操作,但是他们又是可以并发的,这样就很可能存在条件竞争的漏洞。

这是 noteedit 操作的部分代码,这里的 krealloc 并未对 newsize 做任何限制。同时并没有及时更新 note 指针,反而在更新前加入了 copy_from_user 的操作,那么就可以考虑通过 userfaultfd 操作卡死当前线程,避免 note 的更新,这样就可以保留一个被 kfree 的 slab 的指针。这样操作的问题是 note 的 size 被更新为了 0,之后 read 和 write 操作就无法读写数据了。

不过在 add 操作时,也类似的在更新 size 前加入了 copy_from_user 的操作,我们也可以把线程卡死在这里,把 size 改为 0x60。

因此,我们可以做到

  • 申请任意大小的 slab。虽然 add 操作限制了 size 最大为 0x60,但是通过 edit 可以 krealloc 出任意大小的 slab

  • UAF 任意大小的 slab。不过只能控制前 0x60 字节的数据

利用

由于 edit 函数中使用了 copy_from_user(),这为 userfaultfd 的介入提供了可能性,我们可以:

  • 分配一个特定大小的 note

  • 新开 edit 线程通过 krealloc(0) 将其释放**,并通过 userfaultfd 卡在这里**

此时 notebook 数组中的 object 尚未被清空,仍是原先被释放了的 object,我们只需要再将其分配到别的内核结构体上便能完成 UAF

这里我们还是选择最经典的 tty_struct 来完成利用,由于题目提供了读取堆块的功能,故我们可以直接通过 tty_struct 中的 tty_operations 泄露内核基地址,其通常被初始化为全局变量 ptm_unix98_opspty_unix98_ops

开启了 kaslr 的内核在内存中的偏移依然以内存页为粒度,故我们可以通过比对 tty_operations 地址的低三16进制位来判断是 ptm_unix98_ops 还是 pty_unix98_ops

由于题目提供了写入堆块的功能,故我们可以直接通过修改 tty_struct->tty_operations 后操作 tty(例如read、write、ioctl...这会调用到函数表中的对应函数指针)的方式劫持内核执行流,同时 notegift() 会白给出 notebook 里存的 object 的地址,那么我们可以直接把 fake tty_operations 布置到 note 当中。

不过相比于传统的构造冗长的栈迁移的 ROP chain,长亭在 WP 中提到了一个很有趣的 trick,原文链接arrow-up-right。这里引用原文

控制 rip 之后,下一步就是绕过 SMEP 和 SMAP 了,这里介绍一种在完全控制了 tty 对象的情况下非常好用的 trick,完全不用 ROP,非常简单,且非常稳定(我们的 exploit 在利用成功和可以正常退出程序,甚至关机都不会触发 kernel panic)。

内核中有这样的一个函数:

img

其编译后大概长这样:

img

该函数位于 workqueue 机制的实现中,只要是开启了多核支持的内核 (CONFIG_SMP)都会包含这个函数的代码。不难注意到,这个函数非常好用,只要能控制第一个参数指向的内存,即可实现带一个任意参数调用任意函数,并把返回值存回第一个参数指向的内存的功能,且该 "gadget" 能干净的返回,执行的过程中完全不用管 SMAP、SMEP 的事情。由于内核中大量的 read / write / ioctl 之类的实现的第一个参数也都恰好是对应的对象本身,可谓是非常的适合这种场景了。考虑到我们提权需要做的事情只是 commit_creds(prepare_kernel_cred(0)),完全可以用两次上述的函数调用原语实现。(如果还需要禁用 SELinux 之类的,再找一个任意地址写 0 的 gadget 即可,很容易找)

利用这个原语就可以比较容易的任意函数执行了。

在利用过程当中我们还需要注意两点:

  • 由于题目环境存在多个 CPU core,因此我们应当使用 sched_setaffinity() 将进程绑定到指定核心上,从而确保内核对象分配的稳定性,而无需进行堆喷射

  • tty_struct 的结构也被我们所破坏了,在完成提权之后我们应该将其内容恢复原样

exp

最后进行稳定化提权的 exp 如下,其中 kernelpwn.h 来自于这里arrow-up-right

新版本内核对抗 userfaultfd 在 race condition 中的利用

正所谓“没有万能的银弹”,可能有的人会发现在较新版本的内核中 userfaultfd 系统调用无法成功启动:

uffd_failed

这是因为在较新版本的内核中修改了变量 sysctl_unprivileged_userfaultfd 的值:

来自 linux-5.11 源码fs/userfaultfd.c

来自 linux-5.4 源码fs/userfaultfd.c

在之前的版本当中 sysctl_unprivileged_userfaultfd 这一变量被初始化为 1,而在较新版本的内核当中这一变量并没有被赋予初始值,编译器会将其放在 bss 段,默认值为 0

这意味着在较新版本内核中只有 root 权限才能使用 userfaultfd,这或许意味着刚刚进入大众视野的 userfaultfd 可能又将逐渐淡出大众视野,但不可否认的是,userfaultfd 确乎为我们在 Linux kernel 中的条件竞争利用提供了一个全新的思路与一种极其稳定的利用手法

Reference

【PWN.0x00】Linux Kernel Pwn I:Basic Exploit to Kernel Pwn in CTFarrow-up-right

linux kernel pwn学习之条件竞争(二)userfaultfdarrow-up-right

https://zhuanlan.zhihu.com/p/385645268arrow-up-right

https://www.cjovi.icu/WP/1455.htmlarrow-up-right

https://www.cjovi.icu/WP/1468.htmlarrow-up-right

从内核到用户空间(1) — 用户态缺页处理机制 userfaultfd 的使用arrow-up-right

Last updated