写在最前面
本来打完京麒 CTF 后就和自己说今年不要再打 CTF 了,可是这次是 a3 出题!那不得不打了,想起当年学 kernel 的时候有很长的一段时间都是看他的博客学的。这次 a3 出了两道 kernel 题(听说本来是有五道的),可能是出于难度的考虑,第一道 d2kheap2 非常的简单,直接 cross cache 然后 msg_msg 和 pipe_buffer 叠起来打 dirty pipe 就行了。当时和 cnitlrt 师傅几乎同时打通这题(打法也大差不差),可是成功率不怎么高再加上比赛刚开始远程卡的一批,远程打了两个小时才打通 :(,不过我们打远程前已经有队伍一血了,所以我们最后只拿了二血。
由于题目比较简单就不把那题丢博客上了,这篇博客讲的是另外一题 kernel 即 d3shrm。做这题的时候其实挺魔幻的,比赛开始那天晚上找了很久没有找到洞然后开摆睡觉去了,没想到第二天醒来已经到中午了。吃个饭继续找洞,又看了大半天,才发现自己漏看了 d3kshrm_vm_fault 这个函数🤡。然后写 poc 去调试,结果 poc 在运行时卡死了,然后弹出 root shell 了,于是开开心心的😀把 flag 交了拿了一血然后去外面聚餐了。

去聚餐的路上和 a3 师傅说了一下我的非预期,他也表示很懵逼,不过最后还是找到了原因,然后上了 revenge 版本🤡。可是当时我还在外面聚餐,来不及回来打了 😭(虽然回来也不一定能在这么短的时间内做出来)
关键函数伪代码
好了,说了那么多的废话,该回归正题了,这里还是先给出我认为比较重要的函数的 ida 伪代码(我恢复了部分符号)
init_module
1 | __int64 __fastcall init_module(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6) |
d3kshrm_ioctl
1 | __int64 __fastcall d3kshrm_ioctl(__int64 a1, __int64 cmd, size_t a3) |
d3kshrm_release
1 | __int64 __fastcall d3kshrm_release(__int64 a1, __int64 a2) |
d3kshrm_mmap
1 | __int64 __fastcall d3kshrm_mmap(__int64 a1, _QWORD *a2) |
d3kshrm_vm_close
1 | __int64 __fastcall d3kshrm_vm_close(__int64 a1, __int64 a2) |
d3kshrm_vm_fault
1 | __int64 __fastcall d3kshrm_vm_fault(_QWORD *vmf) |
非预期
还是先说一下我的非预期,我当时是打算用喷大量的页来进行堆布局然后调试 d3kshrm_vm_fault 函数的,结果喷着喷着就 root 了😀
这里给出我当时打远程的时候的截图:

经过 a3 师傅的研究最后得出的结论(原话):
rcS被kill掉之后系统会起一个root shell
所以是大量内存分配触发oom killer杀掉了rcS
这个是内存回收成功的情况,另外一种情况是oom killer如果选择不杀的话就会导致kernel panic,这个一般是预期情况
准确的说是启动了/bin/ash,这个配置来自于/etc/inittab的askfirst
预期做法
非预期没什么意思,除了拿到 flag 和一血以外还有什么用呢😡,还是来看看预期做法吧:)
这里先给出几个结构体(手动逆的,不保证正确):
1 | struct slot{ |
驱动的功能自己简单逆一下就知道了,这里就不做解释 :)
该驱动在 init_module 函数中自定义了一个 kmem_cache(d3kshrm_cache),每个 object 的大小为 0x1000,每个 slab 占用 8 个 page,也就是说每个 slab 里面可以存放 8 个 object
1 | kmem_cache: 0xffff8e73412b3900 |
而 d3 结构体中的 page 结构体就是在这个 kmem_cache 中进行分配的

这个 page_obj 用来存储 kernel 中的 page 结构体指针,且最多存储 0x200 个,即刚好把 page_obj 所有的空间用完。驱动在向 pages 分配 page 指针前做了检测,无法溢出
1 | if ( a3 - 0x201 < 0xFFFFFFFFFFFFFE00LL ) |
漏洞出现在 d3kshrm_vm_fault 中:

当 pgoff == v1->page_count 时会出现越界索引,若 page_count 为 0x200 即 page 结构体指针刚好占满,此时 v4 获取到的就是与该 pages 相邻的结构体的前八字节的内容。如果该 pages 刚好是 d3kshrm_cache slab 的最下方 object,那我们就能够实现 cross cache 的越界索引

接下来就非常的简单了,我们可以在与该 slab 相邻的 page 上喷 pipe buffer 并使用 splice 让 pipe 的 page 指针指向一个文件映射页,那我们就能够通过越界索引实现对文件的越权写入。
这里需要注意的是 pipe_buffer 不能瞎喷,不然可能永远都喷不到:
1 | /home/ctf # cat /sys/kernel/slab/kmalloc-1k/order |
注意到 d3kshrm_cache 的 order 为 3,所以我们喷的 pipe_buffer 也应该在 order-3,这里我选择的是 kmalloc-2k

最后的问题是如何调用 d3kshrm_vm_fault,在内核驱动中 fault 函数是用来处理缺页异常的核心回调函数,负责在用户空间访问共享内存时动态建立物理映射,所以我们只需要在用户空间访问到相应偏移的虚拟地址即可控制 pgoff 的值

但 d3kshrm_mmap 函数做了严格的索引检查,我们无法直接通过 mmap 来申请出溢出的 page 的虚拟地址,后面问了下 tplus 师傅发现可以使用 mremap 来扩大我们的虚拟地址 :)
下面给出最终的 exp,由于该 exp 成功的需要 oob pages object 在 d3kshrm_cache slab 的最下方,所以成功概率只有 1/8 :(
exp
1 | // musl-gcc exp.c --static -masm=intel -lpthread -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/ -o exp |
总结
哎,圈圈,你怎么又看 CTF 了 :(
感谢 a3 师傅出的题,还是第一次见漏洞出现在 fault 函数上
听 a3 师傅说他的 exp 成功率能达到百分之 80 以上,我目前的想法是申请多个 page 指针数量等于 0x200 的 pages,然后依次越界并依次判断越界的 object 是否出于 slab 的最下方。但由于太忙没时间去验证和优化我的 exp (保研边缘人 and 考研玩家是这样的😭)
经过好几个月的深思熟虑,最后决定把学习的重心转移到软件安全和 ai 上,希望将来的我还能够做出来 pwn 题把:)