CorCTF2022-Corjail

CorCTF2022-Corjail
环境搭建参考:https://www.52pojie.cn/thread-1849189-1-1.html
学习链接参考:https://syst3mfailure.io/corjail/
这道题目环境弄起来还挺恶心的,搭环境废了我大半天,最关键的是我的 ubuntu22 里的 gcc 版本比较高,导致这道内核题不支持最新的线程库,gcc 降版本也不好使,然后只好换到 ubuntu20。同时还要吐槽一点的是,这道题原理其实还是比较容易清晰易懂的。但是!,实践起来才知道堆喷有多恶心,堆块老是命不中(从开始到打通花了我一天半),通过这道题可以从原作者 D3vil 学到很多关于堆喷的技巧,这一点我也会在后面进行说明。
前置知识
先来对这道题用到的知识简单概括总结
tty_struct
每打开一个 /dev/ptmx
文件,都会生成一个 tty_file_private(kmalloc-32) 和 tty_struct(kmalloc-1024) 结构体。
1 | /* Each of a tty's open files has private_data pointing to tty_file_private */ |
一个是 magic 这里保存的是一个魔数 0x5401,可以用该数来方便判断当前堆块是否是一个 tty_struct 结构体,值的注意的是它的 tty_operations 字段,里面包括了对设备文件操作所对应的函数表,我们可以通过劫持这个字段来完成控制流劫持。
1 | struct tty_struct { |
tty_operations 结构体内容如下,可以看到里面定义了很多函数指针。
1 | struct tty_operations { |
1 | close ==> pty_close |
seq_operations
可以达到泄露内核地址,还有劫持内核执行流的作用。
在 打开一个 stat 文件时(如 /proc/self/stat
)便会在内核空间中分配一个 seq_operations 结构体,该结构体定义于 /include/linux/seq_file.h
当中,只定义了四个函数指针,如下:
参考链接:https://elixir.bootlin.com/linux/v4.19.300/source/include/linux/seq_file.h#L32
1 | struct seq_operations { |
当 read 一个 stat 文件时,内核会调用其 proc_ops 的 proc_read_iter
指针,其默认值为 seq_read()
函数,定义于 fs/seq_file.c
中:
参考链接:https://elixir.bootlin.com/linux/v4.4.298/source/fs/seq_file.c
1 | ssize_t seq_read(struct file *file, char __user *buf, size_t size, loff_t *ppos) |
即其会调用 seq_operations 中的 start 函数指针,那么我们只需要控制 seq_operations->start 后再读取对应 stat 文件便能控制内核执行流
user_key_payload
知识点较多,参考这里:a3’s blog
简单理解,就是这个结构体我们可以用来进行内核堆占位,用来堆喷和实现越界读都是不错的选择(我们这道题的做法)。
1 | struct callback_head { |
1 | struct user_key_payload { |
sys_poll
1 |
|
fds
:指向struct pollfd
数组的指针,数组中包含要监视的文件描述符和关注的事件。nfds
:fds
数组中的元素个数。timeout
:超时时间,单位是毫秒;如果设置为负数,表示无限等待;如果设置为0,则立即返回而不等待。
linux内核为用户态进程提供了一组IO相关的系统调用: select/poll/epoll, 这三个系统调用功能类似, 在使用方法和性能等方面存在一些差异。使用它们, 用户态的进程可以”监控”自己感兴趣的文件描述符, 当这些文件描述符的状态发生改变时, 比如可读或者可写了, 内核会通知进程去处理, 这里的文件描述符可以是socket, 设备文件, 管道等.
相关结构体如下:
1 | struct pollfd { |
其中 poll_list 由三个字段组成,其中 next 指向下一个 poll_list 结构体(伪造该字段可以实现任意地址释放),其中 len 字段为 entries 的数量,每一个 entries 条目大小占 8 字节(即 pollfd 结构体大小)。其中 entries 中具体条目内容由用户指定,即 fd,events,revents。
当调用一个 poll 系统调用时,do_sys_poll() 就会被内核调用,该函数负责完成:分配堆块,并完成我们传入的 pollfd 结构体内容的复制操作。
1 |
|
从上面的代码可以看到,该函数支持快速分配,起初会从栈上分配一块空间用来存放 poll_list 结构体,若不够用则才会触发下面 kmalloc 操作(慢速分配)。可以发现 stack_pps 的大小为 256 字节,其中 poll_list 结构体头部占 0x10 字节,entries 从 0x10 处开始分配,则我们可以计算得到我们传入 30 == (256-16)/8 个大小的 pollfd 时才会用光用于快速分配得到的 256 字节。
同理如果需要再申请一页大小的 poll_list 我们传入的 pollfd 个数为 510 == (4096-16)/8 POLLFD_PER_PAGE 。
也就是说当我们传入 540 个 pollfd时,理论上才是从内存真正分配了一页,理解了这一点,相信这道题从理解程度来讲你已经成功了一半,就能理解后面 exp 中 alloc_poll_list 板子中的代码。
1 | /* nfds need modify :) */ |
再进一步,如果我们传入 30 + 510 + 1 个 pollfd 呢,此时剩下那一个多出来一页的长度了,所以会再次调用 kmalloc-32 (0x8 + poll_list header),申请一个 0x20 大小的堆块,结合原作者的图来理解。
现在我们应该对这个结构体的理解清晰多了。
1 | void init_fd(int i){ |
环境说明
题目链接:https://2022.cor.team/challs
packfile.sh
1 | gcc -pthread -static -masm=intel ./exp.c -o exploit -lkeyutils |
mount.bash
1 | ### mount.bash |
umount.bash
1 | ### umount.bash |
静态分析
漏洞作者给的很清楚,对 cormon 文件write时,触发一个字节的溢出。
漏洞利用
动态分析
这个文件列举了作者允许的一些系统调用,可以看到我们熟悉的 msg_msg 已不复存在。
Step.1 Clear all the partial slab for per CPU by seq_operations
先将 partial slab 中的 page 给清光,确保我们后面申请到的 page 都是来自 buddy system 的页。
1 | // Step.1 Clear all the partial slab for per CPU by seq_options |
Step.2 Spray user_key_payload(kmalloc-32) & poll_list(kmalloc-32)
需要强调的是这道题目没有开 MEMCG 保护。
1 | // Step.2 Spray user_key_payload(kmalloc-32) & poll_list(kmalloc-32) |
注意事项:这里解释一下为什么要有 bind_core(randint(1, 3)); 操作,我们都知道这一步是把当前进程绑定到另外一个 cpu 执行,同理该进程的堆块也从新 cpu 处获得,由于我们 poll_list 中是创建线程去轮询,如果依旧与 cpu0 绑定,这无疑会为我们目前的 cpu0 的内存布局增加很多噪音,使我们的堆喷更加不稳定。那么问题来了这样做岂不是 poll_list 分配的堆块也来自于其他 cpu,其实很简单,我们只需要在线程执行的时候再去执行 bind_thread_core(0),这样就能减少很多噪音了,不得不说这一步在堆喷时增加稳定性带来了极大地提高。
这一步依次堆喷 user_key_payload –> poll_list –> user_key_payload,形成下面的内存布局(图片来自**d3vil ** )。其中绿色代表 poll_list 结构体,橙色代表 user_key_payload。同时这道题限制了 user_key_payload 的申请次数(随着申请大小变换)。
下图为我们调用 write(cormonfd, buf, 0x1000) 时对应的内存分布,poll_list 和 user_key_payload 都是正常的链接情况。
一字节溢出之后的情况如下,可以看到 poll_list next 已经成功指向了 user_key_payload。
注意事项:poll_list 在检测到设置的 timeout 到期后,会自动触发 kfree,这也就意味着我们下面的 user_key_payload 会被 free 掉,这样我们就获得到了一个 uaf 的堆块,我们后面可以想办法通过堆喷占位到该 UAF 堆块,覆盖其 datalen 字段为一个更大值,从而实现越界读泄露地址。
我们需要知道的是为什么在申请一个 user_key_payload 之前要通过 setxattr(“/home/user/.bashrc”, “user.x”, buf, 0x20, 0); 这一步,其实理解起来很简单,就是我们让后面零字节溢出时,poll_list(kmalloc-4096) 的 next 指针指向 user_key_payload (伪 poll_list ) 时,对应的 next 字段为 NULL。不然可能会继续触发 kfree 释放一个无效堆地址。
其实总结来说,这是 user_key_payload 中的 rcu 字段没有初始化才造成的原因。
Step.3 trigger off-by-null by write(cormon_fd, data, PAGE_SIZE)
1 | //Step.3 trigger off-by-null by write(cormon_fd, data, PAGE_SIZE) |
上一步我们已经演示了溢出 0 字节之后的效果。
Step.4 spray seq_operaions struct to fill UAF & Step.5 leak kernel_offset addr
1 | //Step.4 spray seq_operaions struct to fill UAF |
这一步中我们先用 seq_operations 堆喷 uaf 堆块,然后越界读泄露地址
这里泄露的其实就是上图中的 proc_single_show 函数地址,对应 user_key_payload 中的 data,刚好可以泄露出内核函数地址。
同时第五步我们释放所有 user_key_payload 之后(除了 uaf obj),再用 tty_file_private 进行堆占位,这个结构体前面已经讲过,它里面的第一个指针指向一个 tty_struct,我们可以通过越界读泄露该地址,然后在通过 poll_list 任意地址释放完成利用。
Step.6 leak heap_addr of tty_strut(kmalloc-1024) by tty_file_private(0x20)
1 | printf("[*] release all the seq_ops replaced by the tty_file_private\n"); |
跟上一步一样,都是在 OOB 泄露地址。
Step.7 hijack control flow by pipe_buffer
这一步操作有点多,我们慢慢来说,这也是我调试过程中耗时最多的一个阶段。一处代码没写好,可能直接导致整个堆喷策略失败,导致 panic。
1 | for (int i = 0; i < SEQ_SPRAY_NR; i++){ |
这里先释放之前分配的所有 seq_operation 结构体,其中就包括 uaf obj,然后我们堆喷 poll_list 结构体进行占位 uaf obj。下图中左侧绿色即为 poll_list 结构体。
调试界面如下(由于我这里重新跑了一下,所以可能跟前面的地址对不上):
再来看看下一步操作,
1 | key_revoke(keyid[uaf_key_id]); |
这一步实际上是完成前面中右侧的效果,即再次释放 uaf obj,通过 user_key_payload 堆喷获得,利用未初始化,将 poll_list 的 next 指针赋值为前面泄露的 tty_struct 目标地址,即可完成任意地址释放。
通过与前一张调试图来比较,可以发现同样的地址,这里 poll_list 中的 next 字段已经被我们设置为了 tty_struct 所在的地址,设置为其前面的 0x18 字节,是因为我们 user_key_payload 头部要占 0x18 个字节,所以这样我们在申请到堆块后,data 直接从 tty_struct 开始的位置进行伪造。
在看下一步,这里我们其实已经可以堆喷 kmalloc-1024 大小的 user_key_payload 获得上一步任意地址释放的堆块写 tty_struct 完成劫持程序执行流,但原作者在这一步通过 pipe_buffer 来完成利用,原因在于 tty_ops 劫持过程中存在较多的检查(后面还要从 docker 里面逃逸到宿主机),所以由于 pipe_buffer 和 tty_struct 从大小相同的 kmalloc-1024 处分配,所以这里使用 pipe_buffer 劫持执行流更为方便。
1 | // release all the tty struct for pipe_buffer |
这里我们已经堆喷 pipe_buffer 占位了我们释放的 tty_struct 结构体了。
这一步我们向 buff 写入 ROP,然后堆喷获取到 pipe_buffer 的堆块,然后写 payload 即可。
1 | printf("[*] release all the key and alloc again(kmalloc-1024) to write pipe_buffer\n"); |
The Exploit - Container Escape
由于懒得找 gadget,而且对容器逃逸了解的较少,所以这里直接把作者的链子拿过来用了。
1 | char *buff = (char *)calloc(1, 1024); |
最终效果如下:
历经将近两天的时间终于拿到了我最想看到的东西 root!成功率虽比不上原作者的,但感觉也不低。
final exp
1 |
|
- Title: CorCTF2022-Corjail
- Author: henry
- Created at : 2024-04-26 15:56:54
- Updated at : 2024-04-26 16:15:09
- Link: https://henrymartin262.github.io/2024/04/26/CorCTF2022-Corjail/
- License: This work is licensed under CC BY-NC-SA 4.0.
预览: