成钢二炼——逆向入门之试水反调试
1.逆向分析
1. 分析程序基本情况寻找逆向入手点
将“看雪CTF2017爱琴海CrackMe_CRC32_3978B319.exe”拖入PeiD中得到结果,简单观察入口点地址为0,初步怀疑程序加壳。

将程序拖入IDA和Ollydbg中,都不能像正常程序一样正常进行调试分析(入口点地址为0)。


在IDA中忽略提示,继续加载程序,跳转到0×400000入口点地址查看具体汇编代码。

可以看到程序加载入口点地址后,随即跳转到了0×401090地址,则使用LoadPE工具将入口点地址修改为0×401090,在Ollydbg中加载可以正常调试运行。


在IDA中查看字符串,没有发现有用的字符串定位信息,只发现了“MSVBVM60.DLL”信息。
在Ollydbg中按住“Ctrl+N”查看程序函数列表,发现了“msvbvm60.MethCallEngine”函数,同时在修改的入口点地址0×401090附近可以看到jmp到msvbvm60函数的指令,可以判断此为VB程序。至此可以使用VB程序逆向的方法分析,具体方法技巧可以参照看雪论坛的三篇文章学习(Visual Basic程序破解 1,Visual Basic程序破解 2,Visual Basic程序破解 3)。


2. Visual Basic程序分析
2.1 Visual Basic代码反编译
在分析本程序,选用“VB Decompiler”工具,将程序加载其中可以直接得到反编译结果。

可以清晰看到程序的结构,其中“Check_Click_40864C”、“ Form_Load_408040”、“ Proc_0_2_4081F0”都是点窗口创建后直接相关的函数调用。
2.2 Form_Load_408040函数分析
Form_Load_408040在生成窗体时提前调用(Vb中form_load和form_click有什么区别),首先查看这个函数,其主要功能是对一些全局变量赋值,即初始化数据。

2.3 Proc_0_2_4081F0函数分析
在此分析Proc_0_2_4081F0函数,其在Check_Click_40864C函数中被调用,并且可以看出传入的参数是输入的数据。

进入函数中(这里用nodepad++复制显示反编译代码,可以自动换行,便于观察),可以看到这里对输入内容进行了限制,输入的每个字符的ASCII码的范围是在“0×30—0×39”和“0×61—0×66”之间,查询ASCII码对照表为数字0—9字母a—f,即16进制数字。则函数的主要内容是判断输入字符是否为16进制数字。

2.4 Check_Click_40864C函数分析
Check_Click_40864C函数是程序的主要部分,通过观察可以发现最后的if判断语句满足条件:var_94等于global_104(参见Form_Load_408040函数其值为874402299.931726),var_8C等于global_100(其值为373414231.362502)则会提示正确的结果。

可以看到var_94和var_8C的数值是通过多个数字变量多次计算得出的,其中global_76的数值为1711722997(0x6606D1F5),global_84和global_92的数值分别是输入数据的左8位和右8位组成的数字。除此之外还有global_52、global_68等这样的无从查找的数据参与运算,在此通过Ollydbg动态调试的方法确定其数值。
Ollydbg动态调试VB程序的下断点方法与VC有些差别,在这里我们在计算var_94的位置进行分析调试从而确定未知数据,首先转到0x4084CA地址,在此下内存访问断点。

断点下好之后,随便输入数据然后点击“Check it”按钮运行程序,程序会断在VB真正的执行代码处,可以看到在此是将内存断点处的数据取出后计算出真实的地址并jmp跳转过去执行。

通过动态调试的方法定位到global_52取值的位置,可以看到是通过fld将global_52的值放到了FPU寄存器当中,其数值为0,通过同样的方法可以确定其他变量的值也为0。

这里令global_84=a,global_92=b,global_76=c,global_104=N1,global_100=N2。可以将 VB Decompiler 反编译得出的过程替代如下。
N1=0.5*(a^4+b^4+c^4+2*a^2*b^2+2*b^2*c^2+2*a^2*c^2)^0.5/(a+b+c) N2=(a*b*c)/((a+b+c)*(a+b-c)*(a+c-b)*(b+c-a))^0.5 c=1711722997 N1=874402299.931726 N2=373414231.362502
经过计算无法得到a、b的值,怀疑程序中出现某些错误,重新审视程序运行过程。
3. 程序反调试分析
3.1单步调试陷阱
从入口点地址0×401090开始动态跟踪调试,程序首先跳转到地址0x408E00处。
![]()
跟踪到地址0x408E00处,可以看到执行了“push eax”指令后跳转到了地址0x408EE0处,而在此地址处存放了“push edx”,执行后跳转到了地址0x408E20处。
![]()

之后的跳转指令和执行指令都成对出现,将执行代码分散存储,这里去掉跳转指令还原执行指令如下:
00401090 > 68 90754000 push 00407590 00401095 E8 667D0000 call 00408E00 00408E00 50 push eax 00408EE0 52 push edx 00408E20 16 push ss 00408EC0 17 pop ss 00408EC1 9C pushfd 00408E40 5F pop edi 00408EA0 81E7 00010000 and edi, 100 00408E60 C1EF 08 shr edi, 8 00408E90 47 inc edi 00408E80 B8 78780000 mov eax, 7878 00408E50 66:F7E7 mul di 00408EB0 BF 78014100 mov edi, 00410178 00408ED0 2BF8 sub edi, eax 00408E30 5A pop edx 00408E70 58 pop eax 00408E10 57 push edi 00408EF0 C3 retn
这段执行代码中隐藏有单步调试陷阱,当单步跟踪到地址0x408EC1处时,可以观察到EFL寄存器的数值为0×246,但是堆栈处压入的数值为0×346。通过一系列与或加减运算之后,在0x408E10处edi的数值为0×401088(即VB程序入口点)。


在地址0x408EC1处下断点后,不通过单步而是直接运行到此处,可以观察到EFL寄存器和堆栈中的数值均为0×246。通过一系列与或加减运算之后,在0x408E10处edi的数值为0×408900,程序跳转到了另一段代码空间当中。


这段代码的单步调试陷阱(堆栈寄存器反调试技术)发生在地址0x408E20处,在对堆栈寄存器ss采取pop操作时,其下一条指令会被执行,此时调试器无法停下来。后面的“pushf”指令会隐式执行,且调试器没办法更改压入栈中的陷阱标志(即FLG寄存器的TF标志位)的设置,此程序中0×346与0×246的区别就在于TF标志位的不同。

当单步跟踪此段代码时,反调试触发,效果就是直接跳转到VB程序入口点,若在地址0x408EC1之后下断点,越过单步调试陷阱,则可避开反调试,跳转到地址0×408900的代码处执行。
3.2 动态代码加密
从地址0×408900开始动态跟踪调试,程序首先跳转到地址0x408E00处。同样去掉跳转指令还原执行指令如下:
00408900 50 push eax 004088F4 B8 26FFBFFF mov eax, FFBFFF26 00408908 F7D0 not eax 00408911 8B00 mov eax,dword ptr [eax] 004088EA C1E0 03 shl eax, 3 0040891A 83F0 57 xor eax, 57 004088DF A3 E5884000 mov dword ptr [4088E5], eax 004088E4 /E9 D7000000 jmp 004089C0
这段代码中隐藏有动态加密(SMC),若此时执行程序则会跳转到地址0x4089C0处,之后进入VB程序入口点。在地址0×408911处的eax在经过运算之后为0x4000D9,此处内存中的值为10。

在内存中查看此地址处的数据,发现与修改的入口点地址0×401090相同。

在Stud_PE工具中加载程序,查看入口点地址的内存偏移位置即为D8,则地址0×408940处的代码是提取入口点地址的内容。

将地址0x4000D9的内容手动修改为0,则与为入口点地址为0×000000时的状态相同。

在地址0x4088DF处下断点,可以看到下一条指令跳转到地址0x4089C0,当执行了mov指令之后,跳转地址改为了0×408940,跳转到了新的代码处执行。后面代码多次运用了mov指令SMC在此不一一列举。


3.3 beingdebugged反调试
继续动态跟踪调试到地址0x408F01开始,去掉跳转指令还原执行指令如下:
00408F01 64:8B0D3000000> mov ecx, dword ptr fs:[30] 00408F28 0FB649 02 movzx ecx, byte ptr [ecx+2] 00408F45 33C0 xor eax, eax 00408F47 40 inc eax 00408F48 2BC1 sub eax, ecx 00408F63 66:B9 0400 mov cx, 4 0040880B 66:F7E1 mul cx 0040880E 3005 27844000 xor byte ptr [408427], al 0040882D 3005 41844000 xor byte ptr [408441], al 0040884C 3005 93844000 xor byte ptr [408493], al 0040886B 66:B9 0600 mov cx, 6 00408888 66:F7E1 mul cx 0040888B 3005 CB844000 xor byte ptr [4084CB], al 004088AA 3005 7B854000 xor byte ptr [40857B], al
这段代码中隐藏有beingdebugged反调试(基于内存差异的反调试技术),在地址408F01处的程序代码,通过两次mov指令得到当前进程PEB的第2个字节的值,当进程被调试时,系统会设置这个变量为1。可以查看到ecx的值为1。

运行到地址0x40880E处后,al的值为0,则后面参与xor计算书的内存值都将变为0,此时直接运行程序会弹出错误提示。


在beingdebugged反调试处将ecx的值设定为0,则可以避开反调试,运行到地址0x40880E处后,al的值为4,则后面参与xor计算书的内存值都将改变。

地址0x40880E连续三处指令修改内存由0xAB改变为了0xAF。


地址0x4084CB处修改内存由0x6c改变为0×74。


地址0x40857B处修改内存由0×74改变为0x6c。


此处的5次内存修改不同于上面的SMC,上面的SMC修改完毕后直接执行,此处的5次修改并没立即执行,那么分析一下此处修改的作用。由于没有在此处执行,那么应该是在VB程序中执行,在VB Decompiler中寻找一下这5次内存位置如下:
loc_408427: AddR8 loc_408441: AddR8 loc_408493: AddR8 loc_4084CA: FStFPR8 var_94 loc_40857A: FStFPR8 var_8C
用HxD工具修改exe程序这五处的值。

再次在VB Decompiler中加载观察如下:
loc_408427: SubR4 loc_408441: SubR4 loc_408493: SubR4 loc_4084CA: FStFPR8 var_8C loc_40857A: FStFPR8 var_94
可以看出其作用是将加法指令换成了减法指令,将变量var_8C和变量var_94进行了调换。

则令global_84=a,global_92=b,global_76=c,global_104=N1,global_100=N2。可以将 VB Decompiler 反编译得出的过程替代如下。
N2=0.5*(-a^4-b^4-c^4+2*a^2*b^2+2*b^2*c^2+2*a^2*c^2)^0.5/(a+b+c) N1=(a*b*c)/((a+b+c)*(a+b-c)*(a+c-b)*(b+c-a))^0.5 c=1711722997 N1=874402299.931726 N2=373414231.362502
那么计算出a、b的值就可以得到正确的结果。
3.4 rdtsc时间反调试
完成以上分析即可得出结果,这里再跟踪程序中其他的反调试手段,理清整个反调试思路。
在动态跟踪调试到地址0x408F01之前,在地址0×401151处动态加载的真实指令为rdtsc(断点可以下在0×401147,经SMC后可得),之后指令为上面分析的beingdebugged反调试过程,直接运行到地址0x408F8A处动态加载的真实指令为rdtsc(断点可以下在0x408F80,经SMC后可得)。从地址0x408F01开始,去掉跳转指令还原执行指令如下:
00401151 > /0F31 rdtsc 0040115D . 8BD8 mov ebx,eax . . . 00408F8A . 0F31 rdtsc 00408F96 . 2BC3 sub eax,ebx 00408FA2 ? 3D00000003 cmp eax,0x3000000 00408FB1 >^\0F8F 9A81FFFF jg 看雪CTF2.00401151 00408FB7 . /EB 01 jmp short 看雪CTF2.00408FBA 00408FBA > \68 C18F4000 push 看雪CTF2.00408FC1 00408FC1 . 83F8 00 cmp eax,0x0 00408FCE >^\0F8C 7D81FFFF jl 看雪CTF2.00401151 00408FD4 . /EB 01 jmp short 看雪CTF2.00408FD7
可以看出这是一个循环程序,地址0x408FA2处判断时间差是否大于0×3000000,小于这个时间差则跳出循环,否则循环将一直执行,这里直接在地址0x408FBA处下断点,不要在循环中纠缠,时间差不会那么大,直接执行到0x408FBA处,最终执行到0x408FD4避开时间反调试。
3.5 DLL检测反调试
经时间反调试之后,跟踪代码运行到地址0x408B0E处,去掉跳转指令还原执行指令如下:
00408B0E . mov ecx,dword ptr fs:[0x30] 00408B23 . 8B49 0C mov ecx,dword ptr ds:[ecx+0xC] 00408B34 . 8B49 0C mov ecx,dword ptr ds:[ecx+0xC] 00408B45 ? 8BD9 mov ebx,ecx 00408B55 . 8B11 mov edx,dword ptr ds:[ecx] 00408B65 ? 3BDA cmp ebx,edx 00408B75 . /0F84 3E010000 je 看雪CTF2.00408CB9 00408B89 . 8B79 30 mov edi,dword ptr ds:[ecx+0x30] 00408B9A ? 66:833F 62 cmp word ptr ds:[edi],0x62 00408BAC . /0F85 37010000 jnz 看雪CTF2.00408CE9 . . . 00408CE9 > \8BCA mov ecx,edx 00408CF9 >^/E9 49FEFFFF jmp 看雪CTF2.00408B47 00408B47 > /68 4E8B4000 push 看雪CTF2.00408B4E 00408B4E . 68 558B4000 push 看雪CTF2.00408B55
这里通过访问进程PEB数据得到不包含路径的模块名,即获得程序名和加载的DLL名。
(1)mov ecx, fs:[0x30] 得到PEB的地址;
(2)mov ecx, ds:[ecx+0xC] 得到Ldr的地址;
(3)mov ecx, ds:[ecx+0xC] 得到按加载顺序的模块列表;
(4)mov edi, ds:[ecx+0x30] 得到不包含路径的模块名。
地址0x408B9A处将模块名的第一个字符与0×62比较,这里如果不相等则继续获取下一个模块名,如果相等则继续比较后面的字符是否为0×64、0×61、0×73、0x6d,这里代码省略了没有贴出来,用Ollydbg动态跟踪可以得到是与“bdasm”字符串进行比较。
![]()
![]()
![]()
![]()
![]()
随后代码运行到地址0x408C50处,运行此处代码之后程序数据发生改变。

在此直接运行程序会弹出错误提示。

可以看到此处的DLL检测是查询是否加载了名称为“bdasm”的DLL,这个DLL是WKTVBEdbugger工具所调用的DLL。则此处的DLL检测反调试是防止使用WKTVBEdbugger对其进行调试分析。

4.Keygen设计
4.1 算法分析
1.根据上面分析结果计算出公式中a、b的值就可以得到正确的结果,那么首先可以化简公式,这里取因式分解的化简步骤。
-a^4-b^4-c4^+2*a^2*b^2+2*b^2*c^2+2*a^2*c^2 = 2*a^2*b^2+2*b^2*c^2-b^4-(a^4+c^4-2*a^2*c^2) = (2*a^2+2*c^2-2*a*c+2*a*c)*b^2-b^4-(a^2-c^2)2 = ((a+c)^2+(a-c)^2)*b^2-b^4-(a+c)^2*(a-c)^2 = (a+c)^2*b^2-b^4+(a-c)^2*b^2-(a+c)^2*(a-c)^2 = ((a+c)^2-b^2)*b^2-((a+c)^2-b^2)*(a-c)^2 = ((a+c)^2-b^2)*( b^2-(a-c)^2) = (a+b+c)*(a+b-c)*(a+c-b)*(b+c-a)
那么可以消除根号下(0.5次幂)的数据,化简得到:
b=2*N1*N2*(a+c)/(a*c-2*N1*N2)
那么求解a、b的值只需要同时满足a、b是8位16进制数字,即a在0×0到0xffffffff之间取值(十进制0—4294967295),计算出的b落在此区间内则符合条件得出结果。同时可以做以下化简缩小a的范围。
a*c-2*N1*N2> 0 a > 2*N1*N2/c
4.2 Keygen编写
1. Python代码
# python 3.6.1
import time
c = 1711722997
N1 = 874402299.931726
N2 = 373414231.362502
N1_s = '874402299.9317'
def main():
start_time = time.clock()
min = int(2 * N1 * N2 / c)
max = 4294967295+1
for a in range(min, max):
tmp = 2 * N1 * N2
b = (tmp * (a + c)) / (a * c -tmp)
if (a%10000000) == 0 :
print('complete %.4f%% no result,'%(a/(max-min)),'now a is %d'%(a))
if b >= 0 and b <= 4294967295 and b == int(b):
if str((a*b*c)/(((a+b+c)*(a+b-c)*(a+c-b)*(b+c-a))**0.5)).startswith(N1_s):
print('---------------OK--------------------')
print(' a=', a, ';' 'b=', b)
print(' a=', hex(a), ';' 'b=', hex(int(b)))
print(' Found Res is:', str(hex(a))[2:], str(hex(int(b)))[2:])
print(' Use time: %.3f min'% ((time.clock()- start_time)/60))
break
if __name__ == "__main__":
main()
2.运算结果

4.3 其他算法
1.MATLAB计算
syms a syms b c=1711722997.0 N1=874402299.931726 N2=373414231.362502 eq1=a*b*c-N1*sqrt((a+b+c)*(a+(b)-c)*(a+(c)-b)*(b+(c)-a)) eq2=2*N2*(a+b+c)-sqrt((a+b+c)*(a+(b)-c)*(a+(c)-b)*(b+(c)-a)) [a,b]=solve(eq1,eq2,a,b) a=real(vpa(a)) b=real(vpa(b))
2.数学公式
N2=0.5*(-a^4-b^4-c^4+2*a^2*b^2+2*b^2*c^2+2*a^2*c^2)^0.5/(a+b+c)
由上面因式分解可将此公式为可以化简为:
N2=0.5*((a+b-c)*(a+c-b)*(b+c-a))^0.5/(a+b+c)^0.5
此公式是边长为a、b、c的三角形内切圆半径N2的计算公式
N1=(a*b*c)/((a+b+c)*(a+b-c)*(a+c-b)*(b+c-a))^0.5
此公式是边长为a、b、c的三角形外接圆半径N1的计算公式。同时另两圆心距离为d,使用欧拉公式。
d = (N1^2-2*N1* N2)
本题作者通过CAD等画图工具参数化作图得出答案。
5.总结进步
1.VB Decompiler工具可以较好地反编译VB程序进行静态分析,程序反调试禁用了WKTVBEdbugger工具进行动态调试分析,以后遇到VB程序可以适时使用此工具进行分析。
2.遇到“pop ss”和“pushf”连接使用的时候,可以回想堆栈寄存器反调试技术,注意单步调试会出现问题。
3.可以将执行代码以数据形式放在程序中,当执行时,将其复制到相应的地址处改写原有程序,实现动态的代码加密执行,这里还可以对这种数据形式进行加密,复制之前解密之后再动态加载执行。


4.类似简单的beingdebugged反调试手段,可以通过Ollydbg插件直接进行规避。


5.rdtsc指令出现的时候,可以考虑时间反调试情况的出现。
6.在DLL检测反调试时,对PEB操作的“mov ecx,fs:[0x30]”指令要相对敏感一些,这些操作有可能涉及到一些反调试或者动态加载代码等技术。代码执行到地址0x408C50处,没有搞清楚为何运行之后程序可执行代码内存数据发生了改变。
7.最后Keygen算法设计时,由于运算数据量较大,只运行了0.4%的数据之后,就得出了结果,但耗时21分钟,其中有运气成分,运算速度太慢,可以适时考虑多线程计算。
6. 参考资料
2.入口点地址为0
6.Vb中form_load和form_click有什么区别
8.FPU寄存器
10.TF标志位
11.基于内存差异的反调试技术
12.PEB结构
13.rdtsc指令详解
14.LDR_MODULE结构
15.分析文档
Via: freebuf


