利用纯数据漏洞逃逸Google kCTF容器

本文详细介绍了如何利用Linux内核中的io_uring漏洞,通过纯数据利用技术实现容器逃逸,包括漏洞分析、利用链构建和实际代码实现。

利用纯数据漏洞逃逸Google kCTF容器

引言

自去年秋季以来,我一直在断断续续地进行Linux内核漏洞开发和漏洞研究。几个月前,我在度假期间有空闲时间,决定挑战自己,为kCTF中一个真实被利用的漏洞编写我的第一个纯数据利用。io_ring在该项目历史上一直是一个热门目标,因此我选择了一个易于理解的已被利用的漏洞作为漏洞开发创造力的沃土。

我选择的漏洞导致了一个struct file的释放后使用(UAF),使得我们可以持有对已释放对象的打开文件描述符。关于文件UAF利用的文章已经有很多,因此我决定挑战自己,我的利用必须是纯数据的。这个自我设定的挑战参数完全是任意的,但我只是想尝试编写一个不依赖劫持控制流的利用。

漏洞

这个漏洞非常简单(为什么我找不到这样的漏洞?),并在去年11月在kCTF中被利用。我没有在kCTF Discord中仔细查找或询问,但未能找到这个特定利用的PoC。我找到了几个利用类似漏洞的优秀文章,特别是pqlpql和Awarau的这篇文章:https://ruia-ruia.github.io/2022/08/05/CVE-2022-29582-io-uring/。

我不会深入讨论漏洞细节,因为它对于发挥创造力和编写新型利用(对我来说是新的)并不重要;然而,从补丁中可以看出,有一个对put(减少)文件引用的调用,而没有首先检查该文件是否是io_uring中的固定文件。

巨人的肩膀

必须强调的是,上面链接的@pqlpql和@Awarau1的博客文章对这个过程非常关键。在那篇文章中,他们详细分解了如何利用称为"cross-cache"的技术强制Linux内核将整个文件对象页面释放回页面分配器。

我没有使用这个技巧将整个受害者文件对象页面发送回页面分配器,然后让该页面用作通用缓存对象的后备,而是选择以管道缓冲区的形式重新分配该页面。这是一个非常强大的技术,因为我们控制管道缓冲区的所有内容(通过写入),并且可以读取100%的页面内容(通过读取)。

任意读取

我首先寻找的是数据泄漏的方法,因为我一直认为所有Linux内核利用都遵循相同的模式:实现泄漏以击败KASLR,在内存中找到一些有价值的对象,覆盖函数指针等等。

我唯一确定的是我有一个可用的打开文件描述符,所以让我们查看Linux内核中的文件系统代码。最先引起我注意的是fs/fcntl.c中的fcntl系统调用。

copy_to_user函数是Linux内核与用户空间接口的关键部分。它用于将数据从内核自己的内存空间复制到用户进程的内存空间。

在F_GET_RW_HINT情况下,一个u64(“h”)被复制回用户空间。该值来自inode->i_write_hint的值。而inode本身由file_inode(file)返回。

如果我们控制文件,那么我们也控制inode。由于我们控制文件的全部后备数据,因此我们控制inode成员的值。

查找读取目标

我们可以读取任何我们想要的地址的数据,但我们不知道要读取什么。我思考了一段时间,然后记得cpu_entry_area在每次启动时不会随机化,它总是在相同的地址。

在通过GDB进行一些调试后,我注意到在cpu_entry_area中至少有一个内核文本指针一致出现,那就是error_entry函数内部的一个地址。

查找写入gadget

我实际上花了几天时间才找到一个满意的写入gadget。我有点被找到任意读取gadget的经验宠坏了,认为这会是一个类似的简单搜索。

我大致遵循了相同的过程,遍历接受fd作为参数的系统调用,并跟踪它们寻找copy_to_user的调用,但没有同样的运气。在这段时间里,我正在与我非常有才华的朋友@Firzen14讨论这个话题,他提出了这个概念:https://googleprojectzero.blogspot.com/2022/11/a-very-powerful-clipboard-samsung-in-the-wild-exploit-chain.html#h.yfq0poarwpr9。

在P0博客文章中,他们讨论了signalfd文件的signalfd_ctx如何存储在f.file->private_data字段中,以及signalfd系统调用如何允许攻击者执行ctx->sigmask的写入。

利用计划

既然我们拥有了所有可能需要的工具,是时候开始制定利用计划了。在kCTF环境中,您作为非特权用户在容器内运行,您的目标是逃逸容器并从主机文件系统读取标志值。

查找Init

此时,我的目标是在内存中找到主机Init task_struct,并找到几个重要成员的值:real_cred、cred和nsproxy。

伪造对象

有了这些值,我们现在需要在内存中找到我们自己的task_struct,以便用init的成员覆盖我们的成员。为此,我利用了task_struct具有系统上任务链表的事实。

执行写入

现在,是时候进行我们的写入了。记住我们在io_fill_cqe_aux内部要使用的那三个顺序写入,但我说它们不会与利用计划一起工作?原因是,这三个写入如下:

它们工作得很好,直到我去覆盖目标子task_struct的nsproxy成员。其中一个写入不可避免地覆盖了nsproxy旁边的成员:signal和sighand。这给我带来了大问题,因为当中断发生时,这些成员(指针)会被解引用,并导致内核崩溃,因为它们是无效的值。

结论

这对我来说非常有趣,我学到了很多。我认为这类学习挑战很棒且风险低。它们也可以与朋友一起工作,非常感谢已经提到的每个人,还有@chompie1337,他不得不听我在覆盖凭据后无法读取标志时惊慌失措。

完整的利用代码已发布在下面,如果您对任何部分有理解困难,请告诉我。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 编译
// gcc sploit.c -o sploit -l:liburing.a -static -Wall

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <stdarg.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/msg.h>
#include <sys/timerfd.h>
#include <sys/mman.h>
#include <sys/prctl.h>

#include "liburing.h"

// ... 完整代码实现 ...
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计