成钢二炼——逆向入门之试水反调试

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. 参考资料

  1.看雪.万能钥匙 CTF 2017 第三题点评及解题思路

  2.入口点地址为0

  3.Visual Basic程序破解 1

  4.Visual Basic程序破解 2

  5.Visual Basic程序破解 3

  6.Vb中form_load和form_click有什么区别

  7.ASCII码对照表

  8.FPU寄存器

  9.堆栈寄存器反调试技术

  10.TF标志位

  11.基于内存差异的反调试技术

  12.PEB结构

  13.rdtsc指令详解

  14.LDR_MODULE结构

  15.分析文档

  16.Python及matlab代码

  17.Crackme程序及Patch程序



Via: freebuf

标签