看雪.腾讯TSRC 2017 CTF 秋季赛 第二题点评及解析思路

发布者:Editor
发布于:2017-10-28 18:27


今天中午CTF 第二题结束~

大家伙们对第二题,热情高涨,光围观人数就已经达到8914人。

有很多小伙伴都彻夜不眠,只为攻破此题,第二天接着上班。

(年轻人果然身体好啊!)虽然成绩很重要,但是也要注意身体哦!

第二题结束后,目前攻击方排名如下:

目前第三题正在进行时,截止目前已有23人攻破,同志们继续加油!


看雪评委netwind点评


此题一开始释放了两个“烟雾弹”,经过分析后可以判断此处无解,分析发现程序里有一段数据,这里实为真实验证流程的代码,真实的验证流程需要通过溢出覆盖返回地址跳转过去,在清理掉花指令后分析出算法解方程后便可求解。


第二题作者简介


选取攻击者birchfire的解析过程


1、初步分析,陷入迷茫


初步分析程序用全局变量dword_41B034保存了一个控制变量,控制变量初始值为2,当该值变为0时会输出“You get it!”

而程序在获取输入之后调用了2个函数,这两个函数在满足一堆条件下分别会对dword_41B034做减1操作,看似2次减一之后dword_41b034就能变成0

再看条件,两个函数的v0都用的输入的前4字节,v1都用的输入的5到8字节。

我们选出其中的2个方程

求差发现 12*(v1-v0)== 奇数,而输入无论如何都是整数,所以等式左边一直都是偶数,等式永远不会成立。理论上证明这一组方程无整数解,不用去穷举了。


2、发现玄机


发现函数在获取输入时存在栈溢出,输入16个字符程序出现崩溃,返回地址被输入的第13~16个字节覆盖,考虑直接ret到0x40102f位置,但优于规则限制输入字符只能是数字和字母,该方案行不通;而用shellcode的话很难构造出只含有数字和字母的shellcode。考虑到验证码唯一性、字符限制等综合条件,出题人的意图可能是在栈溢出后ret到预先设置好的代码,ret的地址是可以通过输入数字和字母字符构造出来的,用IDA浏览一下程序代码,不难发现一个看似符合条件的可疑块00413131,0x41对应字符A,0x31对应字符1,而0x00刚好是字符串结尾。由此猜测输入共15个字符,最后3个字符是"11A"


3、抽丝剥茧,柳暗花明


用输入11223344556611A作为输入测试,发现程序并没有崩溃,可见思路是正确的。

将输入字符标记为污点,使用污点传播提取出相关的指令如下图,我们假定输入的前4字符组成的int为a,5~8字节组成的int为b,9~12字节组成的int为c,标记出相关指令对应的表达式,推断表达式需满足的条件,构造满足条件的一个输入,让程序往下执行,再得到下一个条件,以此类推。

得到3个3元一次方程组

方程2和方程3求差,得到2*c = 0xdceacc60 得到 c = 0x6e756630, 对应字符 "0fun"

4*(a-b) + a + c == 0xeaf917e2 …… 13*(a-b) + a + c == 0xe8f508c8 …… 23*(a-b) + a – c == 0x0c0a3c68 …… 3

方程1和方程2求差,得到(a-b) = 0x02040f1a得到 a = 0xe8f508c8 – c -3*(a-b) =0x7a7fa298-3*0x2040f1a = 0x7473754a, 对应字符“Just”

得到 b = a - 0x02040f1a = 0x726f6630,对应字符“0for”

最后串起来得到对应输入字符串:Just0for0fun11A


4、深入分析,窥探奥秘


解题过程的基本思想是用污点传播找出输入影响到的指令及跳转分支,根据验证码的唯一性去求分支的“取等”条件(往往也是随机输入对应的另外一条未走过的分支),构造出方程求解。但是到此为止还未弄清楚程序最后如何输出"You

get it!"


为了剖析出题者的思路,我们Trace记录下程序处理正确验证码的过程,在通过层层验证之后,经过了一系列的条件跳转,设计者通过大量的跳转垃圾指令干扰分析,选出核心的16条指令如下所示:

第1条call指令长度5字节,并将返回地址0x413835压入栈0x12ff4c的位置,而第16条指令call的一个Printf函数,刚好使用栈0x12ff4c中的值作为参数指针,也就是说会输出0x413835内存位置的字符串。

第2条指令pop eax是为了获得写入数据的内存地址,为了维持栈平衡有了第3条指令push eax,效果是此时eax存储的是要写入”You get it!”的内存地址;

第7,9,11,12条指令通过将一些立即数异或运算得到字符”You get it!”的编码,不直接使用待输出字符串是为了防止搜索;

第8,10,13条指令将“You get it!”字符串依次填入了栈0x12ff4c中指针指向的内存缓冲区0x413835

最后构造跳转,去执行0x401044位置的Printf函数,输出“You get it!“字符串。


5、灵光闪现,另辟蹊径


分析到此处不难想到,如果在栈溢出位置直接ret到0x413830位置的call指令是否也能输出“You

get it!”,测试发现并不能,原因是“You get it!”字符串是“解密”运算得到,并且“密钥”刚好是输入的前面几个字符运算得到,如果不经过前面代码的铺垫。

0x413950 xor eax, edx 中的edx值不会是0x20756f59,后面无法获得“You get it!”字符串,并且 0x413bbb jmp eax中的eax值也是根据输入的a、b、c三个变量计算得到,没有前面的代码铺垫无法调用Printf函数,可见出题者在防多解方面也是下了功夫的。

另外考虑在ret到0x413131地址之前先ret到其它地方,构造rop链最后再ret到0x413131位置构造多解,此时还需保证esp-0x10位置的数据是满足上述3个方程的a、b、c。构造方案如下图所示,ret addr1是第一个需要构造的值,该位置刚好存储折ret 0x10指令,溢出后的第一次ret之后eip指向了ret addr1,esp自动加4,指向了ret addr2;

之后执行addr1处的 ret 0x10 指令,eip指向了 ret addr2, esp自动加0x10指向了第三个需要跳转的目标地址0x413131;然后执行 addr2 处的 ret 指令,eip指向0x413131位置。由于ret addr1和addr2不能含有0,而主程序模块地址高2字节为0,因此搜索ret \ ret 0x10 应该在kernel32.dll、ntdll.dll等动态库中搜索。

我这边使用windows7进行了尝试,测试时kernel32.dll刚好加载在0x76330000-0x76404000的地址空间,在kernell32.dll中搜索到了大量ret和ret

0x10指令,当然ret 0x10 可以换成其它指令,但栈空间布局需要相应更改,至少为ret 0x0c,底下才有a、b、c的存储空间。另外ASLR等环境因素的影响,读者去尝试重现时可能加载的地址不一样。

搜索出的ret 0x10和ret指令分别选取0x76367931位置和0x76395659位置,也就是说构造的第一个返回地址为0x76367931,对应输入字符串ly6v,第二个返回地址为0x76395659,对应输入字符串为YV9v。

针对这个环境下我们可以构造出只包含数字和字母的另外一个输入 1111222233331y6vYV9v1111Just0for0fun11A 使其输出“You get it!”,其中下划线字符可任意更改其它数字和字母。

但是这类型的验证码受到不同版本操作系统影响,kernel32.dll的不同会导致所需构造的验证码不一样,需要根据版本差异重新搜索指令的位置。另外ASLR也会有一定影响,kernel32.dll在系统启动之后加载地址不会再发生变化,但系统重启之后其基地址会重新随机生成,验证码还得重新构造。因此理论上难以得到第二个稳定通用的只含有数字和字母的解。

最后给出题者点赞,题目构造犹如鬼斧神工!


声明:该文观点仅代表作者本人,转载请注明来自看雪