DEF CON CTF 2021决赛回忆

作者:alon、kira、elnx

前言

上个周末(2021年8月7日~8月9日),Katzebin战队在DEF CON CTF决赛中获得了冠军,十分有幸作为队员全程参与了比赛。感谢OOO为我们带来了这场精彩的比赛。

关于赛制、流程等信息,刘保证大佬已经写得很详细了[1],这里不再赘述,本文主要做一些揭秘和八卦爆料:P

赛前准备

相较去年而言这次准备工作做的并不多,仅在前一周的一个下午组织了一次线下做题活动,参与人数也不多。另外去年准备的各项工具今年可以继续用,包括VPN、流控器等。唯一遗憾的是没准备点Wireshark的DoS :D。

OOOPF:“Katzebin独家exp”揭秘

ooopf是最早放出的几道题之一,[1]中提到Katzebin有一份ooopf的“独家exp”,这里简单“揭秘”一下。

这道题一看题目名字就能猜到和BPF有关,运行一下可以发现这确实是一个BPF的菜单题。宏观来说,你可以安装(install)、 测试(test)、运行(eval)你的BPF规则,测试、运行的时候还需要灌入数据进去让BPF做“过滤”。这道题的最大特点是,你输入的BPF规则会被JIT起来执行。 eval和test有所不同,eval是依次应用所有的BPF规则,test只是临时跑一条。无论是test还是eval,执行完了的结果会送进0x3ce0这个函数,返回值如果是102会直接喷flag。那我们的目标就是让JIT代码运行完rax是102了。然而0x3900处是个“静态分析”,各种检查,判断目的寄存器不能是rax、立即数不能是小于等于102等等。所以题目的意思很清晰了:你需要构造BPF规则,在绕过这个静态分析的前提下操作rax。

比赛一开始,ttx很快就发现了第一个漏洞:test接收数据输入时有个off by one的问题,可以覆盖掉后面一个dword,而这恰好是BPF规则的数量。于是可以先布置好操作rax的BPF规则,然后用这个洞来绕过静态检查。要写exp必须先逆出操作rax的字节码,逆的过程中StarBugs已经一血,手速太快了……

搞完这个洞,大家猜测JIT部分应该还有其他洞,这就需要更加仔细的逆向了。G6和yz在逆的过程中,PPP在39轮打出了新的exp,eqqie抓到了流量:

from pwn import *
import sys
r = remote("10.13.37.3", 6666)
r.recvuntil(b'Welcome to ooopf')
r.recvuntil(b'\nWhat to do? 1) install, 2) eval, 3) test, 4) quit.\n> ')
r.send(b'1\n')
r.send(b'5\n171798691847\n40975\n377\n6468220748039\n4219\n')
r.recvuntil(b'What to do? 1) install, 2) eval, 3) test, 4) quit.\n> ')
r.send(b'2\n')
r.send(b'17\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4702111234474983745\n4\n')
r.recvuntil(b'Nooop!')
r.recvuntil(b'\n')
r.interactive()

稍加分析发现是通过JIT后的代码写栈跳到喷flag的函数上。这个洞实际上有点诡异,eval过程中执行用户输入的规则时根本没判断静态检测的返回值……

1.png

而所谓的“Katzebin独家exp”就是第三个洞了。alon注意到在处理返回值的函数里还有其他几个怪功能:

2.png

其中101也是被静态检测禁止的,而那个token更是不知所云。仔细看了下,test中会判断0x80A0的一个dword,如果是0才会进入是“strict mode”,

3.png

有了这个参数才会对立即数进行检测(eval没有处理这个全局变量)。

4.png

但正常代码里完全没有和这个全局变量相关的逻辑,而你想要返回101正常情况下一定会被静态检测防住,死循环了。这时不得不想起程序初始化阶段塞了一个规则进去,这个规则是完全不会被检查的。在执行这段这个规则前还有一些拼接操作:

5.png

显然这是把token的内容拼到了前面,可以看到偏移60的地方是我们的输入。G6把JIT后的代码dump了出来:

6.png

上面的循环显然是在做memmem,搜索THETOKEN,搜到之后偏移加8,开始一个8x6的比较,也就是和48字节的真token比,如果完全一致会返回101(输出这个🤩emoji)。这里的逻辑就类似一个后门了,只要你token匹配就能关掉test里的strict mode,但我们是拿不到正常token的,而且token似乎每轮都会变。

alon指出在memmem的循环中,offset是当作u32计算的,然而到了后面却只作为u8参与计算(各种LOBYTE)。那么只要加到0x100,就能回到开头,让token自己和自己比了,然后我们就可以绕过test里的静态检查。

6点左右,alon写完这个洞。由于这题流量非常容易重放,这个exp只用来打之前两个洞打不了的队伍的stealth port,并请bigtang监控流量,看是否有其他队伍打出这个洞。结果一直到题目下线都没有其他人在normal port打出这个洞。

OOOWS:从零开始虚拟化

这个系列的题目一看名字也能猜出是模仿AWS,这一系列题目众多。

Baby’s First

首先是从flag baby看起,作者实现了一套调用kvm api的虚拟机,支持多种设备如disk、serial、vga以及本题的noflag。对于基本的逻辑理解清楚后,kira去看noflag逻辑找洞,bird继续精逆vmm逻辑,hzshang和lan开始处理实模式汇编解决交互问题。漏洞很快找到,由于shell里printf的奇特行为且过滤flag名为单字节对比,导致IO操作给两字节就可以绕过过滤逻辑。但是即使找到漏洞,想要正常交互也不是件容易的事情,还需要逆向bios找到磁盘加载的逻辑。一番折腾后搞定,可惜还是慢shellphish一步,拿到了本题的二血。

完成exploit之后,自动化攻击脚本仍然没那么容易,本系列题需要和web界面交互,还用了socketio,于是赶紧呼唤了一位web大佬ilfrank来写自动化脚本,在写脚本的同时,把ip分组安排大家手打。可能是手打的分比较香,这题得分最后竟然反超shellphish,拿下了第一。

这题完事后,第一个shift已经结束,一些人去休息了,剩下还能看题的人分成了三个线程同步看不同的ooows题(p92021、ogx、broadcooom)。

逆向+编程大赛

一些人开搞p92021,有了之前的经验,大家直奔p9fs这个device而去。开始都觉得这是系列第二题,肯定不会太难(啪啪打脸),估计文件系统随便搞个路径穿越啥的就搞定了。

出题人真的很硬核,在virtio之上实现了一套9pfs。由于缺乏交互代码(本题需要实现virtio的交互),且题目是C++写的,逆得不清楚,导致大家认为文件操作没有验证路径,可以穿越。dmxcsnsbh用gdb直接写内存的方式绕开virtio开始调试,nagi开始着手解决virtio交互问题,主要是去改(抄)uboot代码。休息回来的kira加入处理交互的队伍,交互还没搞定,调试那里传来噩耗,路径并不能穿越,类里有个函数会检查。大伙陷入僵局,只得继续逆向找洞,xhj也加入逆向大军。

卡了很久,最终dmxcsnsbh发现一处UAF。继续使用之前的方式调试写exp,随后没多久virtio的发包也搞定了。不过最终exploit的编写过程很是坎坷,先是写了disk IO解决shellcode长度的限制。然后实现flag读取,但我们还不会获取virtio的返回值。nagi和lan逆向发现是在另一个virtqueue中处理,kira写代码实现了收包。一切就绪后,没想到远程竟然失败了!hzshang解决了docker中的调试问题,发现是因为open的flag设置为2在docker里会permissiondenied。

小坑不断,最后终于生成了一份可用payload,在Starbugs和Tea Delivers之后开始拿分。

ogx几乎全程仅由hen和dydxh两人搞定,无敌。

broadcom由yz bigtang bird homura等同学负责,听说他们整了一套很牛的exp,最后读flag还有一步单表替换,不过还是被Nu1L的好兄弟重放了。

Intel手册读书会

第二个shift结束,p9原班人马投入hyper-o这道题。elnx吃完午饭介入,稍微看了下,这题比之前的题是有过之而无不及,出题人自己撸了一套VT跑虚拟机的代码。所幸整个驱动都有符号,减少了一些逆向时间,不过vmx指令看起来还是不舒服,nagi很快找到一份插件(https://github.com/synacktiv/vmx_intrinsics/blob/master/vmx_intrinsics.py)。之后就是逆向、讨论。

结合intel手册等书籍,再参考https://github.com/gamozolabs/chocolate_milk,可以很快分析出基本的逻辑:init负责读入shellcode,然后shellcode通过ioctl发给内核驱动,内核驱动会在一个VT虚拟环境中跑shellcode。AAA的好兄弟们负责逆向页表相关的逻辑,hzshang先准备好了shellcode encoder来处理终端下有些不可见字符输入不了的问题。然后发现之前的代码切保护模式失败了!elnx也加入调试,发现问题出在初始的CS基址上,于是hzshang提出复制自身到0xf000处,然后就可以正常进入保护模式了。

由于主办方提醒让大家focus在驱动上,说init只是示例,现场的大家开始发散思维,甚至开始脑洞起多个虚拟机是否存在race的问题。AAA众人竟然开始看起了vmwrite中各个写入的bit是什么含义。

7.png

北京时间23点30左右,就在这山穷水尽的时候,h0twinter指出ept_map_range存在off by one的问题,也就是初始化虚拟机页表的时候多映射了一个页,导致虚拟机内可以直接覆盖掉eptp,然后就是任意物理内存访问了,之后搜内存就完事了。可惜时间太紧张,elnx和kira手忙脚乱地调试,各种小坑,没做完exploit(赛后整理到了https://gist.github.com/elnx/3c568db706bf1c27299ac53907ff6c62),而AAA的同学们成功重放了StarBugs的流量。流量里可以看到他们用\x16轻松解决了终端输入的问题,佩服。

没打多久,进入二阶段,回归了上传虚拟磁盘的形式。这下flag可不是在initrd里了,想直接在1GB的内存里搜flag的内容感觉有点扯?众人讨论是否要写内核shellcode,然而内核shellcode往哪写呢?写完了你还不能让它乱崩,毕竟是宿主机,内核要是panic了flag都没处拿了。在大家的讨论中,StarBugs再次一血,再没过多久比赛结束。

一些八卦

  1. 写这“回忆”还挺麻烦的,由于线下即时交流居多,时间线都不好整理也记不太清,各位大佬凑活看吧。但有的东西一直没人写的话那感觉还是挺可惜的。文章显得头重脚轻,希望有其他队友能补充下其他题目吧 :D
  2. 做ogx的过程中dydxh遇到了sudo密码不对的问题,重启解决了
  3. hyper-o二阶段中,dotsu发现了别人访问/static/flag.txt的流量,大家震惊之余找到了相对应的payload。payload中首先是逐64K搜索_stext,然后拷贝内核shellcode的代码,覆盖的目标是sys_ioctl的ret。内核shellcode的逻辑是从栈上找到用户态的返回地址,然后拷贝用户态shellcode回去……这部分代码应该还不完整,应该还有另一段代码做了wrmsr相关处理代码的hook?总而言之,StarBugs能短时间内就写出这样漂亮的利用,实在佩服
  4. Tea Deliverers赛前制定了patch规范,由BrieflyX大佬指挥[2]。而Katzebin本次比赛除了ooopf由pyro负责patch,ooows-flag-baby由kira负责patch,其他赛题的patch均由xhj完成,orz
  5. ooopf这道题让人想起去年的keml和bdooos……不知道是不是同一个人出的
  6. 其实曾经有个叫Cat Chamber的队伍,可惜转瞬即逝
  7. 据说整场比赛下来玩KoH的人最多,房间里椅子都不够用了,得坐地上
  8. 因为题目有新进展,有些人睡觉睡一半被强行唤醒继续做题。septyem在比赛中间又会过来提醒大家要去休息了
  9. 有些队友周一要上班,没能一起迎接胜利

致谢

感谢给大家安排各种早饭、中饭、晚饭、夜宵以及零食、饮料的同事们。

8.png

再次感谢OOO为我们带来了这场精彩的比赛。

参考

[1] https://iromise.com/2021/08/11/DEF-CON-CTF-29-Final/

[2] http://brieflyx.me/2021/dc29-memo/

附录

BDOOOS:迟到的回忆

一份elnx去年写的关于bdooos的完整记录,一直没有发出来,这次顺便一起发出来。

https://elnx.github.io/bdooos/

updated at 2021-08-14