第二届强网杯线下赛新技术分享

0x0 写在前面

强网杯是具有国家背景的CTF赛事,具有“延揽储备锻炼网信领域优秀人才,提升国家网络空间安全能力水平”两个目的。办赛任务下发后,队员和老师们尽力将赛事呈现出色,与其奖金和影响力相匹,但是我们能力有限,在竞赛过程中出现不尽如人意的地方,也请参赛选手多多包含。强网杯是一个优秀的平台,相信竞赛过后也会有越来越多的高校、企业关注网安学科。我们也收到了很多关于强网杯存在的问题,会在今后的赛事中进行改正。也谢谢给予了强网杯肯定的参赛队员和单位,大家的肯定和支持让我们几个月的义务付出有了意义,在此鞠躬感谢。
本次竞赛线下赛在确定为AD模式+春秋(中标)平台后,我们根据自身的参赛经验,决定在我们能够接触的范围内在一定程度上解决参赛的不适度问题。从办赛过程中总结来看,值得分享的技术或经验主要有:

  1. 赛题相关问题答疑;
  2. 如何制作AD模式的gamebox;
  3. 全新的堆check机制;
  4. 全新的线下赛防作弊手段;
  5. 其他。

 

0x1 赛题相关问题答疑

本次竞赛部分赛题的选手疑问在这里进行集中答疑。

revolver

revolver
该题目是对线下赛中出密码学题目的尝试,题型为re+crypto+pwn。在该题目的命制过程中,考虑了很多做题和check的技术,也导致了很多人check不过。
首先题目flag被分组密码(AES256)加密,公钥密码(RSA)则加密了AES的key,模拟分组密钥分发过程。Binary中存储固定的公钥,aes key随机生成,flag以密文的形式存储在内存中。
首先介绍攻击方式,全场存在4种攻击方式:

1.OOB Write+Related Message Attack
很多密码学在出相关消息攻击的时候均为给出两个密文的线性关系条件,本次出题让选手通过pwn的手段自己构造线性关系。通过3-7的OOB Write修改key,并分别在修改前后进行加密,可以获得前后相差只有1bit的密文使用e=3的相同公钥加密密文,达到相关消息攻击的条件。即使用3-6,3-7,3-6选项即可。

2.AES round key Recovery
AES256需要连续的256bit轮秘钥即可逆推,回复出初始秘钥。其实不连续也可以,但是考虑到只有三发子弹,这是一个几乎不可能完成的任务。

3.LLL Attack
这个点本来是一个干扰项(包括前面存在的OOB Read也是干扰项),但是0ops发现了这个点其实也可以攻击,因为出题的时候忘了hex,导致高512bit是0,往后256bit是1,爆破1个bit即可完成Known High Bits Message Attack。

4.Factory N
现场“风吹雨战队”以分解了1024bit n的方式进行了攻击。
以上为攻击方法,下面解释一下check的设置和思路。个人感觉,在题目中引入密码学会使得check异常合理且严谨:

1.首先flag是读进来的,为了保证flag的open不会被篡改,并且在checker拥有私钥的前提下,进行如下check:获取flag的密文(被aes256加密),获取aes256的key的密文(被rsa加密),使用私钥解密key,使用key解密flag,使用paramiko ssh登录gamebox并cat flag,验证两个flag是否相等。通过以上步骤,可以保证flag在内存中是确实存在的。

2.保证所有攻击流程不会被篡改:在拥有私钥的前提下,所有的攻击流程的每一个步骤都可以进行完整的验证,思路非常简单,以正常数据(比如不会越界的数据)走完攻击流程,并用私钥解密,观察flag是否和ssh获取的flag相同。

3.针对aes轮秘钥,读取了不连续的进行check。
所以patch方法必须需要patch的非常精确,修补方法为:

1.修改程序中造成OOB Write的size,48改为16即可。

2.需要保证不能拿到连续的两个轮秘钥:对输入进行限制或者返回错误的轮秘钥。

3.修改padding,让高bit位不可知。
有部分队伍修补成功,并防御了所有攻击;但是在有战队将rsa1024分解后,所有的patch失效。如果具备分解1024RSA(pq使用openssh生成,不具备可攻击特性)的能力,或许这个题目的分数送给他也无妨。
另外感谢大佬们的好评。

 

gamebox

出题方面考虑到只有一个web题,为了保证题目的难度,特意选取一个难度较大的框架,使用的漏洞也基本以0day为主,预设了五个漏洞,包括一个任意文件读,三个任意文件写和一个无限制注入。比赛期间基本都被挖掘出来了,其中还有一个非预期的文件上传漏洞。但是由于出题人的经验不足,导致在checker的编写上出现了较大的问题,很多地方check不到位或是不合理,同时后期的思考中也发现了该题目其实某种程度上不太适合作为线下对抗赛的题目。

xxxxxxxx

xxxxxxxx这道题的本意是想让大家通过越界读去获得信息,包括堆上的和tls段上面的,但是由于出题者的愚蠢导致了大家不好的体验(向大家抱歉)…大部分的人都是通过load(‘flag’)的变量报错读取a-f开头的flag的,这是因为在本地测试的时候生成的flag恰好是0-9开头的,会被当成是数字,没有报错。预期的解法应该是通过diff找到charAt的越界,结合garbage collection不让load到heap上的flag chunk被重新分配,最后读取flag。现场也有队伍是用预期的解法解出这道题目的。本来还有另外一个snprintf的栈溢出的,要通过分配适当大小的chunk使得memstr正好在tls的前面,但是由于用charAt泄露canary的时候会把它变成utf-8编码,当canary里面有badchar的时候输出全部都是bad,要爆一段时间才能跑出来,这在5分钟一轮的线下是一个不好的设计,于是直接放弃了它(洞还在,只是没法触发了)。

0x2 如何制作AD模式的gamebox

线下赛的竞赛平台中,部分公司提供的gamebox较为完善合理,部分公司提供的gamebox权限设置和防搅屎机制基本靠出题者自己折腾。为了给大家提供一个纯粹的漏洞挖掘、利用、修补、重放的AD竞赛,我们参考了一些文献资料,并向用过办赛经验的战队进行了请教,结合一些自己的理解,重新制作了gamebox,下面将制作方法开源。

1.准备并安装虚拟机:
强网杯使用ubuntuserver16.04×64进行安装,鉴于近期曝出的linux内核提权,我们对系统进行了更新和漏洞检查;

2.设置用户:
Root用户用于导调组进行竞赛维护,ctf用户给选手,problem用户用于启动题目。其中root用户和ctf用户设置密码,problem用户不用设置密码。

groupadd ctf
useradd -g ctf ctf
groupadd problem
useradd -g problem problem

需要给ctf用户sudo –u到problem的权限,方便选手对题目进行维护:

3.删除计划任务:

4.chroot与目录权限
chroot的想法源自清华刘一吨大佬的git,该git为提供线上赛现在最常用的docker。https://github.com/Eadom/ctf_xinetd。
首先制作题目目录。目录整体如下:

problem目录中存放题目文件,题目文件权限和属主和problem目录一样,该目录为给ctf选手的目录,这个目录里他想干啥就干啥,不管。
flag要放在准备chroot后的根目录上,并且只让ctf和problem可读。
要把必要的lib和bin拷贝到准备chroot的目录下,比如为/home/ctf:

cp -R /lib* /home/ctf && 
cp -R /usr/lib* /home/ctf
mkdir /home/ctf/dev && 
mknod /home/ctf/dev/null c 1 3 && 
mknod /home/ctf/dev/zero c 1 5 && 
mknod /home/ctf/dev/random c 1 8 && 
mknod /home/ctf/dev/urandom c 1 9 && 
chmod 666 /home/ctf/dev/*
mkdir /home/ctf/bin && 
cp /bin/sh /home/ctf/bin && 
cp /bin/ls /home/ctf/bin && 
cp /bin/cat /home/ctf/bin

5.chroot与xinetd
然后就是使用xinetd结合chroot启动程序,我problem用户的uid和gid为1001,所以:

6.防搅屎的额外工作
首先获取sh后,所有的目录chroot后都没有写权限,而且可以使用的命令只有三个:

使用chroot+xinetd的方式的话可以有效地避免部分沙盒、木马等攻击,但是经过实际测试,forkbomb仍然可以使用,所以我们对sh文件进行了patch,阉割掉其所有的循环功能。

综上,基本gamebox制作完成,还有一些细节需要处理,这里不过多叙述


感谢大家的支持!

 

0x3 一种有效的堆check机制

lowbits leak check

我们Flappypig战队经过探索,设计并实现了一种新颖的堆分配检查机制(lowbits leak check),可以检测修改预期堆分配结构使得漏洞利用失效的通用防御方法,并应用到了本次强网杯线下赛的pwn类型题目的checker中,下面给大家介绍一下这种checker机制。
按照linux内存分配机制,在每个进程默认创建时会预先分配堆栈空间,默认堆栈空间的大小是4K(0x1000=4096),然后进程再分配空间是使用malloc对这4k大小进行管理,如果超过了4k再向内核申请。所以,在用户空间下,总堆块大小不超过4k的情况下,malloc返回的堆地址在低12bit(0xfff)是变化的,而前面的比特是相对不变(随着ASLR变化)的。
那么这就给了我们在CTF线下赛中一种针对堆漏洞的Checker的思路,我们在程序交互中预先在每次malloc后,把堆地址的低12bit输出。
然后正常设计堆漏洞(UAF、Double Free、off by Null),再写Checker脚本时,通过不断申请、释放不同大小的堆块,可以检查出选手对漏洞修补过程中,是否有nop掉free函数或者改大malloc(size)等破坏预期堆分配逻辑的操作。
举例分析:
强网杯-secular-checker脚本部分示例

    cur=build(io,0x90,'an')
    #print 'low_address->0x%03x'%(cur)
    if cur!=0x070:
        raise Exception("Heap_check error")
    cur=build(io,0xa0,'bn',777)
    print 'low_address->0x%03x'%(cur)
    if cur!=0x130:
        raise Exception("Heap_check error")
    cur=build(io,0xf0,'cn')
    print 'low_address->0x%03x'%(cur)
    if cur!=0x200:
        raise Exception("Heap_check error")
    cur=build(io,0x100,'dn',777)
    print 'low_address->0x%03x'%(cur)
    if cur!=0x320:
        raise Exception("Heap_check error")
    cur=build(io,0x30,'dn')
    print 'low_address->0x%03x'%(cur)
    if cur!=0x450:
        raise Exception("Heap_check error")
    delete(io,2)
    delete(io,3)
    cur=build(io,0xa0,'dn',777)
    print 'low_address->0x%03x'%(cur)
    if cur!=0x200:
        raise Exception("Heap_check error")
    cur=build(io,0xf0,'dn',999)
    print 'low_address->0x%03x'%(cur)
    if cur!=0x490:
        raise Exception("Heap_check error")
    delete(io,1)
    cur=build(io,0x90,'dn',999)
    print 'low_address->0x%03x'%(cur)
    if cur!=0x070:
        raise Exception("Heap_check error")
    magic(io,777)
    cur=build(io,0x90,'an')
    print 'low_address->0x%03x'%(cur)
    if cur!=0x590:
        raise Exception("Heap_check error")

可以看到脚本中间有一个地方是申请了0xf0大小的堆块,然后得到预期地址尾部应该是0x200,然后后面释放之后再次申请了0xf0大小的内存,所以预期情况下应该是会重新分配到0x200这个空闲的堆块上,如果选手在修补过程中,nopfree操作,那么分配不到0x200上,便可以判定check down。本次线下赛,使用了这种checker机制,第一天查出了许多队伍使用nop掉free的套路,但是没有想到主办方还有这种sao操作,所以被判down了。
这种全新的方法可以用于日后的线下赛参考,借某大佬的评价全新的堆check方式会被大家广泛使用,线下赛不适宜出堆将成为过去时。
所以,以后大家还是好好针对漏洞点修补漏洞吧。此处at某wings

0x4 基于水印的防作弊机制

本场比赛使用了一种适合AD的反作弊机制。可以检测到如下行为:
某个战队使用了其他战队的binary。
具体思想其实比较简单,就是给每个战队的binary上水印。因为时间仓促,水印上法比较简单,就是在5个不同的位置设置了使用aes加密的字符串。每个队伍每个题目的每个字符串全部使用队伍相关信息加密进行签名。
竞赛过程中,我们使用NPC主机作为水印checker,每隔五分钟通过paramiko使用scp去download所有战队的所有binary,主要实现两个目的:

1.通过size大小变化来推测是否使用了通防

2.检测所有队伍的binary中是否出现了其他队伍的水印
针对1,因为沙盒,ptrace,或者加段的通防修改幅度较大,因此通过size大小变化可以识别出那些可能有通防的binary。然后通过人工分析即可。
针对2,因为水印是用队伍信息签名的,因此在某战队的binary中出现其他战队的水印是不可能事件。所以一旦有队伍的binary出现了其他队伍的水印,那么证明是作弊行为,进行了binary的交换,可以实锤。设置5个水印的原因是怕队伍在patch的过程中破坏了水印,因此设置多个用于备份。

 

0x5 其他

因为每次使用春秋平台时,流量获取的体验很差,这次特别地进行了及时的流量给予,发现对一血队伍不太友好,听取了部分战队反馈的建议后,后续竞赛中应该适当对流量给予进行延迟。
另外依据部分大佬的反馈,CTF竞赛的AD模式自身上存在不足,Defcon也一直在改进AD模式,也希望去拉斯维加斯等国外参赛的大佬们将更多更好的竞赛模式引进国内,让我们学习下,否则Jeopardy模式其实是更好的线下赛模式选择。

(完)