0%

L3HCTF命题记事

在我写这篇博客的时候,L3HCTF还有不足10个小时结束。这也是我第一次为一场正规的,全国及以上范围的CTF比赛命题。

当队长将pwn方向的命题管理权交给我时,我实际上是心虚的。要说pwn,我也学了两年多了,我真的是一名有水平、有实力的pwn选手吗,还是一个只能靠那些队内研究生元老大杀四方来蹭到决赛机会的CTF寄生虫呢。从目前来看,我似乎更像后者一些。每逢比赛,只有看到一些熟悉的,自己仔细分析过的赛题类型才敢去做,才敢尝试,且不一定能够尝试成功;而对于那些较为陌生的东西,则是避之不及,连查资料的时间也不愿意去花。

而当我命题结束时,我想清楚了一件事。一成不变,不愿接触新事物的选手,无论如何都无法取得真正的成就。你永远都不可能记得所有Linux常用命令的所有用法,解题本身不是一个对已有知识的复制粘贴,而更多的是将已知与未知相结合,并通过赛题本身学到更多的东西。

扯远了,说回命题。

本次L3HCTF的4道pwn题中,我命题的只有1道——treasure_hunter。它的灵感来源于我前段时间的Rust逆向学习上。我本来的打算,是通过对Rust二进制程序进行分析,同时提升自己对Rust语言以及Rust程序逆向的理解。这是一门优雅的语言,值得我细细品味。

在我接触到Rust的Hashmap时,我真正地尝到了一丝逆向的苦头。一开始,我并不知道Rust基于Swisstable实现Hashmap,只是想着通过纯逆向搞清楚其中的逻辑。但经过了长时间的尝试后,我发现这很难。于是我抱着碰一碰运气的心态,随便找了一些Rust源码中Hashmap底层的函数名放到网上查,居然一查就出现了想要的结果,我的理解进程大幅加快。

但在查资料的过程中,我也发现,网络中对于这个新型高效的Hashmap数据结构并没有太多的分析,有较为完整的介绍博客,但数量很少。因此,我萌生了以Swisstable为主题命题的想法,让更多选手了解这个数据结构以及相关的算法。

最初,我计划出的是Rust pwn,以Rust语言现成的Swisstable模板出题,这样更加方便。但出题过程中我发现,Rust语言是一个天生不适合出pwn题的语言,一些C/CPP中习以为常的内存操作却必须使用Unsafe包裹,很是不优雅,因此仅尝试了一小段时间后我就放弃了Rust pwn这个想法,转而想使用CPP手搓一个简易的Swisstable。这样埋设漏洞更加方便。当然,这样也就意味着我的工作量大大提高。好在,在牺牲了一些低耦合与灵活性的情况下,我还是成功完成了数据结构的构建。

在题目框架完成之后,下一步就需要考虑赛题应该使用什么漏洞利用方式了。由于数据结构本身比较复杂,如果需要使用一些利用条件较为苛刻的利用方式,无疑对解题者来说是一个身体和心理上的双重折磨,此类问题也是我最为深恶痛绝的,因此我决定将漏洞点设置地简单一些,但又让选手绕不开Swisstable这个数据结构本身,这样的话,解题体验应该会好很多吧。(另外做过题的选手应该都知道,我在最终给出的ELF文件中没有去除符号表,这实际上一方面暗示了本题的考点,另一方面省去了一些令人抓狂的逆向环节。事实证明,即使如此解出的队伍数量也不超过20,符合最终的难度预期)(笑)

在经历了两届招新赛和本次L3HCTF后,我发现我实际上是有自己的出题风格的。我喜欢将题目本身置于一个真实的场景之中,让选手解题时能够身临其境(笑)。本题也是如此,创建了一个挖宝的场景,并通过该场景中可能出现的经典元素作为本题的关键内容。本题的漏洞点实际上很简单,第一个是一个10字节的溢出,我还特意在堆的最低地址处塞了一个0x400的chunk,这样选手可以通过这个溢出修改Swisstable内部的指针,对内部的数据结构进行伪造,从而达到攻击效果。另外如果选手攒够了足够的金币,可以以一个较低的价格“买到”修改control bytes的机会以及Hashmap的地址。这也是第二个漏洞点,选手可以通过这个漏洞点,与第一个漏洞点配合完成若干次任意地址的读写。最终我的exp中就是通过任意地址读写直接修改栈上的返回地址,构造一个短的ROP链完成控制流劫持。

所以总的来说,本题如果除去Swisstable不看,实际上一个很简单的赛题,没有用到对glibc堆的任何house。因此本题非常考验选手对Swisstable数据结构的理解,否则将无法通过其完成读写操作。这也是我认为我出题出的不好的一点,没有完全贯彻“将已知和未知相结合”的理念。

行了不废话了,下面贴出本题的源码。

hashmap.h

hashmap.cpp

main.cpp

下面是本题的出题人版本exp以及Dockerfile等一些配置文件。

exp.py

Dockerfile

service.sh

start_docker.sh

pwn.xinetd

我的思路是,首先把所有能挖的金币全挖出来,然后买到hashmap地址。由于本题的堆环境比较固定,可以通过这个hashmap地址获取到其他chunk的地址,通过固定偏移实现。随后我们通过将最开始的0x400的chunk分配出来(后面就是hashmap的chunk),通过10字节溢出将存放数据的指针进行修改,修改到我们伪造的地址中去,在exp中,伪造的数组在0x400中进行构造。由于hashmap没有检查边界,所以伪造后可以实现最多0x1C字节任意地址读。本题的挖矿区域是由mmap分配的,调试时可以看到这块空间位于ld.so的正下方,因此考虑可以在ld.so中寻找合适的偏移来泄露栈地址、libc地址等关键地址。这一步在正式比赛过程中成为了出题人的噩梦,因为我发现远程环境的偏移不一样,虽然说也可以通过爆破的方式通过多次连接完成多个字节的读写,但是这样会大大破坏做题的体验,因此比赛时不得不在队内服务器又部署了一份正常的然后端口映射到平台的端口,这也是为什么treasure_hunter在第一天不太稳定。(在此磕头谢罪砰砰砰)

在获取到关键地址之后,我们再一次分配那个最开始的0x400地址,准备开始写ROP chain。不过由于一开始我们并不知道返回值那个地方保存了什么值,所以需要首先读取然后通过加减金币完成写操作。出题人脚本里面是写入了一个pop rdi, ret ; addrof /bin/sh ; system这样一个简单的ROP chain,由于本题hashmap的大小为0x20,在不扩展的情况下最多可以读写0x1C个字节(0x1C这个数字怎么来的呢,这个就是Rust Swisstable的一个实现,Swisstable在填满7/8空间时就会进行扩展),足够完成这样一个ROP chain的编写。(审wp补档:看到了好几队都是通过写_IO_list_all来打fsop的,这种攻击方式我认为是更加出色的)

以上就是本题的做题流程。说实话我感觉这题出的还是不太好,有种强迫选手学Swisstable的感觉。但好在这也是迈出了第一步。后面的话还是要多接触一些好题,多学一些东西,向L3H大手子之路继续迈进。也非常感谢各位选手的包容以及评价。