逆向入门学习——看雪CTF2017第2题 lelfeiCM
1.逆向分析
1.1 分析程序基本情况寻找逆向定位点
将“2-lelfeiCM.exe”拖入PeiD中得到结果,简单观察是由Microsoft VisualC++ 6.0编写的Win32 console程序,没有加壳。

打开程序输入“123456789”可以看到结果,其中“Please input KEY:”和“WRONG KEY…”可以作为定位点。

将“2-lelfeiCM.exe”拖入IDA中,查找到字符串“Please input KEY:”所在位置如下图。

在IDA中点击空格键切换视图,根据程序运行逻辑查找到“WRONG KEY…”、“WELLDONE!”、“key format error…”、“key len error…”等关键字符串。

1.2 程序输入判断条件
在IDA界面按f5 利用Hex-Rays插件分析代码执行过程,输入数据后对其有一个判断过程,同时用Ollydbg加载程序跟踪动态调试。

可以调试跟踪出sub_401BE0函功能数为printf,程序对输入的内容判断条件为8到20 位长的非 0 的数字。其中的strlen函数的反汇编代码中有一条repne scasb指令不熟悉,可以查看指令详解。

1.3 主体程序流程分析
满足判断条件后程序进入计算加密混淆的主要过程。可以看出如果运行到LABEL_17和LABEL_16处,就会出现WrongKey错误提示,否则出现WellDone的正确提示。则第70,81和92行处的条件判断为关键跳转代码,若patch可以直接得到正确提示,这里要编写Keygen继续分析程序算法。

1.4 sub_4012C0函数分析
sub_4012C0函数是主要过程的第一个函数,跟进其中。

函数主要功能,一个是调用GetTickCount函数得到时间并存储记录到偏移2051和2050的位置,一个是调用sub_401A60函数。

第一个循环是将偏移4104大小1024数组从0到1023顺序填充;第二个循环是随机从数组中选出内容,顺序填充,应该是为了打乱数组的内容;第三个循环是将已打乱偏移4104的数组逆序拷贝到另一个偏移8的数组中。第二个循环中的sub_4019E0函数是随机取小于1023(0x3ff)的数,进入其中可以看到sub_401A00函数第一次执行时的参数是偏移2050,即为刚才GetTickCount函数得到时间。

进入sub_401A00函数,可以看到是对传入函数的参数作为种子,进行乘法加法与或等运算得到结果,并改变了传入种子的数值,所以每次调用此函数会得到不同的结果,可以看做一个伪随机函数吧。

sub_4012C0函数的主要功能是对偏移4104和8的数组进行初始化。
1.5 sub_4014E0函数分析
sub_4014E0函数是对输入的数据进行处理。

对每个输入字符乘以相应的10的n次方(可以猜测是大数处理),作为参数传入sub_401580函数中,最后调用sub_401970函数收尾。
sub_401580函数的功能,是以偏移4104数组的内容位索引,在偏移8数组中填入输入的字符,其中偏移4位置存放输入字符的长度。
sub_401970函数在sub_401580函数中也有调用,这个函数开始看起来有些奇怪不知道什么用意,后来仔细分析可以发现是将每个字符(数字),除以10的余数留下,除以10的结果加到后一位,可以看做是加强版的10进制的加法进位功能,那么sub_401970函数就是校验调整偏移8数组中保存的数据(数字)。

sub_4014E0函数是将输入的数据保存在偏移8的数组中,由于是初始输入数据,sub_401970函数也没有发挥作用,动态调试时可忽略。
1.6 sub_401730函数分析
sub_401730函数其中包含了多个小函数对数据进行处理。

首先用sub_4012C0函数初始化了一段内存V11,然后调用了sub_401310函数,是不是很熟悉,没错基本就是sub_4012C0函数的功能,只不过后面多了sub_4014A0函数的调用,进入sub_4014A0函数,可以看到当第二个参数是0的时候基本没什么作用,那么sub_401310函数也是初始化了一段内存V12。


sub_401540函数功能是将第三个参数内容复制到第一个参数中,即将v11数组初始化。

sub_401660函数是在参数a2大于0的时候才会执行功能代码,其中有两段循环代码,第一个循环是将存储的数据向前推a2个位置存储,第二个循环将留下的a2个位置赋0;相当于v11数组存储的数字乘以了10的a2次方。

sub_4016E0函数主要对v11数组每位数字进行了乘法运算,然后调用了sub_401970函数对v12数组数字进行了进位修正。

sub_4015E0函数主要将刚才乘法运算过的v11数组数字加到v12数组数字中,然后调用了sub_401970函数对v12数组数字进行了进位修正。

sub_4013A0函数是返回数字的长度(位数)。

sub_401390函数是释放数组指针。

sub_401730函数是将偏移8的数组中保存的数字与一个数字进行乘法运算。
1.7 sub_401840函数分析
sub_401840函数之前调用sub_401350的函数,其功能与sub_401310函数相同。sub_401840函数中调用的函数在sub_401730函数中都有调用,其中循环是将每一位取出相乘,则函数其主要功能是将上一步sub_401730函数计算的结果,与输入数字进行相乘得出结果。

sub_401840函数调用之后紧接着调用了sub_401730函数,之后来到第一个关键跳转点,当计算后的数字的长度为奇数时,程序继续运行,否则程序结束。
1.8 sub_4013B0函数分析
sub_4013B0函数是返回数字某一位的结果。此函数调用之后来到第二个关键跳转点,当计算后的数字的中间位(上一步已判断结果为奇数),与输入数字的第一位相同时,程序继续运行,否则程序结束。

1.9 sub_4013E0函数分析
sub_4013E0函数调用之后来到第二个关键跳转点,当两次函数调用返回都为0时,程序继续运行并输出成功的结果。

sub_4013E0函数中,要使返回值为0,在关键判断位置使其跳转,即使v14值为0,则主要使下图判断中结果为假。

其中循环退出结果IDA没有完全翻译过来,在Ollydbg中动态跟踪,循环次数为7次。

![]()
sub_4013E0函数的两次调用,分别逐位比较用户输入的第1-7位是否与计算结果的第1-7位相等;用户输入的第1-7位是否与计算结果的倒数后7位数字相等。return后面相加的是两次GetTickCount函数得到时间相加,结果右移7位,此处应该是反调试,当两个相隔时间较大时计算结果则不为0。
2. Keygen设计
2.1 算法分析
由逆向分析过程可知,满足以下条件可以得到正确的结果:
1.输入的内容为8到20 位长的非 0 的数字;
2.将输入数字乘以9,结果乘以输入数字,结果乘以9(相当于输入数字的平方乘以81),计算结果的长度为奇数;
3.计算结果的中间位等于输入数字的最后1位;
4.计算结果的第1-7位与输入的内容的第1-7位相等;计算结果的后7位与输入的内容的第1-7位相等。
2.2 Keygen编写
1.Python代码
# python 3.6.1
import time
#非零数字
def check_zero(Num):
count = 0;
while count < len(str(Num)):
if Num % 10 != 0:
Num //= 10
count += 1
else:
return False
return True
#计算结果
def cal_input(Num):
return Num * 9 * Num * 9
#是否奇数
def check_oddNumlen(Num):
return len(str(Num)) % 2
#校验计算结果后7位
def check_end(inputstr, calstr):
lencal = len(calstr) - 1
for i in range(0, 7):
if inputstr[i] != calstr[lencal - i]:
return False
return True
#校验计算结果前7位、中间位和后7位
def check_res(inputNum, calNum):
strinput = str(inputNum)
strcal = str(calNum)
if strinput[len(strinput)-1] == strcal[len(strcal)//2]:
if strinput[0:7] == strcal[0:7]:
if check_end(strinput, strcal):
return True
else:
return False
else:
return False
else:
return False
def main():
start_time = time.clock()
for inputNum in range(11111111, 10**19):
if check_zero(inputNum):
calNum = cal_input(inputNum)
if check_oddNumlen(calNum):
if check_res(inputNum, calNum):
print('Found Res is:', inputNum)
print('Input Num is:', str(inputNum)[::-1])
print('Use time: %.3f second' % (time.clock() - start_time))
break
if __name__=="__main__":
main()
2.运算结果

3.总结进步
1.在逆向分析过程中sub_401A60函数打乱了数组中的内容增加了分析的难度,可以将此部分代码NOP填充,这样调试过程中在内存中可以直观看到存储数字的内容。


2.本题的作者提供了程序的源代码,有幸对照代码,可以看出在分析过程中的内存读写操作其实是对C++的类的私有成员变量进行调用和修改,加深程序的理解,对以后分析类似的内存操作过程能有一些借鉴意义。

3. strlen函数在此程序的反汇编代码中利用repne scasb指令实现,再次可以找机会总结各版本编译器下strlen函数的实现方法。
4.参考资料
1.看雪CTF2017第二题点评及解题思路,2.repnescasb指令详解,3.分析文档,4.Python代码,5.Crackme程序及源码
Via: freebuf
