比赛于二零一七年十一月三日至十一月九日进行。十一月十日请前二十名同学提交writeup(后文简称wp)给我们审核。十一日晚wp审核完毕,公布最终结果并对参赛同学发放官方及前二十名同学的wp。时至今日,回想从大三下学期几个人脑海中不成形的计划到如今社团初立、比赛达到预期效果,我终于有如释重负的感觉。承前启后,薪火相传。当然,比赛结束,很多事情还只是开始。

十一月二日晚九点五十八分,我发了一条朋友圈:“所以还有两个小时Tongji CTF 2017就要开赛了。赛后总结赛后再写吧,或许这是朋友们和自己在这里举办的第一场,最后一场比赛。一年以来,从参赛者变为主办方,看比赛群里学号从13排到17,想起自己大一时的迷茫,想想自己现在也算不忘初心还在路上,更有幸结识几位志同道合的好友和老师。加油,加油~”。

那么这就是赛后总结。

首先来谈谈技术方面。这里主要是一个角色和角度的转变。在一定程度上说,又是从参与第一届比赛“攻击者”视角到筹办第二届比赛“防御者”视角的转变。再升华一下,就是思维由点到面的转变。

作为一场信息安全竞赛,首先要保证的就是比赛平台和赛题本身的达到一定程度的安全。

现在可以说,这次比赛的服务器架构上存在一些问题。首先,我们仅仅使用一台服务器承载比赛。服务器安装Ubuntu系统,平台直接部署在系统上,使用80端口;Pwn和Web题目以Docker容器形式部署在系统上,分别映射到10001~1000511001~11002端口。其他的端口开放有:SMTP的25端口,经过修改的SSH端口4xxxx,MySQL的3306。由于比赛是校内赛,域名和IP都只能在校内网络访问,这一特点使得本次比赛并未出现大的安全事故。如果以后考虑筹办公开赛,以上架构是不能满足要求的。

一、平台与赛题环境物理隔离。在本次比赛的架构下两者部署在一起,那么无论是因为平台漏洞导致平台被侵入,还是由于赛题Docker容器环境的漏洞导致容器逃逸(2017/11/17注:这里的说法不太准确,我本来是指之前利用DirtyCoW实现的Docker逃逸,但这实际上是Linux内核的漏洞,而非容器的漏洞。当然,也许容器本身的漏洞也会导致逃逸?),都会将另一个暴露给侵入者,后果可想而知。

二、平台本身与数据库物理隔离。这也就是人们常说的“站库分离”。设置数据库服务器只能被平台直接访问而与参赛者隔离。这样即使平台被侵入并拿到权限,侵入者侵入数据库服务器依旧有难度,可以为比赛维护提供充分的时间。

三、平台前端设置代理服务器做流量过滤。这一点主要是为抗DDoS做准备。另外使用apache2或nginx之一作为HTTP服务器。本次竞赛中仅用8个并发的Gunicorn直接承载与参赛者的交互,是有一定风险的。

四、Docker环境的差异性。举例来说吧,由于之前没有考虑到这方面的问题,我在写Pwn题目环境的自动化部署脚本时,对所有Pwn题目都使用了相同的Ubuntu镜像。这导致有选手在做Pwn500时先到Pwn400的shell下查看了libc的版本信息,然后用于Pwn500题目,并成功获得shell。在保证安全性的情况下尽量使用不同版本的环境,这一点对于Pwn和Web的环境搭建都是需要考虑的。要注意的是,如果为了环境差异化而使用非最新版本的镜像,一定要提前考察该版本是否存在已知未修补的安全漏洞。

五、SSH的安全加固。本次竞赛中我们采取的是以下措施:设置禁止root用户登录;修改默认22端口;使用高强度密码并在比赛中后期更换一次密码。由于我们使用个人电脑进行平台操作,而个人电脑的IP可能是不固定的,所以没有采用IP白名单限制;另外由于不能确定比赛期间平台登录人数以及考虑到电脑可能损坏无法继续使用的情况,也没有使用公钥私钥方式。这一点于这次比赛来说并不算是安全问题,但是还是值得以后的筹办者去考虑。

就赛题平台来说,一是自己开发一套平台出来;二是使用第三方平台。我们当时没有能力在短期开发出一套足够安全的平台,因此采取第二套方案。第二套方案又有两个平台供选择:Facebook开源的fbctf和由ColdHeat创建的CTFd。最终我们选用CTFd,因为fbctf的平台由于众所周知的网络现状,在国内较难部署,另外其华丽的平台界面也会占用更多的资源。CTFd方面,外观简约、功能完善,安装、修改都很方便。另外,它也被FireEye和CSAW等组织使用,应该具有相当的稳定性和安全性。

就赛题环境来说,主要需要考虑环境问题的就是Pwn和Web。暑期在华为参与云计算项目的实习经历给了我很大帮助。在那之前,我本计划对每个Pwn题目使用一个虚拟机,但是这样无疑浪费了大量资源,另外部署的灵活性也不高。在那之后,我发现Docker是完美的选择:创建、修改、删除速度快,又能做到与物理机的隔离。当然,它也有一些限制:使用的是宿主操作系统的内核。然而这次的Pwn和Web仅仅涉及用户层,和内核层毫无关系。唯一需要注意的就是由于内核共用,所有的Docker容器和操作系统的ALSR状态是保持一致的,要么全部关闭,要么全部打开。本次竞赛中ASLR是全部打开的,Web题目与ASLR并无关系,而Pwn除了最后一题考查到绕过ASLR外,其余题目并不需要刻意绕过ASLR。如果以后Docker不能满足的话,还是可以考虑虚拟机加容器一起使用的。

所有可能允许参赛者获得shell的题目(以及那些需要考虑同一个环境同时供多个参赛者使用的题目),都要考虑两个问题:一是参赛者可能通过当前题目对整个比赛其他组件进行破坏;二是参赛者可能对当前题目环境本身进行破坏,导致后续参赛者无法正常使用环境。第一点在本次比赛中主要考虑的是容器逃逸的可能性。不考虑0day,那么最好的做法就是使用最新版本的Docker,并在比赛前及比赛中对宿主系统进行及时更新。第二点则没有一个明确答案。我的做法可以参考附录一:Pwn500的Dockerfile,主要就是创建一个受限用户ubuntu,他对于flag文件只能读取。但是由于Pwn500程序本身是以ubuntu身份运行的,所以还是有很多小动作可以搞的。不过参赛者们素质都非常高,并且这个Ubuntu的Docker镜像中很多工具也没有,所以整个比赛中没有出现环境被破坏的情况。写到这里我突然想到,还有一件需要做的事就是在题目镜像制作好以后,应该禁止容器访问外部网络,仅仅允许题目涉及的端口有流量进出。

Web方面本来有4道题目,最终弃用2道。其中一道给出了一个webshell,设置了一些字符过滤,参赛者需要绕过这些过滤。本题在出题人系统环境下可以成功通过测试,但在赛题环境下却无法成功,弃用;另一道考查ZIP文件目录遍历,但是解压路径不是很好限制,这导致前述两个问题的产生,弃用。必须承认,这些问题与我在Web安全方面的薄弱有很大关系。文行至此,想起《恰同学少年》中杨、毛师生二人的对话:“修学储能,先博后渊;博采众长才能相互印证,固步自封必将粗陋浅薄”。所以路漫漫其修远兮,吾将上下而求索。

下面来说说我负责的Pwn题目,这中间的过程还是蛮有意思的。Pwn有两个特性值得出题人注意:一是它的逼真性。Pwn往往要求挑战者分析目标端口跑的应用程序存在哪些漏洞,写出攻击程序获得对方主机的控制权限,这与传统的黑客攻击(非Web渗透)非常相似,一旦成功将给人带来非常大的成就感。二是它的入门门槛。由于Pwn依赖了或多或少的操作系统背景知识,所以初学者的学习路线往往是螺旋式的,这里学一些,那里学一些,拼起来才能够往上走。去年第一届比赛时学长们或许也是考虑到了难度,出的两道Pwn都很简单:一个仅仅需要把函数返回地址覆盖为另一个函数地址,另一个是基本的格式化字符串泄露内存信息。第二届比赛本着**“始于上届,高于上届”**的原则,我计划有从一百分到五百分共五道题目。考查的内容分别是:

  • 栈溢出覆盖变量
  • 格式化字符串漏洞
  • 最基本的shellcode传入(直接把传入内容当做指令执行)
  • ret2plt
  • ret2libc+格式化字符串漏洞进阶利用方法

其中前两道题目与上届比赛的看齐,承上;后三道题目由浅入深,启下。我希望这些题目可以组成一套由浅入深的教程,带着参赛者在做题的过程中一点点前进。当然解决方法需要自己上网学习。从答题情况看,前两道的解答率还可以,后三道基本上就是那些本来就有基础的同学做出来了。

本来Pwn500题目存在一些问题,一直没有调试成功,也就没有打算放出来。等后来Pwn400和Reverse500相继放出,并被参赛同学攻破以后,我和oblivi0n、Remind商量出道更难的题目,来“守门”。这个时候参赛者的名次区分其实已经出来了,设置“守门”题目事实上是出题人和排名靠前选手的对抗。我修改了一些参数,下午把pwn500调通,第二天早上把题目放出来。然而到中午的时候有选手攻破了Pwn500。而Remind新的Reverse500下午终于调好,到晚上又给放出来。终于,这道题目坚持到了比赛结束,成功守门。

赛后读选手们的wp也是件很享受的事情,因为几乎每道题能看到有好几种有效方法。有的方法还很优雅,真是“还有这种操作”!想起高中时席老师叮嘱我们“独学而无友,则孤陋而寡闻”。交流和分享的确十分重要。

比赛中可以写的还有很多,比如怎么放提示才不影响公平公正,遇到平台flag设置错误的情况怎么处理等等。我们第一次办比赛,做的当然也不可能尽善尽美。但我们尽己所能问心无愧。

我始终记得大一时第一次上“C++高级语言程序设计”时沈坚老师说的话。那时自己真的是什么都不会,老师说“现在是小白不要担心,一个月以后你再回过头来看看自己的进步”。

比赛结束了,我知道我依然在路上。

附录一:Pwn500 Dockerfile

# for pwn500

FROM ubuntu

RUN dpkg --add-architecture i386 && apt-get update && apt-get install -y apt-utils libc6-i386 socat

RUN useradd --create-home ubuntu

WORKDIR /home/ubuntu/
COPY ./bin/pwn500 ./bin/flag /home/ubuntu/

RUN chown -R root:root /home/ubuntu/
RUN chown ubuntu:ubuntu /home/ubuntu/pwn500
RUN chmod o+x /home/ubuntu/pwn500
RUN chmod 744 /home/ubuntu/flag

EXPOSE 10000

CMD ["/bin/bash"]

ENTRYPOINT su -c "nohup socat tcp-listen:10000,reuseaddr,fork exec:./pwn500" ubuntu

注:后来发现为了在64位系统上运行32位程序,需要安装:

sudo apt-get install libc6-dev-i386