基于文件DirtyCred的容器逃逸新方法 | STAR Labs
目录
最近,我在研究针对Linux内核漏洞CVE-2022-3910的各种利用技术。在成功编写了一个利用DirtyCred实现本地权限提升的漏洞利用程序后,我的导师Billy问我是否可以通过修改代码,通过覆盖/proc/sys/kernel/modprobe来实现容器逃逸。
答案比预期的要复杂;这让我陷入了一个漫长而黑暗的技术探索过程…
在本文中,我将讨论漏洞的根本原因,以及我用来利用它的各种方法。
所有显示的代码片段都取自Linux内核v6.0-rc5的源代码,这是最新的受影响版本。
相关io_uring组件介绍
io_uring已经在这篇较早的文章中介绍得相当好了,所以我不会再重复相同的细节。相反,让我们专注于这个特定漏洞的相关组件。
这两个组件在Jens Axboe的幻灯片中都有简要讨论。
固定文件
固定文件或直接描述符可以看作是io_uring特定的文件描述符。io_uring维护对任何已注册文件的引用,以减少每次涉及它们的操作解析文件描述符所产生的额外开销;这个引用只有在固定文件被注销或io_uring实例被拆除时才会释放。
固定文件可以通过传递一个文件描述符数组给io_uring_register()来注册。或者,可以使用各种函数(如io_uring_prep_openat_direct())指示io_uring直接注册一个(无需用户空间程序首先使用其他系统调用获取文件描述符)。随后,可以通过设置IOSQE_FIXED_FILE标志并传递注册文件数组中的固定文件索引(而不是实际的文件描述符)来在未来的SQE中引用这些文件。
环消息
io_uring支持通过io_uring_prep_msg_ring()在环之间传递消息。更具体地说,根据手册页,此操作在目标环中创建一个CQE,其res和user_data设置为用户指定的值。
正如这里所指出的,此功能可用于唤醒等待环的休眠任务,或简单地传递任意信息。
漏洞分析
CVE-2022-3910是io_msg_ring()函数中的引用计数更新不当。源文件在这里,但相关代码片段如下所示:
|
|
漏洞本身的提示可以在补丁的提交消息中找到:
通常,io_uring的消息传递功能期望另一个io_uring实例的文件描述符。如果我们传入对其他任何内容的引用,它只会通过调用io_put_file()被丢弃,并返回错误。
如果我们传入一个固定文件,io_put_file()仍然被调用。但这种行为实际上是不正确的!我们没有获取文件的额外引用,因此不应该递减引用计数。
直接后果
io_put_file()只是fput()的包装器。你可以在这里找到其源代码,但以下理解已足够:
|
|
换句话说,通过重复触发漏洞直到引用计数降至0,我们可以在io_uring继续持有对其引用的情况下释放相关的文件结构体。这构成了use-after-free。
以下是一些显示我们如何做到这一点的代码:
|
|
我最初的漏洞利用尝试使用了一堆跨缓存喷射,并最终覆盖sk_buff结构体的析构函数(不是sk_buff->data分配,因为其最小大小太大)来获得执行控制。
这种"标准"漏洞利用不是本文的主要焦点,但如果你感兴趣,可以在这里查看我的代码。我未能在网上找到以与我相同方式使用sk_buff的其他文章,所以我认为值得简要提及。
DirtyCred技术
在我完成上述漏洞利用后,Billy鼓励我尝试编写一个不同的漏洞利用,改用DirtyCred。
DirtyCred是一种仅针对文件和凭据结构体的数据攻击。原始幻灯片比我能更清楚地解释这个概念,所以如果你不熟悉该技术,我建议先阅读此内容。特别相关的是"攻击打开文件凭据"部分,这正是我们将要使用的。
文件结构体
顾名思义,文件结构体表示一个打开的文件,并在文件打开时在filp slab缓存中分配。每个文件结构体跟踪其自己的引用计数,可以通过dup()和close()等操作进行修改。当引用计数达到零时,结构体被释放。
此结构的一些重要成员如下所示:
|
|
让我们简要介绍每一个:
f_op是指向函数表的指针,该表决定在请求对文件进行操作时调用哪个处理程序。例如,对于存在于ext4文件系统上的所有文件,这是ext4_file_operations。f_count存储文件的引用计数。f_mode存储文件的访问模式。这包括我们是否被允许从中读取或写入的标志。
注意:当我们多次open()同一个文件时,会分配多个文件结构体。相比之下,当我们在文件描述符上调用dup()时,现有文件结构体的引用计数会增加,并且不会进行新的分配。
代码分析
现在让我们尝试理解DirtyCred到底是如何工作的。假设我们以访问模式O_RDWR打开了文件A,并尝试写入它。这最终调用vfs_write(),如下所示:
|
|
假设在权限检查完成之后但实际写入开始之前,我们设法释放了文件A的文件结构体,并喷射了一个新的对应不同文件B的文件结构体,我们以访问模式O_RDONLY打开了文件B。访问模式不会再次检查,因此写入将在文件B上执行,即使我们不应该被允许这样做!
但是有可能一致地赢得这场竞争吗?
传统DirtyCred:针对ext4文件
在DirtyCred的典型应用中,文件A和B都驻留在ext4文件系统上。在这种情况下,写入最终由ext4_buffered_write_iter()处理:
|
|
为了避免多个任务同时写入同一文件产生的问题,写入操作被包含在互斥锁中。换句话说,任何时候只有一个进程可以写入文件。这使我们能够用下图所示的想法稳定漏洞利用(取自DirtyCred幻灯片):
当线程A对文件A执行慢写入时,它获取相应inode的锁。这阻止线程B进入[A]和[B]之间的临界区。我们可以利用这个等待期将文件A的文件结构体换出为文件B的文件结构体。当线程A释放inode锁时,线程B获取它并继续对错误的文件执行写入。
本地权限提升
不难看出这样的原语如何允许我们实现本地权限提升。一种可能性是通过定位/etc/passwd来添加具有root特权的新用户。但我采用了不同的方法,定位/sbin/modprobe。
当我们尝试执行具有未知魔术头的文件时,内核将以root特权并从root命名空间调用由全局内核变量modprobe_path指向的二进制文件。默认情况下,这是/sbin/modprobe。
因此,我用以下shell脚本覆盖了/sbin/modprobe:
|
|
当我尝试执行具有无效魔术头的文件时,内核执行了上述脚本,创建了/bin/sh的setuid副本。我们现在有一个root shell。
技术探索
当我把我的漏洞利用给Billy看时,他指出我的方法在容器化环境中不起作用,因为从容器的命名空间无法访问/sbin/modprobe。相反,他问我们是否可以直接通过/proc/sys/kernel/modprobe定位modprobe_path变量。
proc文件系统
/proc是一个伪文件系统,“充当内核内部数据结构的接口”。特别是,/proc/sys子目录允许我们更改各种内核参数的值,只需通过像文件一样写入它们。
作为一个相关的例子,/proc/sys/kernel/modprobe直接别名到modprobe_path内核全局变量,写入此"文件"将相应更改modprobe_path的值。
重要:如果我们不是root,就无法写入/proc/sys/*中的任何内容。但这不是大问题,因为我们可以先利用传统DirtyCred通过定位/etc/passwd获得本地权限提升。
应该清楚这些文件操作需要特殊的处理函数。与/proc/sys/*“文件"关联的文件结构体将f_op设置为proc_sys_file_operations。
这产生了一个问题,因为之前的inode锁定技术依赖于ext4_buffered_write_iter()仍然可以成功写入目标文件的假设。实际上尝试使用/proc/sys/*文件执行此操作将导致未定义行为,通常会导致返回错误代码。
相反,我们必须在写入处理程序调用解析之前交换文件结构体,这意味着我们有以下竞争窗口:
|
|
这相当小。我们能提高机会吗?
新目标:aio_write()
内核AIO子系统(不要与POSIX AIO混淆)是一个有些过时的异步I/O接口,可以看作是io_uring的前身。Billy指向我aio_write()函数,如果我们通过内核AIO接口请求写入系统调用,将调用该函数:
|
|
aio_setup_rw()使用copy_from_user()从用户空间复制iovecs。此外,它位于我们的竞争窗口内(在权限检查之后,但在写入处理程序解析之前)。因此,如果我们有权访问userfaultfd或FUSE,我们可以一致地赢得竞争,允许我们将写入操作重定向到/proc/sys/kernel/modprobe。
但是等等。为什么有人需要在容器内启用FUSE或用于userfaultfd的内核页面故障处理?可悲的事实是,利用上述技术所需的条件过于严格,在平均真实世界利用场景中无用。
注意:从技术上讲,即使userfaultfd内核页面故障处理被禁用,如果我们有CAP_SYS_PTRCAP能力(实际检查在这里),我们仍然可以使用它。然而,一般来说,即使作为容器root,我们也不太可能拥有此能力。
除非…
慢页面故障救援
让我们思考一下userfaultfd和FUSE在我们利用技术中扮演的角色。当内核在尝试从用户空间复制数据时遇到页面故障:
- userfaultfd导致故障内核线程暂停,直到我们从用户空间处理页面故障。
- 当内核尝试将故障页面加载到内存时,调用我们的自定义FUSE读取处理程序。
在这两种情况下,我们可以简单地在copy_from_user()调用处暂停内核线程,直到我们完成其他工作,比如堆喷射。但是否可能使页面故障花费足够长的时间,以便我们可以在该时间窗口内完成堆喷射?
在我花费几次尝试实验各种不太有效的想法后,Billy建议采用这种方法来显著增加页面故障产生的延迟(图片来自Google CTF discord):
shmem_fault()包含一个有用的注释,解释了为什么会这样:
|
|
整体利用方案
总之,我们的攻击计划如下:
-
以访问模式
O_RDWR打开某个随机文件,文件A。内核将分配相应的文件结构体。 -
使用漏洞,重复递减文件A的文件结构体的引用计数,直到下溢。这释放了它,尽管文件描述符表仍然包含对它的引用。
注意:这是必要的,因为
fget()(稍后在我们提交AIO请求时将被调用)如果在引用计数为0的文件结构体上调用,将导致内核停滞。违规代码在这里(检查get_file_rcu的宏扩展)。 -
使用
memfd_create()创建并获取临时文件B的文件描述符。使用fallocate()为其分配大量内存。 -
使用跨越页面边界的缓冲区准备AIO请求。第二页应由文件B支持,并且尚未驻留在内存中。
-
(CPU 1,线程X):在文件B上调用带有模式
FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE的fallocate()。 -
(CPU 1,线程Y):提交AIO请求。这触发由文件B支持的页面的页面故障。由于打孔正在进行,线程Y将把自己放在等待队列上,暂停执行直到线程X完成。
-
(CPU 0,线程Z):当线程Y暂停时,重复在
/proc/sys/kernel/modprobe上调用open()以用相应的文件结构体喷射堆,用/proc/sys/kernel/modprobe的文件结构体覆盖文件A的文件结构体。 -
线程Y恢复执行,写入在
/proc/sys/kernel/modprobe上执行。
你可以在这里找到漏洞利用的源代码。
实际容器测试
一旦所有这些完成,我继续尝试我的漏洞利用针对我在易受攻击的Ubuntu Kinetic镜像上设置的一些测试容器。这NOT运行内核版本v6.0-rc5,但任何与漏洞利用相关的代码几乎没有变化,所以这不应该是个问题。
注意:更具体地说,我使用了这个镜像,然后手动将内核降级到受影响版本(ubuntu 5.19.0-21-generic)。
为了演示成功的容器逃逸而不使事情过于复杂,我选择了一个简单的有效载荷,它在主机的系统上(容器外部)创建一个文件:
|
|
标准Docker容器
命令:sudo docker run -it --rm ubuntu bash
令人惊讶的是,我的漏洞利用对第一个测试目标不起作用。相反,我收到了Permission denied。怎么回事?
事实证明,在调用aio_setup_rw()之后,rw_verify_area()调用安全钩子函数。默认情况下,Docker容器在受限的AppArmor配置文件下运行,因此aa_file_perm()中的额外权限检查失败,导致aio_write()返回而不实际执行写入操作。😥
禁用AppArmor的Docker容器
命令:sudo docker run -it --rm --security-opt apparmor=unconfined ubuntu bash
但是,如果Docker容器以apparmor=unconfined运行,aa_file_perm()在实际权限检查发生之前提前退出,允许我们的漏洞利用顺利进行。
这种情况不是超级有用,因为不太可能有人会在已部署的Docker容器上特意禁用AppArmor。
标准containerd容器
命令:sudo ctr run -t --rm docker.io/library/ubuntu:latest bash
如果我们改为使用直接在containerd API之上操作的ctr命令行客户端启动容器,漏洞利用也可以正常工作。这很整洁!我们可以使用这种技术逃逸开箱即用的containerd容器。这是这种技术更现实的用例。🙂
演示
现在是演示时间。这是漏洞利用针对全新containerd容器运行的视频:
致谢
我要感谢:
- 我的导师Billy采纳了我看似荒谬的想法,并设法帮助我改进和稳定它,成为一种新的、一致的容器逃逸技术。
- Star Labs的其他人 :)
- @pql的慢页面技术。
参考文献
io_uring
- https://kernel-recipes.org/en/2022/wp-content/uploads/2022/06/axboe-kr2022-1.pdf
- https://lwn.net/Articles/863071/
- https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023#ring-messages
DirtyCred
- https://i.blackhat.com/USA-22/Thursday/US-22-Lin-Cautious-A-New-Exploitation-Method.pdf
- https://blog.hacktivesecurity.com/index.php/2022/12/21/cve-2022-2602-dirtycred-file-exploitation-applied-on-an-io_uring-uaf/
- https://lkmidas.github.io/posts/20210223-linux-kernel-pwn-modprobe/#the-overwriting-modprobe_path-technique
proc文件系统
内核AIO
fallocate()慢页面