仅用数据实现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中的固定文件。
技术实现
跨缓存技术
我选择使用管道缓冲区(pipe buffer)的形式重新分配页面,而不是使用将整个受害者文件对象页面送回页面分配器,然后让该页面用作通用缓存对象的后备存储的技巧。这是一个极其强大的技术,因为我们控制管道缓冲区的所有内容(通过写入),并且可以读取100%的页面内容(通过读取)。
任意读取原语
我首先寻找的是数据泄漏的方法,因为我一直认为所有Linux内核漏洞利用都遵循相同的模式:实现泄漏以击败KASLR,在内存中找到一些有价值的对象,覆盖函数指针等等。
我发现的任意读取gadget在fs/fcntl.c中的fcntl系统调用中。在F_GET_RW_HINT情况下,一个u64(“h”)被复制回用户空间。该值来自inode->i_write_hint的值,而inode本身由file_inode(file)返回。
由于我们控制文件,因此我们也控制inode。通过设置管道缓冲区中的fake file结构,我们可以将inode成员设置为任意地址,从而实现任意字节读取。
写入原语构建
在io_msg_ring中,可以看到target_ctx(类型为io_ring_ctx)源自req->file->private_data。由于我们控制fake file,我们可以控制private_data内容(在这种情况下是指向fake io_ring_ctx的指针)。
在io_commit_cqring中,我们基本上执行了一个memcpy操作:将ctx->cached_cq_tail(100%用户控制)的内容写入&ctx->ring->cq.tail(100%用户控制)。这种情况下写入的大小是4字节,因此我们实现了一个任意的4字节写入。
利用计划
在kCTF环境中,您作为非特权用户在容器内运行,目标是逃逸容器并从主机文件系统读取标志值。
我计划找到主机Init task_struct在内存中的位置,并找到几个重要成员的值:real_cred、cred和nsproxy。通过任意读取能力,我们可以复制这些值,因为它们位于已知地址(从init任务符号的偏移量)。
实现细节
对象伪造
我利用task_struct具有系统上任务链表的事实。在漏洞利用的早期,我生成一个具有已知名称的子进程,该名称适合task_struct comm字段。当我遍历系统上的任务链表时,只需检查每个任务的comm字段以查找易于识别的子进程。
我还遍历了自己任务的文件描述符表,了解了管道缓冲区的地址。这是因为管道缓冲区页面显然是页面对齐的,因此我可以将文件描述符表中读取的地址页面对齐作为UAF文件的地址。
写入操作
现在可以使用相同的步骤覆盖三个成员real_cred、cred和nsproxy,现在我们的子进程具有与init相同的所有权限和能力,包括对主机根文件系统的可见性。
结论
这对我来说非常有趣,我学到了很多。我认为这类学习挑战很棒且风险低。它们也可以与朋友一起合作完成。完整的漏洞利用代码已发布在下方,如果您对其任何部分有理解困难,请告诉我。
|
|