4.5 一次算法逆向之旅

前面我们了了解各种运算的基础知识,接下来进行一次简单的逆向之旅:通过分析程序的反汇编代码来了解程序的算法,巩固所学知识。

这里要分析的程序为简单的CrackMe程序,是用VC++编写的控制台程序。程序功能是验证输入密码,显示密码验证结果(密码为命令行输入方式),如图4-5所示。

图4-5 CrackMe程序的运行结果

为了尽量减少分析的工作量,程序中没有设置任何错误检查,只有密码加密与密码检查。输入正确的密码后,程序将会显示“密码正确”的字样。本次将使用IDA进行静态分析。使用IDA加载分析程序后,IDA会直接定位到main()函数的入口处,省去了查找main()函数的过程。如果使用OllyDbg进行动态分析,需要先查找并定位到main函数的入口处。分析程序为Release版,如代码清单4-24所示。

代码清单4-24 CrackMe程序分析片段1(Release版)

var_10= byteptr-10h    ;从var_10到var_3都是连续的1字节大小的局部变量
var_F= byte ptr-0Fh
var_E= byte ptr -0Eh
var_D= byte ptr -0Dh
var_C= byte ptr -0Ch
var_B= byte ptr -0Bh
var_A= byte ptr -0Ah
var_9= byte ptr -9
var_8= byte ptr -8
var_7= byte ptr -7
var_6= byte ptr -6
var_5= byte ptr -5
var_4= byte ptr -4
var_3= byte ptr -3
argc= dword ptr 4
argv= dword ptr 8
envp= dword ptr0Ch

代码清单4-24为CrackMe程序在main()函数中的参数以及局部变量定义。在IDA中,正偏移为参数,负偏移为局部变量,详细讲解见第7章。从标号var_3到var_10的变量都占byte(1字节)的连续局部变量,将其暂时看作数组,找到起始标号var_10的地址,按*键转换14个字节数据为数组。转换数组后将标号名称var_10重命名,按N键修改名称为“charNumber14”,如图4-6所示。

图4-6 数据转换数组

按Esc键返回反汇编视图窗口,在反汇编视图窗口的反汇编代码中,所有引用var_3~var_10的地方都被替换成了数组访问方式,如代码清单4-25所示。

代码清单4-25 CrackMe程序分析片段2(Release版)

charNumber14= byte ptr -10h      ;转换后的数组
; main()函数的三个参数:argc命令个数、argv命令行信息、envp环境变量信息
argc= dword ptr 4
argv= dword ptr 8
envp= dword ptr0Ch
sub    esp,10h                        ; 取参数argv数据放入ecx中
mov    ecx,[esp+10h+argv]
mov    al,1
mov    [esp+10h+charNumber14+6],al    ;将数组charNumber14第6项赋值为al,即1
mov    [esp+10h+charNumber14+7],al    ;将数组charNumber14第7项赋值为al,即1
                                      ;即edx中保存为argv[1]
mov    edx,[ecx+4]                    ;在ecx中保存的参数为argv,根据argv类型,这里为
                                      ;argv[1]操作
                                      ;即edx中保存为argv[1]
mov    [esp+10h+charNumber14+0Ah],al  ;将数组charNumber14第10项赋值为al,即1
mov    al, byte ptr[esp+10h+argc]     ;将al赋值为命令行参数个数argc
push    ebx                           ;保存环境
mov    bl,al                          ;将bl赋值为al,即命令行参数个数argc
push    esi                           ;保存环境
dec    bl                             ;对bl执行减等于1操作,等同argc减1
push    edi                           ;保存环境
or    [edx],bl                        ;在edx中保存为argv[1],这步操作为argv[1]
                                      ;[0]|=argc-1    ①
mov    edx,[ecx+4]                    ;在edx中保存为argv[1]
mov    [esp+1Ch+charNumber14], 77h    ;将数组charNumber14第0项赋值为0x77
mov    [esp+1Ch+charNumber14+1], 76h  ;将数组charNumber14第1项赋值为0x76
xor    [edx+1],bl                     ;在edx中保存为argv[1],这步操作为argv[1]
                                      ;[1]^=argc-1    ②
mov    dl,6                           ;修改dl为数值6
imul    dl                            ;对dl做有符号乘法,乘以al,al中保存的数据为命令
                                      ;行参数个数
                                      ;结果存入al中
mov    esi,[ecx+4]                    ;在esi中保存为argv[1]
sub    al,dl                          ;使用al减等于dl
mov    [esp+1Ch+charNumber14+2],0Cah  ;将数组charNumber14第2项赋值为0xCA
mov    [esp+1Ch+charNumber14+3],0F9h  ;将数组charNumber14第3项赋值为0xF9
imul    byte ptr[esi+2]               ;在esi中保存argv[1],此句指令为argv[1]
                                      ;[2]*al,al中保存的值
                                      ;为命令行参数个数乘以6后,再减去6。转换为al =
                                      ;argv[1][2] * (argc - 1) * 6
mov    [esi+2],al                     ;esi+2中的数据等于al,即argv[1][2]=argv[1]
                                      ;[2]*(argc-1)*6  ③
mov    esi,[ecx+4]                    ;在esi中保存为argv[1]
mov    [esp+1Ch+charNumber14+4],0A8h  ;将数组charNumber14第4项赋值为0xA8
mov    [esp+1Ch+charNumber14+5], 0Ch  ;将数组charNumber14第5项赋值为0x0C
movsx    eax, byte ptr[esi+2]         ;取argv[1][2]数据存到eax中
cdq                                   ;扩展高位到 edx
and    edx,3                          ;使用eax扩展后的高位edx与3进行位与运算
mov    [esp+1Ch+charNumber14+8],0Feh  ;将数组charNumber14第8项赋值为0xFE
add    eax,edx                        ;使用eax加扩展高位edx
mov    [esp+1Ch+charNumber14+9],0DBh  ;将数组charNumber14第9项赋值为0xDB
sar    eax,2                          ;将eax右移动2位,此数可套用除法公式,移动次数为
                                      ;2的幂,因此除数为4转换后变为:eax=argv[1][2]/4
mov    [esi+3],al                     ;将al赋值到esi+3,即argv[1][3]=argv[1][2]/4 ④
mov    eax,[ecx+4]                    ;使用eax保存argv[1]
mov    [esp+1Ch+charNumber14+0Bh],0E0h;将数组charNumber14第11项赋值为0xE0
mov    [esp+1Ch+charNumber14+0Ch],0FBh;将数组charNumber14第12项赋值为0xFB
mov    dl,[eax+4]                     ;在eax中保存argv[1],即:argv[1][4]数据存入dl
mov    [esp+1Ch+charNumber14+0Dh],0   ;将数组charNumber14第13项赋值为0x00
                                      ;到此所有的数组成员都被赋值

在代码清单4-25中,对数组charNumber14中的每一项进行赋值,并对命令行参数进行一些计算,这就是在对我们输入的密码信息进行加密。charNumber14数组中保存的就是加密后的密码,分析后得到charNumber14中:“0x77、0x76、0xCA、0xF3、0xA8、0x0C、0x01、0x01、0xFE、0xDB、0x01、0xE0、0xFB、0x00”,共14个字节的数据。这个数组为密码比较数组,这里保存的数据可以为密码加密信息,因此可知密码长度,以及加密后的密文字符串信息。代码清单4-25为CrackMe程序部分的反汇编信息,继续向下分析程序,获取完整的加密过程,如代码清单4-26所示。

代码清单4-26 CrackMe程序分析片段3(Release版)

通过代码清单4-26对命令行参数argv[1]的层层运算,最终得到一个加密后的字符串,与程序中的密文进行比较,如果转换结果一样,表示密码正确,反之密码错误。

在转换过程中,遇到了没有接触过的对2取模运算。2的取模运算相对特殊,由于取模就是求余,所有有符号数对2求余只有3种结果:“-1”“0”“1”。因此编译器进行了优化,对十六进制数0x80000001做位与运算,无论这个数字是多少,只会保留最高位与最低位。如果数字为奇数,则最低位必然为1,会被保留下来,同理,符号位也会被保留。

使用跳转指令JNS判断正负标记位SF。edx和十六进制数0x80000001做位与运算后,如果为负数,则结果为0x80000001,由于存放编码方式为补码,而这个数字并不是补码的-1,需要进行补码转换。如果是正数,则不存在转换问题,直接跳过负数处理部分即可。分析代码清单4-25和代码清单4-26的加密过程可以得出,加密运算步骤共有13步。对这13步加密步骤进行分析并还原后可以得出整个加密过程,如代码清单4-27所示。

代码清单4-27 还原成源码的加密过程

代码清单4-27为CrackMe程序加密算法还原后的代码,使用了大量的位运算。由于这些运算不可逆,所以无法推算回正确的密码。这里给出程序的正确密码:www.51asm.com。读者可自己将CrackMe程序的反汇编代码翻译成对应的C++代码,使用此密码进行程序验证。