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++代码,使用此密码进行程序验证。