4.5 一次算法逆向之旅

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

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

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

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

代码清单4-20 CrackMe程序分析片段1—Release版


var_10=byte ptr-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 ptr 0Ch


代码清单4-20为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-21所示。

代码清单4-21 CrackMe程序分析片段2—Release版


;转换后的数组

charNumber14=byte ptr-10h

;main函数的三个参数,argc命令个数,argv命令行信息,envp环境变量信息

argc=dword ptr 4

argv=dword ptr 8

envp=dword ptr 0Ch

sub esp,10h

;取参数argv数据放入ecx中

mov ecx,[esp+10h+argv]

;将al赋值为1

mov al,1

;将数组charNumber14第6项赋值为al,即1

mov[esp+10h+charNumber14+6],al

;将数组charNumber14第7项赋值为al,即1

mov[esp+10h+charNumber14+7],al

;在ecx中保存的参数为argv,根据argv类型,这里为argv[1]操作

;即edx中保存为argv[1]

mov edx,[ecx+4]

;将数组charNumber14第10项赋值为al,即1

mov[esp+10h+charNumber14+0Ah],al

;将al赋值为命令行参数个数argc

mov al, byte ptr[esp+10h+argc]

;保存环境

push ebx

;将bl赋值为al,即命令行参数个数argc

mov bl, al

;保存环境

push esi

;对bl执行减等于1操作,等同argc减1

dec bl

;保存环境

push edi

;在edx中保存为argv[1],这步操作为argv[1][0]|=argc-1 ①

or[edx],bl

;在edx中保存为argv[1]

mov edx,[ecx+4]

;将数组charNumber14第0项赋值为0x77

mov[esp+1Ch+charNumber14],77h

;将数组charNumber14第1项赋值为0x76

mov[esp+1Ch+charNumber14+1],76h

;在edx中保存为argv[1],这步操作为argv[1][1]^=argc-1 ②

xor[edx+1],bl

;修改dl为数值6

mov dl,6

;对dl做有符号法,乘以al, al中保存的数据为命令行参数个数

;结果存入al中

imul dl

;在esi中保存为argv[1]

mov esi,[ecx+4]

;使用al减等于dl

sub al, dl

;将数组charNumber14第2项赋值为0xCA

mov[esp+1Ch+charNumber14+2],0CAh

;将数组charNumber14第3项赋值为0xF9

mov[esp+1Ch+charNumber14+3],0F9h

;在esi中保存argv[1],此句指令为argv[1][2]*al, al中保存的值为命令行参数个数

;乘以6后,再减去6。转换为al=argv[1][2]*(argc-1)*6

imul byte ptr[esi+2]

;esi+2中的数据等于al,即argv[1][2]=argv[1][2]*(argc-1)*6 ③

mov[esi+2],al

;在esi中保存为argv[1]

mov esi,[ecx+4]

;将数组charNumber14第4项赋值为0xA8

mov[esp+1Ch+charNumber14+4],0A8h

;将数组charNumber14第5项赋值为0x0C

mov[esp+1Ch+charNumber14+5],0Ch

;取argv[1][2]数据存到eax中

movsx eax, byte ptr[esi+2]

;扩展高位到edx

cdq

;使用eax扩展后的高位edx与3进行位与运算

and edx,3

;将数组charNumber14第8项赋值为0xFE

mov[esp+1Ch+charNumber14+8],0FEh

;使用eax加扩展高位edx

add eax, edx

;将数组charNumber14第9项赋值为0xDB

mov[esp+1Ch+charNumber14+9],0DBh

;将eax右移动2位,此数可套用除法公式,移动次数为2的幂,因此除数为4

;转换后变为:eax=argv[1][2]/4

sar eax,2

;将al赋值到esi+3,即argv[1][3]=argv[1][2]/4 ④

mov[esi+3],al

;使用eax保存argv[1]

mov eax,[ecx+4]

;将数组charNumber14第11项赋值为0xE0

mov[esp+1Ch+charNumber14+0Bh],0E0h

;将数组charNumber14第12项赋值为0xFB

mov[esp+1Ch+charNumber14+0Ch],0FBh

;在eax中保存argv[1],即:argv[1][4]数据存入dl

mov dl,[eax+4]

;将数组charNumber14第13项赋值为0x00

;到此所有的数组成员都被赋值

mov[esp+1Ch+charNumber14+0Dh],0


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

代码清单4-22 CrackMe程序分析片段3—Release版


;在代码清单4-21中,dl被赋值为argv[1][4]

;将argv[1][4]左移动3次

shl dl,3

;将结果存入eax+4,即argv[1][4]=argv[1][4]<<3 ⑤

mov[eax+4],dl

;将eax赋值为argv[1]

mov eax,[ecx+4]

;将argv[1][5]赋值到dl中

mov dl,[eax+5]

;将dl向右移2位

sar dl,2

;结果赋值到argv[1][5]中,即argv[1][5]=argv[1][5]>>2 ⑥

mov[eax+5],dl

;将esi赋值为argv[1]

mov esi,[ecx+4]

;在代码清单4-21中,bl为argc减1,且未被修改

;将al赋值为bl,即argc减1

mov al, bl

;取argv[1][6]数据存入dl中

mov dl,[esi+6]

;将al与数字7做位与运算,即(argc-1)&7

and al,7

;将al位与运算后的结果与dl做位与运算

;即argv[1][6]&((argc-1)&7)

and dl, al

;将dl中的数据赋值到esi+6地址处

;即argv[1][6]=argv[1][6]&((argc-1)&7) ⑦

mov[esi+6],dl

;将esi赋值为argv[1]

mov esi,[ecx+4]

;取esi+7地址处1字节数据带符号扩展到edx,即edx=argv[1][7]

movsx edx, byte ptr[esi+7]

;将edx与0x80000001做位与操作,此操作只会保留下edx的最高位与最低位

;此操作会影响标记位SF,当edx为负数时,会修改SF标记位

and edx,80000001h

;SF标记位为0则跳转到标记loc_4010BF处,edx为负数跳转

jns short loc_4010BF

;以下为edx为负数的处理代码

;edx减等于1,由于之前操作会使edx只保留最低位和最高位

;这里对edx减1操作,结果必然为0x80000000

dec edx

;对edx与0xFFFFFFFE做位或运算,除最低位外全部置1

;此时的edx值只有1种可能:0xFFFFFFFE

or edx,0FFFFFFFEh

;对edx加1后,变为0xFFFFFFFF为-1

inc edx

;地址标号;这里为IDA标注引用此标号处

loc_4010BF:

;CODE XREF:_main+B8j

;将dl赋值到esi+7处,这里的操作为对2取模操作,即argv[1][7]%=2 ⑧

mov[esi+7],dl

;eax赋值为argv[1]

mov eax,[ecx+4]

;在bl中保存argc减1,对其取反

not bl

;将bl的值赋值到eax+8地址处,即argv[1][8]=~(argc-1) ⑨

mov[eax+8],bl

;将eax赋值为argv[1]

mov eax,[ecx+4]

;取argv[1][0]数据赋值到dl

mov dl,[eax]

;取argv[1][2]数据赋值到bl

mov bl,[eax+2]

;取argv[1][9]地址到esi,这里使用esi保存地址,为一个指针操作

;esi的使用将指针记作:char*pArgv9

lea esi,[eax+9]

;使用dl减等于bl,即argv[1][0]-argv[1][2]

sub dl, bl

;对esi取内容到bl,即bl=*pArgv9

mov bl,[esi]

;bl加等于dl,即*pArgv9+argv[1][0]-argv[1][2]

add bl, dl

;将bl复制到esi保存地址中:*pArgv9=*pArgv9+argv[1][0]-argv[1][2] ⑩

mov[esi],bl

;将eax赋值为argv[1]

mov eax,[ecx+4]

;将esi中保存数据为argv[1][9]地址,对其加1表示地址向前偏移1字节

;这时esi保存数据为argv[1][10]的地址,即pArgv9+=1

inc esi

;取eax+7地址处1字节数据带符号扩展到edi,即edi=argv[1][7]

movsx edi, byte ptr[eax+7]

;取eax+6地址处1字节数据带符号扩展到eax,即:eax=argv[1][6]

movsx eax, byte ptr[eax+6]

;将eax扩展高位到edx

cdq

;使用有符号除法,即argv[1][6]/argv[1][7]

idiv edi

;对esi指向加1操作,之前esi中保存的数据为argv[1][10]的地址

;加1后,保存的数据为argv[1][11]的地址,即pArgv9+=1;

inc esi

;对esi-1取内容,寻址到argv[1][10],赋值为al, al中保存数据为除法商值

;此条语句即*(pArgv9-1)=argv[1][6]/argv[1][7][11]

mov[esi-1],al

;将eax赋值为argv[1]

mov eax,[ecx+4]

;dl被赋值为argv[1][3]

mov dl,[eax+3]

;bl被赋值为argv[1][1]

mov bl,[eax+1]

;al被赋值为*pArgv9,pArgv9中保存的数据为argv[1][11]的地址

mov al,[esi]

;dl减去bl,即argv[1][3]-argv[1][1]

sub dl, bl

;al加dl,即*pArgv9+argv[1][3]-argv[1][1]

add al, dl

;将al值复制到esi保存地址中,即*pArgv9+=argv[1][3]-argv[1][1]12

mov[esi],al

;将eax赋值为argv[1]

mov eax,[ecx+4]

;赋值dx为argv[1][5]

movsx dx, byte ptr[eax+5]

;赋值dx为argv[1][4]

movsx ax, byte ptr[eax+4]

;执行有符号乘法,eax乘以edx,即argv[1][4]*argv[1][5]

imul edx, eax

;将乘法结果dx复制到esi中,由于dx占2字节,而pArgv9为char,需要转换

;即*(short)pArgv9=argv[1][4]*argv[1][5]13

;指针pArgv9未被修改,仍然指向argv[1][11]的地址

mov[esi],dx

;ecx赋值为argv[1]

mov ecx,[ecx+4]

;这里为strcmp函数调用,比较密码是否正确

;strcmp函数是编译选项,可能会被转换成内联方式,详解略

;函数的讲解见第6章

;到此,整个程序的加密过程就结束了

retn


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

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

通过使用跳转指令JNS判断正负标记位SF。edx和十六进制数0x80000001做位与运算后,如果为负数,则结果为0x80000001,由于存放编码方式为补码,而这个数字并不是补码的-1,需要进行补码转换。如果是正数,则不存在转换问题,直接跳过负数处理部分即可。

根据对代码清单4-21和代码清单4-22的加密过程进行分析可以得出,加密运算步骤共有13步。对这13步加密步骤进行分析并还原后可以得出整个加密过程,如代码清单4-23所示。

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


//main函数定义

void main(int argc, char*argv[],char*envp){

//还原加密算法的①~步

argv[1][0]|=argc-1;

argv[1][1]^=argc-1;

argv[1][2]=argv[1][2]*(argc-1)*6;

argv[1][3]=argv[1][2]/4;

argv[1][4]=argv[1][4]<<3;

argv[1][5]=argv[1][5]>>2;

argv[1][6]=argv[1][6]&((argc-1)&7);

argv[1][7]%=2;

argv[1][8]=~(argc-1);

//这里有一步指针定义操作

char*pArgv9=&argv[1][9];

*pArgv9=*pArgv9+argv[1][0]-argv[1][2];

//执行步骤⑩后,对指针pArgv9进行了加1操作

pArgv9+=1;

//在步骤中,首先对指针pArgv9进行了加1操作,再参与运算。这里为一个前加操作

pArgv9+=1;

*(pArgv9-1)=argv[1][6]/argv[1][7];

*pArgv9+=argv[1][3]-argv[1][1];

*(short)pArgv9=argv[1][4]*argv[1][5];

//密码比对部分使用strcmp进行字符串比较,实现过程略

}


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