目录

某CTF一道逆向题分析

某个周末,公司群里发了个某CTF的逆向题,刚好没啥事就尝试做一下。


详情分析

首先,先运行看看情况,打开程序之后提示找不到VCRUNTIME140_1.dll,如下:

/images/某CTF一道逆向题分析/25cc06601ed89db366570cad1703f2d5721eba30f6f0d5c0e7013ded467b042c.png

此链接下载vcruntime140_1.dll,并将其放到C:\Windows\System32\目录下,再次打开程序,可以看到是个控制台程序,并且需要输入,如下:

/images/某CTF一道逆向题分析/bc33e0345653bf4b8ec0d2087cadcbfd68bbabbac39fc909c8f47b28d3632878.png

注意
Win7下才会缺失vcruntime140_1.dll,Win10下不受影响

接着查壳,看看是否加壳,使用LoadPE查看区段,发现区段正常,并无异常,应该是没有壳的,如下:

/images/某CTF一道逆向题分析/afb84c93fc29d2b8544884e1600df25afc490855e7685969f08b095ff639a632.png


接下来用x64dbg打开程序,并且搜索字符串,如下:

/images/某CTF一道逆向题分析/ce1d8f6c2aeddf7c24213148c08b22e99208d7463d7139d8c0c5f39ce51c09e2.png

可以看到如下字符串:

/images/某CTF一道逆向题分析/ee3839203cddfa10514722a818c44440288dc259573d74dd2e9adab8ef6b247a.png

猜测应该是输入特定的字符串,然后对输入一顿加密,最后比较加密后的结果是否与程序内设定的一致,一致则提示找到flag,否则就退出啥的。


接下来再用IDA打开程序,找到main函数,看下流程图,如下:

/images/某CTF一道逆向题分析/0e317470337773054a5fd00f56525e3254bdb5a06183b1c2f9aff7e840dd4f4a.png

看到这种的时候,就是 IDA关闭 ==> 虚拟机关闭 ==> 爱奇艺打开。追了会剧,吃了个饭,想了想,还是分析分析吧,毕竟还是想装装逼学习学习的。

整个程序分成以下几块:

  • 获取输入
  • 判断输入是否合法
  • 判断第二段字符串是否正确
  • 判断第一段字符串是否正确
  • 判断第三段字符串是否正确

获取输入:

IDAf5 大法之后,从104 ~ 155行,是获取输入,如下(请忽略小学鸡的注释😂):

/images/某CTF一道逆向题分析/ae8ae2807200eac3ef55e13e7cb36194e0af8defa1f67a0121d25589bf61f0b1.png

上图中,标注了两处地方,这两处地方是这个获取输入的地方最重要的地方,下面详细分析。

首先看第二处,第二处比较简单,也就是这个输入怎么结束。这里的逻辑是判断输入的字符是否为#号,当输入了3个#号之后,循环结束,完成输入。

再来看第一处,这里比较复杂。

if判断的是两个全局变量,这两个变量一开始都为0,然后开始执行sub_140001F60函数,该函数会将输入的字符存放在某个数组中,并且给三个变量赋值。

变量1为存放字符的数组首地址。变量2为该数组+1的地址,其实也就是输入字符串的长度,变量2 - 变量1 = 输入字符串长度变量3不知道存放的是什么地址,它要么与变量2相等,要么比变量2大一些。

if判断的就是变量2变量3,当相等时就去执行sub_140001F60函数,不相等时则将变量2+1。

这里最主要就是变量3的变化,经测试,变量3的变化主要与输入的字符长度有关,具体如下:

输入字符长度变量3的值
1 ~ 4变量2+0,也就是字符串实际长度
5 ~ 6变量2+1,也就是字符串实际长度+1
7 ~ 9变量2+2,也就是字符串实际长度+2
10 ~ 13变量2+3,也就是字符串实际长度+3
14 ~ 19变量2+5,也就是字符串实际长度+5
20 ~ 28变量2+8,也就是字符串实际长度+8

可以看出点端倪了,这应该是斐波那契数列。当字符串长度大于等于 5 时,就将变量2按照斐波那契数列依次累加,并且赋值给变量3

从这个获取输入的位置,可以得出以下结论:

  • 输入的字符串,包含3个#
  • 输入的字符串长度应该有所限制

判断输入是否合法:

当接受完输入的字符串之后,接下来就是判断输入是否合法了,从这个标题应该就能看出,这个输入字符串是有格式的,我们接着来分析。

156 ~ 162行,是在进行输入合法性校验,代码如下:

/images/某CTF一道逆向题分析/c805d9c8419c1151f62b8a066458d99f530d7d38d184dfc92985eac3bdd509f9.png

这里也有两处比较重要的点。

第一处判断几个全局变量相加相减之后的值是否等于37,不等于直接退出。这几个变量就是上面分析的变量1变量3,只不过有三组变量1变量3。为什么有三组呢,因为前面输入的位置每遇到一个#号,就进行一组的变量操作,三个#号刚好三组。用每一组的变量3 - 变量1,也就是将斐波那契数列处理过的数 - 字符串首地址,得到的差值再相加,必须等于37。下面是输入字符串长度与变量3的关系,也就是上面的完善版:

输入字符长度变量3与变量2的关系最终的差值
1 ~ 4变量2+0,也就是字符串实际长度字符串实际长度
5 ~ 6变量2+1,也就是字符串实际长度+16
7 ~ 9变量2+2,也就是字符串实际长度+29
10 ~ 13变量2+3,也就是字符串实际长度+313
14 ~ 19变量2+5,也就是字符串实际长度+519
20 ~ 28变量2+8,也就是字符串实际长度+828

从这个表格中可以看到,要想满足37的标准,最简单得是28+9,如果这样得话那么一段字符串为20 ~ 28位,一段字符串为7 ~ 9位。

第二处,是三个变量相比较,也就是三段字符串的长度,比较结果是第二段字符串长度最长,其次是第一段,最后是第三段。

从这个地方可得出以下结论:

  • 输入为一段包含3个#号的字符串,刚好分成3段字符串,第二段字符串最长,其次是第一段,最后是第三段。
  • 按照上面这个长度划分,再结合相加得为37来看,第二段字符串应该在20 ~ 28位之间,第一段和第三段加起来为9,且第一段大于第三段。

判断第二段字符串是否正确:

接着是判断第二段字符串是否正确,代码为163 ~ 189410 ~ 414行。

先是执行410 ~ 414行,这里这个while循环,是遍历输入的第二段字符串,判断每个字符是否等于1或者等于2,不等于这两个直接退出,代码如下:

/images/某CTF一道逆向题分析/9592d26e4660faf9c9f8d3f409e9ca59700f1ef9cdd02fbf0fd1a32360a17542.png

接着就是163 ~ 189行,将第二段字符串进行MD5哈希,并且判断是否与39c1ca4b6d64c40558425432c11624a8相等,不相等则退出,代码如下:

/images/某CTF一道逆向题分析/7b0d2e97416ed3fd0553dc64c98b1b9058b24b50e278e5e5ed696e138d5f65a5.png

从这里得到结论:

  • 第二段字符串必须由12组成
  • MD5后必须等于39c1ca4b6d64c40558425432c11624a8

在这里将39c1ca4b6d64c40558425432c11624a8放入到网上的彩虹表查询,都没有查询到结果。由于只由两个字符组成,这里直接采取暴力破解的方式,来找到对应的字符串,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import hashlib
import itertools

for i in range(1,33):
    for i in itertools.product('12',repeat=i):
        data = bytes("".join(i),'utf8')
        md5str = hashlib.md5(data).hexdigest()
        # 1221222221212121211122112111
        if md5str == '39c1ca4b6d64c40558425432c11624a8':
            print('it\'s find!')
            print('str:%s, md5str:%s'%(data, md5str))
            exit()

最后找到1221222221212121211122112111,如下:

/images/某CTF一道逆向题分析/6900027f902b897fc04bb6d7fd50a5aa7aecc142012b960ac2823ffa28a04dd0.png

最终结论:

  • 第二段字符串为1221222221212121211122112111
  • 最终字符串为:?#1221222221212121211122112111#?

判断第一段字符串是否正确:

然后判断第一段字符串是否正确,这里的代码就比较长了,主要点在下图:

/images/某CTF一道逆向题分析/92178762452bc88f95157f5bd335280413de748b89c84021d1c5dbc1b47d3b0e.png

主要逻辑为先定义了一个加密字符数组,然后取数组内容相加,并与输入的字符一个一个异或,得到的结果再与当前字符所在的下标进行异或,最后将异或后的结果与定义的字符相比较,其中一个字符不相等则退出。

这里我用的笨办法,因为本来剩余的字符就不多了,所以我直接动态调试找到异或的加密字符,然后与正确字符异或就得到了输入的字符串,代码如下:

1
2
3
4
5
6
7
a = 'F4 F3 D2 EE 8B A1 77 99'.split(' ')	# 正确字符
b = 'A6 91 E4 D9 EC F6 F6 1A'.split(' ')	# 加密字符

for i in range(0,6):
    i1 = int(a[i],16)
    i2 = int(b[i],16)
    print(chr(i1^i2^i),end='')		# 将三者异或回去就得到了输入的字符

得到的结果为Rc44cR‡„

/images/某CTF一道逆向题分析/6a82ae4993e66c2e3dfa2d32f2f750a04e67ae657d13b381710bca4c76fcc391.png

可以看到,输入的字符有乱码,说明第一段字符串肯定不是9,那就可以用3+6的组合来实现,也就是第一段字符长度为6,第二段字符长度为3,这样既能满足37(6+28+3)的要求,又能满足第一段字符长度 > 第三段字符长度 && 第一段字符长度 < 第二段字符长度的要求,所以最后第一段字符串为Rc44cR

这里如果用3+6的方法的话,根据上面表格总结出的关系,第一段字符串也可以是5个字符,所以这里可能有两种答案。

总结:

  • 第一段字符串长度为6,正确答案是Rc44cR
  • 完整字符串为:Rc44cR#1221222221212121211122112111#?

判断第三段字符串是否正确:

第三段字符串判断逻辑,与第一段字符串判断逻辑类似,只是字符串长度变短了,并且最后异或比较的时候,没有与字符所在下标进行异或了,所以代码如下:

1
2
3
4
5
6
7
a = '66 02 55'.split(' ')		# 正确字符
b = '32 41 13'.split(' ')		# 加密字符

for i in range(0,3):
    i1 = int(a[i],16)
    i2 = int(b[i],16)
    print(chr(i1^i2),end='')	# 将两者异或回去就得到了输入的字符

得到结果为TCF

/images/某CTF一道逆向题分析/62f246100e93cb469919b534eadc2fbbaadab09f578289d140cdb4cf39bb30d9.png


最终结果

最终的输入字符串为:Rc44cR#1221222221212121211122112111#TCF#,由于上面的表格总结的关系,以及CTF已经关闭的缘故,没办法验证Flag是否正确,字符串也可能是Rc44c#1221222221212121211122112111#TCF#。执行结果如下:

/images/某CTF一道逆向题分析/13aae406d1c3320e290a6fee3f90e6230c246e0ad630cf9e39586fbdff0043cd.png


END

第一次写 CTF 逆向题的WriteUp,写得比较啰嗦,而且太久没搞逆向了,可能有些地方分析得也不对,如发现有任何错误,欢迎与我联系。


相关链接
相关文件:cpp.exe_WP.zip