【CTF.0x0A】D^ 3CTF2025 d3kheap2、d3kshrm 出题手记
本文最后更新于:2025年7月3日 早上
《火星救援之企鹅男孩与美人a3战士与非预期解》
0x00. 一切开始之前
众所周知笔者已经从 Xidian University 本科毕业将近两年了,目前也暂时不在这个学校继续就读,因此按理来说由三大本科 CTF 战队 L-team、Vidar-Team、CNSS 组成的联合战队 El3ctronic 所举办的名为 D^3CTF 的国际赛事的出题的担子首当其冲应该不是笔者这种毕业良久的老登要考虑的事情,因此你可以看到虽然笔者在 2022、2023 年都和 大企鹅鹅 一起出了题但是在 2024 年我俩作为已经毕业的老登是都没有出题的,2024 年由学弟学妹们( 真的有学妹学 Pwn 吗 )出的 Pwn 题好像也没有出什么大状况因此一开始笔者也没有太关注今年的 D^3CTF 到底办得怎么样了毕竟 也是时候到后辈们独当一面的时候了 ,直到比赛开始的大概十天之前笔者突然想到 “今年还有个比赛呢,问问学弟们准备得咋样了” ,最后才知道三个战队的学弟学妹们( 真的有学妹学 Pwn 吗 ) 今年一道 Pwn 题都弄不出来…
笔者寻思今年要真没 Pwn 题那这 b 比赛不是直接炸了,于是赶紧大手一挥开了一堆新建文件夹,考虑到笔者还出题的那些年基本上平均一个战队出两道题,然后再加上今年老企鹅也愿意抽空出一道题,于是笔者做了个出五道题的计划,可惜最后由于时间实在还是过于紧张了再加上确实没有准备什么过于惊艳的 idea ( 谁能预料到今年还得👴和🐧这种老登来救场啊 )于是最后只完成了两道题,不过好在今年的比赛时间只有 24h,再加上🐧弄的一道题总共有 3 道题也算是勉强能够撑起一个 24h 的比赛了,万幸最后整个比赛还是正常地举办下去了没有出现因为缺题目导致的特殊状况严重后果(大嘘
至于比赛举办过程中出现的一些其他突发情况那就不是👴能够管辖的范围了,毕竟作为现任队员的小东西们总归是需要自己去独当一面来维持战队的这块招牌的,至少在事态没闹太大之前笔者认为自己作为毕业的老登是不适宜直接参与决策的,
再说了都毕业这么久了还天天干政那👴不成慈禧了,不过仔细很多事情本来也未必就能有一个体面的收场
其他的就暂且不论了,虽然从最后的解题数量来看没有被爆得太烂,但是从题目质量上来看相比起 2023 年的那道题而言笔者自己对今年的题目其实是不太满意的( 虽然在最近的比赛当中你或许很难找到这个程度的 CTF 题目了 ),不过想到今年的出题时间比较限制,笔者觉得这或许也是无可奈何的事情,但无论如何,笔者还是希望你能喜欢今年的这两道融合了笔者数个日夜心血的题目:)
你以为批话环节到这就结束了?too young too simple!后面针对每道不同的题目还会有不同的批话大放送(错乱
以及由于笔者先写的英文博客,所以本文主要是由英文博客翻译过来, 在语序上可能会有一些大家喜闻乐见(?)的翻译腔 ,希望不要介意:)
0x01. D3KHEAP2 | 6 Solves
“Once I was seven years old my arttnba3 told me”
“go make yourself some d3kheap or you’ll be lonely”
“Soon I’ll be 60 years old will I think the kernel pwn is cold”
“Or will I have a lot of baby heap who can sign me in”
Copyright(c) 2025 <ディーキューブ・シーティーエフ カーネル Pwn 製作委員会>
Author: arttnba3 @ L-team x El3ctronic x D^3CTF
You can get the attachment at https://github.com/arttnba3/D3CTF2025_d3kheap2.
Introduction
这道题目和 2022 年的 d3kheap 一样不需要花太多精力进行逆向,题目给了一个内核模块 d3kheap2.ko
,其只有一个有用的核心函数 d3kheap2_ioctl()
,核心功能只有从独立的 kmem_cache d3kheap2_cache
当中进行对象分配,漏洞点在于对内核对象的引用计数的初始化错误导致能够将一个对象释放两次:
1 |
|
老朋友们或许注意到了今年这道题从题目架构设计上似乎和 d3kheap 高度类似,因为今年的重点在于更先进的 exploitation 技术,当年的题目在于对通用 kmem_cache
上的 double free 的利用通法,而今年则关注于对 任意 kmem_cache 上的 double free 的高成功率的利用通法
Exploitation
由于漏洞对象在独立的 kmem_cache
当中,我们很容易想到应当使用 cross-cache attack
:
- 首先堆喷分配大量题目对象,再将其全部释放,从而填满题目的
kmem_cache
以将部分 SLUB pages 释放回 buddy system - 在另一个
kmem_cache
上进行大量分配取回这些页面,这里我们选择system V IPC
作为第一阶段的漏洞利用对象 - 将垂悬指针进行释放以在
msg_msgseg
上构造 UAF,之后重新分配该对象回来以使得两个msg_msgseg
指向同一个内核对象 - 将其中一个释放并重新分配为
pipe_buffer
,因为其 GFP flag 与msg_msgseg
相同,都从kmalloc-cg
分配(当CONFIG_SLAB_BUCKETS
未启用时) - 通过
msg_msgseg
修改pipe_buffer
以在内核空间获取任意内存读写的权能
下面的便是我们最终的利用程序,在超过 1024 次的本地测试当中其最终的利用成功率约为 99.32%
,在笔者看来应当已经足够稳定了
需要注意的是在打远程的时候你可以通过使用
musl-gcc
编译来缩减二进制文件的大小,或是手写汇编代码如果你比较闲的话
1 |
|
What’s more…
题目的介绍是由笔者曾经最喜欢的一首名为 7years 的歌的歌词修改而来(虽然当时笔者是 15 years old),因为这常让我回忆起曾经的少年时期,而笔者希望这也能让大家想起自从 D^3CTF 2022 的 d3kheap 以来 Linux kernel exploitation 的发展的步子所迈之大( 虽然这两件事好像没什么关联 ),有了惊艳的 cross-cache attack 我们近乎能够通过将 SLUB page 从一个 kmem_cache
迁移到另一个的方式来利用所有的 UAF 与 DF 漏洞,这也是为什么我将这道题目命名为 d3kheap2
的缘故: Solution upgration from limited one for d3kheap’s easy double free to general one for d3kheap2’s lunatic double free
尽管这道题目的核心技术在 2025 年并不是一个非常新的事物(甚至在 2022 年就已经 有人提出,虽然笔者不知道这是不是最早的),但在过去几年的 CTF 当中 cross-cache attack 并不常见,这也是为什么我选择在今年的 D^3CTF 当中展示这个技术,因为在 2024 年我比较忙,而在 2023 年我又展示了一些 别的东西 (在一年后被一个名为 胡嘉懿 的参加过 D^3CTF 2023 的学生 抄袭 并 偷去发了 BlackHat USA 2024 ,敢在 BlackHat 上直接展示一个和👴博客几乎一模一样的东西确实还真是 挺不要脸的 )
另一个我选择 cross-cache attack 的原因是 我确实没有太多时间来完成这些题目 ,由于我已经从本科毕业了,我并没有太关注于我的后辈们今年准备 D^3CTF 的情况,直到 比赛开始的大概 10 天前 才知道今年几乎还没有 pwn 题,因此我不得不在脑子里几乎没有什么新的研究成果的情况下冲刺准备今年的 Pwn 题以确保比赛能像往年一样正常举办, 非常抱歉今年笔者未能带来和 2023 年的 d3kcache 一样炫酷的玩意 ,但幸运的是我仍然给你们准备了一些特殊的礼物,那就是我玩弄 msg_msg
与 pipe_buffer
的小技巧: tricky but useful gadgets you may be love in
以及如果你足够细心你或许会发现这道题没有像 d3kshrm 那样开启 CONFIG_SLAB_BUCKETS
配置(一种对抗堆喷的缓解措施),虽然通过全量堆喷来代替精确对象分配以绕过并不难,但考虑到今年的 D^3CTF 只有 24h,我还是希望这道题能够让选手们在 “Pwn” 这一分类比较简单地能签上到,就像 D^3CTF 2022 的 d3kheap
的介绍一样,因此这道题在最初设计时并不是一道非常难题目
对于最后的解题结果,绝大部分选手都使用了预期的 cross-cache attack,笔者非常开心能够看到参与比赛的 CTFer 们大都已经掌握了这项能够在近乎任意堆漏洞上进行利用的高级技术,而随着 cross-cache attack 在近年已经被广泛使用,我确信这将、或已经成为了如今 Linux kernel exploitation 的基础步骤或是标准入口;非常遗憾的是我 忘了 开启 CONFIG_MEMCG
以分开 GFP_KERNEL
和 GFP_KERNEL_ACCOUNT
对象,你们可以看到我使用了非常复杂的多阶段利用技术操纵 msg_msg
与 pipe_buffer
,但有的选手就可以直接用 sk_buff
来读写 UAF 的 pipe_buffer
;另一个遗憾的是取得一血的 We_0wn_y0u 战队在 D^3CTF 2025 当中仅做了 d3kheap2 这一道题目就走了,因此我并不知道他们的具体解法
现在让我们来看看那些 最先进的学术技术 如 Dirty PageTable( SLUBStick ,我不知道为什么有两个名字,我也不确定他们的作者是否相同 ,因为 Dirty Pagetable 的原博客似乎被移除了,我暂时也没有足够的时间去做区分)与 DirtyPage (作者也叫他 Page Spray), 其基础技术都是 cross-cache attack :他们是否强大到足以应用在这道题目上?结果似乎是 没那么容易 ,因为他们都是为不同的漏洞范式而设计的
- 对于 SLUBStick 而言,我们需要额外的几次进行 UAF 写 的权能,这会需要我们构造复杂的多阶段的 cross-cache 页释放与重取回,在提升了构造利用的难度的同时也降低了可用性与稳定性
- DirtyPage 说其通过迷惑在一个 SLUB 上的对象计数(参见
Figure 1: Page Spray Exploit Model for Double Free.
)“走了更远的一步” ,但覆写一个没有任何功能的对象是 毫无意义的 ,在笔者看来这或许更适用于攻击有着特定功能的内核对象(例如file
或是pipe_buffer
?),但若是目标对象缺少为后续攻击阶段的足够的供能,这样的利用或许无法被应用
因此, 纯粹的 cross-cache attack 在我看来更适用于 d3kheap2 ,但无论如何感谢他们开发了如此强大的利用技术并拓宽了我们的视野到另一个层面
另一个点便是如同 Pspray 这样的通过计时侧信道攻击来预测分配 SLUB 页面的辅助技术对于不局限于 d3kheap2
的通用内核堆利用而言似乎没有太多作用,一个核心的原因便是随着像 CONFIG_RANDOM_KMALLOC_CACHES
这样的缓解措施的出现在内核主线使得一个新的 SLUB page 是否被分配对于我们而言不再那么重要,因为我们的对象总会从不同的独立的池中随机分配,进行大量的堆喷并进行近似估计似乎是唯一的可行方法,尽管在 d3kheap2
没有开启这一缓解措施,笔者仍然想讲一讲与真实世界利用有关的东西,希望大家不要介意:)
尽管关于 Linux kernel exploitation 笔者还有很多想说的话,但似乎写到这里的时候文章已经太长了,那么就让我们就此打住吧,无论如何我要感谢每一位参加了这个 CTF 并尝试进行解题的选手,无论你们是否获得了 flag
0x02. D3KSHRM | 1 Solve
You know what? Sharing is always a good moral quality. That’s the reason why I’m going to share some of my precious memories with all of you!
Copyright(c) 2025 <ディーキューブ・シーティーエフ カーネル Pwn 製作委員会>
Author: arttnba3 @ L-team x El3ctronic x D^3CTF
You can get the original attachment at https://github.com/arttnba3/D3CTF2025_d3kshrm.
Introduction
这道题目提供了一个名为 d3kshrm.ko
的内核模块,其为用户提供了创建共享内存的功能,通过 ioctl()
我们有着如下权能:
- 创建一个特定大小的新的共享内存
- 绑定到一个现有的共享内存上
- 与当前共享内存解绑
- 删除一个现有的共享内存
而要访问这块内存,我们可以在绑定之后去 mmap()
对应的文件描述符,而这也是漏洞所在的地方,由于缺乏对 d3kshrm::pages
的恰当的范围检查,攻击者可以将 d3kshrm::pages
的相邻的 8 字节作为一个 struct page
指针并映射到用户地址空间中:
1 |
|
Exploitation
由于 d3kshrm::pages
会从独立的 kmem_cache
中进行分配,我们必须使用页级堆风,水技术来操纵页级内存以尝试去映射题目功能以外的页指针,因为由于引用计数的存在我们无法通过直接映射一个页面两次的方式来直接进行页级的双重释放,因此我们可用的利用策略是将原本只有只读权限的页面映射到用户空间,这不禁让我们想起 CVE-2023-2008 ,其同样也是利用了越界页映射来完成类似 Dirty Pipe 的攻击,因此下面是我们的利用策略:
- 使用页级堆风水技术重排布页级内存以让题目的独立
kmem_cache
的 SLUB page 被放到目标对象的两张 SLUB page 中间,这里我们选择pipe_buffer
作为我们的目标对象,因为其结构体开头有一个struct page
指针,这让我们能够进行 oob mapping - 打开一个只读文件并使用
spice()
系统调用来将其第一个页面放到pipe_buffer
当中 - 利用漏洞来进行 oob mapping 以将只有只读权限的页以可读写权限映射到用户空间当中,由此我们便能修改只读文件
我最终选择 /sbin/poweroff
(其链接到 busybox
上)作为我们的目标文件,因为 /etc/init.d/rcS
的最后一行是以 root 权限执行 /sbin/poweroff
,这让我们能够以 root 权限执行任意代码,最终的利用程序如下,有着将近 84.63%
的成功率(在经过超过 2048 次的本地自动测试得出的结果),且我确信还有能将其优化到 95%+
的空间,因为我并没有采用更加复杂的高级页风水技巧:
1 |
|
Unintended Solution
非常抱歉我并未将文件系统配置好,从而导致了非预期解的出现,在开始讲解之前我要感谢最初发现这个问题的来自 W&M 的 Qanux 选手,说实话,这个漏洞的出现是我将文件系统配置得太常规了
一个最小化的能够 在不使用题目模块 的情况下 稳定 触发非预期解的概念验证函数如下 ( prepare_pgv_system()
和 alloc_page()
等函数参见前面的 exp.c
):
1 |
|
当我们执行这一概念验证时,我们注意到我们的进程突然停止了,随后我们没有原因地得到了一个 root shell:
为什么? 为了弄清楚在这个过程当中所发生的,让我们简单看看这个 poc,其只是简单地通过 packet socket 的 setsockopt()
系统调用 进行了内存分配 ,我们都知道当一个进程分配并占用了大量内存的情况下,系统会没有足够的空闲内存可用,因此 OOM Killer 会被唤醒以杀死进程来回收内存
哪个进程会被杀掉?我们都知道在这个环境中用户态只有少数几个进程,因此被杀者只会从 rcS
、 sh
、exploit
当中产生,但是谁是那个不幸者呢?好吧, OOM Killer 通过包括资源占用的多种因素来确定受害者,而我们可以通过 /proc/[pid]/oom_score
来判断,分数越高越容易被杀,一个简单的测试结果如下(使用一个简单的 C 函数读取):
我们可以看到 rcS
与 sh
有着同样的 OOM 分数,其中之一会成为那个不幸者,因为我们的 exploit
的分数更低,而由于 rcS
是 root 权限运行而 sh
不是,似乎杀掉 sh
是有意义的?答案是 是,但不仅仅是是 ,让我们看看真正发生了什么:
他们全都被杀掉来回收内存了! 但是,为什么?一个非常重要的原因便是在杀掉一个特定进程之后,仍有可能仍旧没有足够的空闲内存能够满足分配请求,这可能是由于异步的内存回收、内存碎片等原因,更重要的是 我们仍在继续进行内存分配 ,因此 OOM killer 会被多次唤醒(甚至在一次分配过程当中),按分数与权限依次杀掉 sh
与 rcS
,并最终杀掉 exploit
若是所有这些进程都被杀掉会发生什么? 由于 ttyS0
此时被闲置,init
将重新取得控制权并检测到其是闲置的,注意到我们的初始系统使用的是 busybox-init
,因为我们可以看到 /sbin/init
是 busybox
的一个符号链接, busybox-init
将会使用 /etc/inittab
作为其配置,让我们看看笔者在很久之前都参照 official example from the busybox 写了点什么东西:
1 |
|
让我们看看值为 /bin/ash
的 ::askfirst:
项,这是什么意思且在什么时候会被执行? 当 TTY 上没有进程运行时,由该选项指定的进程会被 /sbin/init 以 root 权限启动 (就像 getty )
现在我们知道为什么我们能够获得一个 root shell 了:在初始化时 /etc/init.d/rcS
运行在 ttyS0
上并 spawn 了一个用户态 shell 供我们交互,当我们在内核空间进行无限制的内存分配并占用几乎所有的空闲内存时, OOM Killer 会被唤醒并杀掉这些用户态进程,由于此时没有进程在 ttyS0
上运行, 由 ::askfirst: 选项指定的 /bin/ash 将会被执行,给了我们一个 root shell
这也是来自 W&M 的 Qanux 选手在比赛中如何巧合地解出 d3kshrm
的:他只是使用 d3kshrm.ko
的功能进行了内存利用,而由于我的错误配置与错误设计,题目预计的可分配内存比虚拟机的内存大得多,因此 OOM killer 被多次唤醒并杀掉了 init
以外的所有用户态进程,在这之后 ttyS0
被闲置因此 busybox-init
启动了一个 root shell
那么现在又来了另一个问题: 我们是否能够直接在用户空间进行内存分配,而非利用内核的内存分配 API? 答案是 否定的 ,一个非常重要的原因是如果我们的进程直接分配了大量的内存(例如进行大量的 malloc()
来扩展堆段), 我们的 OOM score 将会同步快速增长,且我们的 exploit 进程往往会是第一个被杀掉的 ,由于我们被杀掉了,内存分配停止了,因此内核不再需要唤醒 OOM Killer 来杀掉其他内存
在我在比赛过程中得到选手的反馈图片时,我非常迅速地就意识到这必定是 OOM Killer 引起的,但我没有预计到的是包括 rcS
在内的所有进程都被杀掉了,因为这在我以往出过的 CTF 题目中都没有出现过,我原本的预期是 kernel 将会由于 OOM 而 panic,但结果告诉我 kernel 并不总是 panic (哈哈,kernel 也怕死吗?),据选手所言其给出的非预期解的成功率是至少 30%
,但我写的 POC 的成功率超过 99%
,我认为主要原因是使用的 API 不同,由于 packet_set_ring()
使用 vzalloc_nprof()
,其并不向内核请求物理连续的内存区段(而仅是虚拟地址连续),这意味着内存分配可以被从一个高阶分割为数个低阶,但 d3kshrm.ko
中的函数直接调用了 alloc_pages()
来分配高阶内存,因此内核会更容易 panic 因为我们或许无法回收所需的连续的高阶物理内存
我最终如何修复这个漏洞?我创建了该题目的一个复仇版本,仅修改了 /etc/inittab
的 ::askfirst:
从 /bin/ash
变为 /sbin/poweroff
来临时修复这个非预期漏洞,但我认为将其变为 login
或许是更好的选择?无论如何这教导了我一堂课: 一个完美的环境并不总是最适合的 ,且我应当 检查环境当中的每样事物
What’s more…
本题的介绍来自于我非常喜欢的一个由 Halo Top 设计的广告 ,尽管这个视频或许只是为了乐趣而创造的,但这也给了我一些言语无法表达的特别感受,因此我选择其作为题目描述的基础并修改了一部分以给你们一些无意义的句子,就像 flag 所言:)
我创造这个题目的最初的灵感来自于 CVE-2023-2008 ,其同样是一个 OOB 内存映射的漏洞,因此实话实说这个题目并不如我所预期的那样有难度且有创造性,非常抱歉我虽然一直想给你们展示一些炫酷的东西但这一次并没有展示足够库的玩意
另一个我选择修改现有漏洞的原因是 我确实没有太多时间来完成这些题目 ,由于我已经从本科毕业了,我并没有太关注于我的后辈们今年准备 D^3CTF 的情况,直到 比赛开始的大概 10 天前 才知道今年几乎还没有 pwn 题,因此我不得不在脑子里几乎没有什么新的研究成果的情况下冲刺准备今年的 Pwn 题以确保比赛能像往年一样正常举办, 非常抱歉今年笔者未能带来和 2023 年的 d3kcache 一样炫酷的玩意
而如果你足够注意中国内核模块,你会注意到我在计算 vm_area
的引用计数上写了另一个非预期漏洞: 我忘了写 vm_open() 以添加引用计数,但仍记得写 vm_close() 以减少引用计数! 这迷惑了不少选手并让他们浪费了很多时间尝试利用这个漏洞,因为实际上其并不好利用,因为页面很难被同时用作用户映射页面与 SLUB 页面(但如果你足够感兴趣,或许你可以看看 CVE-2024-0582 ,其情况与之相似,但我不确定这对 d3kshrm
是否同样有用,所以祝你好运),我非常抱歉因为这道题出得太赶了我没有仔细检查
从整场比赛而言,仅有来自 MNGA 战队的 Tplus
选手成功以预期解法解出了这道题,让我们祝贺这位唯一在比赛期间以预期解解出这道题的选手! 而来自 W&M 的 Qanux 选手在比赛结束后也同样以预期解法解出了这道题目(因为他没想到还有个 -revenge
版本从而在用非预期解得分后便出去吃大餐了),无论如何我认为我们都应当为他们鼓掌与庆贺
另一个有趣的点是你们或许会忽视 在 kmem_cache 被创建时便会分配一份新的 SLUB 页面 ,这意味着我们的堆风水应当关注于 下一个新分配的 SLUB 页面 ,我认为这是 Tplus
选手与 Qanux
选手所自行编写的预期解法的成功率较低的缘故:他们关注于第一份 SLUB,而我的官方解法关注于第二份 SLUB,因此我的成功利用页级堆风水的概率超过 80%
且在打远程时无需爆破
尽管关于 Linux kernel exploitation 笔者还有很多想说的话,但似乎写到这里的时候文章已经太长了,那么就让我们就此打住吧,无论如何我要感谢每一位参加了这个 CTF 并尝试进行解题的选手,无论你们是否获得了 flag