第三部分 逆向分析技术应用
第14章 PEiD的工作原理分析
14.1 开发环境的识别
PEiD是一款很好的PE文件分析工具。对于一个标准的PE文件,PEiD可以分析出它是由哪款编译器生成的等信息。学习本章前,读者需要掌握一些简单的与PE文件结构相关的知识,否则将无法理解本章的内容,相关资料可参考《Windows PE权威指南》[1]。
在分析PEiD的工作原理之前,只有了解该软件的功能,才知道要分析哪个部分。下面以查看文件是由哪个编译器生成的为例,使用PEiD打开上一章生成的示例程序,如图14-1所示。
图 14-1 PEiD界面
从图14-1中可以看出,PEiD已经分析出示例程序是由Microsoft Visual C++6.0编写的。PEiD不仅可以分析出生成PE文件的编译器的版本,在PE文件经过加壳处理后,还可以分析出相应的加壳版本。这个神奇的功能是如何实现的呢?有了逆向分析的知识,这个问题将会变得很简单。PEiD的版本较多,为了便于学习,本章所使用的版本为脱壳后的V0.94版,如果读者所使用的版本与本书有所不同,则会有些差别。
确定了分析程序的版本,接下来该如何入手呢?首先使用OllyDBG加载并调试PEiD。利用OllyDbg插件选项中的超级字符串参考功能,查看PEiD中的所有字符信息。为什么要这样做呢?图14-1中的“Microsoft Visual C++6.0”是一个字符串信息,这个字符串信息一般不会是PE文件提供的。在PEiD的加载模块中,通过超级字符串参考功能,即可准确定位到字符串所在的内存地址。有了这个地址信息就可以守株待兔,设置好断点等待字符串的到来。有了初步的判断,再结合OllyDbg的超级字符串参考进行分析,如图14-2所示。
图 14-2 PEiD字符串信息
图14-2中又出现了我们再熟悉不过的“Microsoft Visual C++6.0”,这个字符串的首地址为0x00405A28。我们的下一步操作就是在读取这个地址的代码处设置好断点,再次运行程序并加载VC++6.0所开发的应用程序,程序运行到此处的结果如图14-3所示。
图 14-3 读取字符串0x00405A28的操作代码
在图14-3中,程序流程停留在地址0x00438FFF处。程序运行到这里时,早已通过PE文件的判定阶段,因为得到了结果才会定位到显示结果“Microsoft Visual C++6.0”的流程中。我们需要查看调用到此处的代码在哪里,单击地址0x0043FFD处,OllyDBG会画出红线作为指示,跟踪到红线的另一端进一步分析该程序,如图14-4所示。
图 14-4 读取字符串0x00405A28的操作代码的起始地址
顺藤摸瓜,找到调用字符串的地址为0x00438D13,如图14-4所示。这是一个比较跳转指令,是根据edx与edi中的比较结果来确定的。这两个寄存器中又保存了哪些数据呢?找到这个函数的入口处,记录下地址信息,使用IDA分析PEiD此处的函数,如代码清单14-1所示。
代码清单14-1 0x00438D13地址处的函数—IDA分析
;反汇编代码截取自IDA
00438C20 sub_438C20 proc near
00438C20 var_488=dword ptr-488h
00438C20 var_484=byte ptr-484h
00438C20 var_483=byte ptr-483h
;定义了大量1字节大小的连续变量,初步怀疑是数组结构,从488h~450h
00438C20 var_450=byte ptr-450h
;有区别的局部变量
00438C20 var_44C=dword ptr-44Ch
00438C20 var_448=byte ptr-448h
00438C20 var_408=byte ptr-408h
00438C20 arg_0=dword ptr 4;参数标号定义
00438C20 arg_4=dword ptr 8;参数标号定义
00438C20 sub esp,488h;共开辟了488h字节大小的局部变量
00438C26 push ebx;保存环境信息
00438C27 push ebp;同上
00438C28 push esi;同上
00438C29 push edi;同上
00438C2A mov al,72h;赋值al为72h(等于十进制114)
00438C2C mov[esp+498h+var_469],al;特征码定义
00438C30 mov[esp+498h+var_467],al
00438C34 mov[esp+498h+var_464],al
00438C38 mov[esp+498h+var_45F],al
00438C3C mov[esp+498h+var_45B],al
00438C40 mov al,63h;同上
00438C42 mov[esp+498h+var_458],al
00438C46 mov[esp+498h+var_457],al
00438C4A mov al,73h;同上
00438C4C mov[esp+498h+var_455],al
00438C50 mov[esp+498h+var_454],al
00438C54 mov al,6Ch;同上
00438C56 mov[esp+498h+var_451],al
00438C5A mov[esp+498h+var_450],al
00438C5E mov esi,[esp+498h+arg_4];esi保存第二个参数
;结合OD分析,eax获取到的数据为PE格式中IMAGE_NT_HEADERS头部所在的地址
00438C65 mov eax,[esi+0Ch]
;结合OD分析,edx获取到的数据为".text"节的首地址
00438C68 mov edx,[esi+18h]
00438C6B mov cl,6Dh;特征码定义
00438C6D mov[esp+498h+var_462],cl
00438C71 mov[esp+498h+var_45A],cl
00438C75 mov bl,41h;特征码定义
00438C77 mov[esp+498h+var_46C],7Bh
00438C7C mov[esp+498h+var_46B],4Fh
00438C81 mov[esp+498h+var_46A],75h
00438C86 mov[esp+498h+var_468],50h
00438C8B mov[esp+498h+var_466],6Fh
00438C90 mov[esp+498h+var_465],67h
00438C95 mov[esp+498h+var_463],61h
00438C9A mov[esp+498h+var_461],44h
00438C9F mov[esp+498h+var_460],69h
00438CA4 mov[esp+498h+var_45E],7Dh
00438CA9 mov[esp+498h+var_45D],5Ch
00438CAE mov[esp+498h+var_45C],bl
00438CB2 mov[esp+498h+var_459],bl
00438CB6 mov[esp+498h+var_456],65h
00438CBB mov[esp+498h+var_453],2Eh
00438CC0 mov[esp+498h+var_452],64h
00438CC5 mov[esp+498h+var_480],4Dh
00438CCA mov[esp+498h+var_47F],53h
00438CCF mov[esp+498h+var_47E],43h
00438CD4 mov[esp+498h+var_47D],46h
;eax为IMAGE_NT_HEADERS的首地址,首地址偏移6后取出数据,这个数据在PE格式中对应的是节数目,然后保存在eax中
00438CD9 movzx eax, word ptr[eax+6]
00438CDD lea ecx,[eax+eax*4];节数目乘以5
00438CE0 mov ebp,[edx+ecx*8-18h];计算后得到".data"文件中所占的大小
;经过计算偏移后,eax保存了".data"节的首地址
00438CE4 lea eax,[edx+ecx*8-28h]
00438CE8 mov edi,[eax+14h];获取".data"在磁盘中的偏移
00438CEB mov eax,[esi+4];获取第二个参数指向结构中的第二项数据
;对".data"节首地址加上自身长度,这样使edi指向了".data"节末尾
00438CEE add edi, ebp
00438CF0 mov ebp,[esp+498h+arg_0];获取第一个参数
00438CF7 lea ecx,[edi+3900h]
00438CFD cmp eax, ecx
00438CFF jnb short loc_438D1B;检查比较,如果成功,则跳过oep检查
00438D01 mov edx,[ebp+20h];获取程序到oep,程序入口地址
00438D04 test edx, edx;检查oep
00438D06 jz short loc_438D1B
00438D08 mov ecx,[esi+18h];获取".text"节的首地址
00438D0B mov edi,[ecx+14h];获取".text"文件的偏移
00438D0E add edi,[ecx+10h];".text"文件偏移加文件大小
00438D11 cmp edx, edi;检查oep是否在".text"节中
00438D13 jb loc_438FFD;跳转成功,检查结束
;部分代码分析略
loc_438FFD:
00438FFD push 18h;确定编译器版本
00438FFF push offset aMicrosoftVis_1;"Microsoft Visual C++6.0"
在代码清单14-1中,进入函数不久后就定义了一个很大的数组,并对数组进行了初始化。这个数组中存放的数据为相关特征码。经过分析,此段代码只检查了oep是否在“.text”中,若条件成立,则跳转到显示编译器版本的代码处。
虽然成功地找到了VC++6.0的判定流程,但由于第一个条件判断就确定了结果,导致之前所定义的相关特征码都没有用到。再次分析程序流程,修改地址0x00438D13的jb跳转为nop,继续执行,查看程序流程将会出现哪些特征判定,如代码清单14-2所示。
代码清单14-2 修改跳转指令后的流程—OllyDBG调试
00438D13 nop
;其余nop略
00438D18 nop;此处代码为对代码清单14-1中最后一个jb跳转的修改
;以下检查为oep检查失败的情况
00438D19 mov edi, edx;将oep传入edi中
00438D1B sub eax, edi;eax中保存了".data"节的结尾地址
00438D1D cmp eax,9;检查oep是否在".data"节中
00438D20 jb upPEiD.00438FFD
00438D26 mov edx, dword ptr ds:[esi]
;获取分析程序在PEiD内存中的oep位置
00438D28 lea ecx, dword ptr ds:[edx+edi]
00438D2B mov edx, dword ptr ds:[ecx];edx中保存了oep处前4字节的数据
00438D2D cmp edx,74736E49;特征比较
;oep数据0x6AEC8B55,两者不等,跳转成立
00438D33 jnz short upPEiD.00438D4A;跟踪到地址0x00438D4A处
;如果跳转失败,则继续检查oep的后4字节数据
00438D35 cmp dword ptr ds:[ecx+4],536C6C61
;这两处共检查了oep处8字节的特征码,拼接后为49 6E 73 74 61 6C 6C 53
00438D3C jnz short upPEiD.00438D4A;同样,这里也会跳转到0x00438D4A
;=====================判定为其他编译器所编译的程序====================
00438D3E push 17;压入显示字符串长度
00438D40 push upPEiD.00405AB4;压入显示字符串
00438D45 jmp upPEiD.00439004;修改流程到显示字符串函数调用处
;==================================================================
00438D4A cmp edx,61746164;继续oep特征码比较
;……
;略去部分其他版本的分析
;……
00438E40 mov eax, dword ptr ds:[esi+c];获取IMAGE_NT_HEADERS位置
00438E43 mov eax, dword ptr ds:[eax+28];获取代码段位置
00438E46 push eax
00438E47 mov ecx, esi
00438E49 call upPEiD.00453280;计算偏移值,得到正确的oep
00438E4E mov edi, eax
00438E50 mov eax, dword ptr ds:[esi+18];获取".text"节首地址
00438E53 mov ecx, dword ptr ds:[eax+14];获取".text"节磁盘偏移
;获取".text"节占用的磁盘大小,将ecx调整到末尾
00438E56 add ecx, dword ptr ds:[eax+10]
00438E59 cmp edi, ecx;比较oep是否在".text"节中
00438E5B jb upPEiD.00438FF6;跳转到显示编译器版本处
通过对代码清单14-2的分析,我们可以更加深入地了解PEiD分析VC++6.0所编译的程序的流程。如果oep不在“.text”节中,程序会先根据入口特征码比较oep入口处的8字节机器码,分析目标是否为其他编译器所生成。如果不符合特征,将会调整oep,加入文件偏移与虚拟地址偏移的转换过程,再次用特征码对比oep处的机器码。如符合特征,则程序流程进入字符串“Microsoft Visual C++6.0”的文本输出部分。
代码清单14-2只是VC++6.0判定过程的一部分。真正的判定部分我们还没有接触到。也许读者会有些疑问,VC++6.0的判定过程不是已经在函数地址0x00438D13中了吗?如果你这样想,那就大错特错了,因为地址0x00438D13已经是判定过程的结尾了。代码清单14-2并没有对编译器的分类进行判断处理,这里是经过分类处理后所执行的代码。那么如何找到代码清单14-2的调用处呢?
首先,需要定位到函数地址0x00438D13的调用函数,利用OllyDBG的栈窗口,根据函数的调用机制,函数被调用后会在栈的最顶端压入函数的返回地址,这样就给了我们线索。事不宜迟,在地址0x00438D13处设置断点,运行程序并查看栈窗口信息,如图14-5所示。
图 14-5 栈中返回的地址信息
图14-5中显示了在栈地址0x00FBFF04中保存的地址数据0x00452F46,OllyDBG已经标注出了这个地址就是函数的返回地址。有了返回地址后,单击反汇编视图窗口,按下Ctrl+G组合键,输入地址0x00452F46并定位到返回函数中,如图14-6所示。
图 14-6 返回地址处的代码信息
图14-6中的地址0x00452F3F处就是代码清单14-2中函数的返回地址。从寻址方式上观察,这是一个存放函数指针的数组类型,首地址在0x00401E8C处,ecx中保存着数组的下标值。这个下标值又是由eax计算所得,以此为线索即可找到PEiD的分析答案。首先来观察一下这个函数指针数组,如图14-7所示。
图 14-7 函数指针数组
图14-7只是这个数组的冰山一角,这个数组中还存储了大量的数据,这里就不一一展示了。这些函数地址对应的都是其他编译器的处理过程。接下来,我们沿着获取下标值的“脚印”找到这个函数的首地址处并进行分析,如代码清单14-3所示。
代码清单14-3 编译器检查分类函数—OllyDBG调试
00452E90 mov eax, dword ptr fs:[0];函数入口
00452E96 push-1
00452E98 push upPEiD.0046CDC8;异常处理
00452E9D push eax
00452E9E mov dword ptr fs:[0],esp
00452EA5 sub esp,10;申请局部变量空间
00452EA8 push esi
00452EA9'mov esi, dword ptr ss:[esp+24];参数一
00452EAD mov eax, dword ptr ds:[esi+14]
00452EB0 mov eax, dword ptr ds:[eax+10];获取代码段起始rva
00452EB3 push edi;压入参数一
00452EB4 mov edi, ecx;获取this指针
00452EB6 push eax;压入代码段起始rva
00452EB7 mov ecx, esi
;检查PE文件将式,将oep的文件偏移与虚拟地址偏移进行转换
00452EB9 call upPEiD.00453280
00452EBE cmp eax, dword ptr ds:[esi+4];函数返回调整后的oep
00452EC1 jb short upPEiD.00452ED8;跳转成功,进入分析阶段
;==========================进入分析失败流程==========================
00452EC3 pop edi;无法分析的程序
00452EC4 xop al, al
00452EC6 pop esi
00452EC7 mov ecx, dword ptr ss:[esp+10]
00452ECB mov sword ptr fs:[0],ecx
00452ED2 add esp,1c
00452ED5 retn 8
;===================================================================
00452ED8 push ebx
00452ED9 mov ebx, dword ptr ss:[esp+30]
00452EDD mov dword ptr ds:[ebx+20],eax;保存调整后的oep
00452EE0 mov ecx, dword ptr ds:[esi+4]
00452EE3 mov edx, dword ptr ds:[esi]
00452EE5 sub ecx, eax
00452EE7 push ecx
00452EE8 add edx, eax
00452EEA push edx;载入内存后的程序入口地址
00452EEB lea ecx, dword ptr ss:[esp+14]
00452EEF push ecx
00452EF0 mov ecx, edi
;在此函数中将oep代码与特征码进行了对比,这是一个重要的函数,有了它,PEiD
;可以检查出分析程序是否在可识别的编译器范围内
00452EF2 call upPEiD.0045A3E0
00452EF7 mov eax, dword ptr ss:[esp+14]
00452EFB mov ecx, dword ptr ss:[esp+10]
00452EFF mov edx, eax
00452F01 sub edx, ecx
00452F03 sar edx,2
00452F06 push edx
00452F07 push eax
00452F08 push ecx
00452F09 mov dword ptr ss:[esp+30],0
;根据函数0045A3E0对oep处特征码的对比结果,将特征匹配的处理流程的函数指针
;在数组中的下标值都存放在地址"esp+1C"的数组中
00452F11 call upPEiD.004524B0
00452F16 mov edi, dword ptr ss:[esp+1c]
00452F1A mov eax, dword ptr ss:[esp+20]
00452F1E add esp,0c
00452F21 cmp edi, eax
00452F23 je short uppeid.00452f5c;没有匹配的特征函数,结束分析
00452F25 mov eax, dword ptr ds:[edi]
00452F27 push eax
00452F28 push esi
00452F29 push ebx
00452F2A call dword ptr ds:[401e8c];检查".rdata"节是否存在
00452F30 add esp,0c
00452F33 test al, al
00452F35 jnz short uppeid.00452f7f;不存在结束查询分析
00452F37 mov eax, dword ptr ds:[edi]
00452F39 push eax
00452F3A push esi
00452F3B lea ecx, dword ptr ds:[eax+eax*2]
00452F3E push ebx
00452F3F call dword ptr ds:[ecx*4+401e8c];调用分析函数
00452F46 add esp,0c
00452F49 test al, al
00452F4B jnz short uppeid.00452f7f
00452F4D mov eax, dword ptr ss:[esp+14]
00452F51 add edi,4
00452F54 cmp edi, eax
;循环跳转,当没有匹配到对应的处理函数时,将调整下标数组并继续调用处理函数
00452F56 jnz short uppeid.00452f25
代码清单14-3完成了对oep处特征码的比较,并根据比较结果,从图14-7的函数指针数组中找到符合此特征的处理流程,记录下标值,然后将它们保存在另一个存放下标值的数组中。这时第一次过滤已经完成,进入第二次过滤,检查、分析程序中是否存在第二个节,通常VC编译器所编译的程序为“.rdata”节,节名称不作为判断条件。在“.rdata”节也同时存在的情况下,会进行最后一次分析过滤,在下标数组中取出下标值,调用对应的处理函数。PEiD会将oep处的哪些机器码作为特征码进行对比呢?这就需要进一步分析处理函数0x0045A3E0,如代码清单14-4所示。
代码清单14-4 特征码校验分析—OllyDBG
0045A3E0 push-1
0045A3E2 push upPEiD.0046D238;SE处理程序安装
0045A3E7 mov eax, dword ptr fs:[0]
0045A3ED push eax
0045A3EE mov dword ptr fs:[0],esp
0045A3F5 sub esp,14
0045A3F8 push ebx
0045A3F9 push ebp
0045A3FA xor ebx, ebx
0045A3FC push esi
0045A3FD push edi
0045A3FE mov esi, ecx;获取this指针
0045A400 mov dword ptr ss:[esp+10],ebx
0045A404 mov dword ptr ss:[esp+18],ebx
0045A408 mov dword ptr ss:[esp+1c],ebx
0045A40C mov dword ptr ss:[esp+20],ebx;数组清0
0045A410 mov edi, dword ptr ss:[esp+38];获取oep并保存到edi中
0045A414 movzx eax, byte ptr ds:[edi];获取oep地址处的数据
;对this指针进行偏移计算,偏移量为oep首字节数据乘以4再加0x14
0045A417 mov eax, dword ptr ds:[esi+eax*4+14]
0045A41B cmp eax,-1
0045A41E mov ebp, dword ptr ss:[esp+3c];oep差值
0045A422 mov dword ptr ss:[esp+2c],ebx;清空局部变量
0045A426 je short upPEiD.0045A437
0045A428 push ebp
0045A429 push edi
0045A42A push eax
0045A42B lea ecx, dword ptr ss:[esp+20];获取数组首地址
0045A42F push ecx
0045A430 mov ecx, esi;传递this指针
;检查oep处的代码是否与特征码相同,在这个函数中将oep处的字节码与特征
;码进行对比,从oep处开始对机器码和特征码进行比较,比较oep处机器码的下标如下:
;0x0、0x 1、0x 2、0x 3、0x 4、0x 5、0x A、0x F、0x 10、
;0x 11、0x16、0x18、0x1D、0x1E
0045A432 call upPEiD.0045A1D0
0045A437 mov eax, dword ptr ds:[esi+414]
0045A43D cmp eax,-1
0045A440 je short upPEiD.0045A451
0045A442 push ebp
0045A443 push edi
0045A444 push eax
0045A445 lea edx, dword ptr ss:[esp+20]
0045A449 push edx
0045A44A mov ecx, esi
0045A44C call upPEiD.0045A1D0;此函数功能同上
;其余代码分析略
通过对代码清单14-4的分析,终于找到了重要的比较函数0x0045A1D0,这个函数完成了获取分析程序的机器码与事先准备好的特征码的比较,最终提取出了具有相同特性的编译器的版本。示例程序oep处的机器码有哪些呢?如图14-8所示。
图 14-8 示例程序oep处的机器码信息
在图14-8中,以地址0x00401634作为首地址,将内存中的数据拼接成机器码指令。地址0x0040/634处是连续的6字节的数据:0x55、0x8B、0xEC、0x6A、0xFF、0x68,这6字节数据组合成的汇编指令如下:
55 push ebp
8B EC mov ebp, esp
6A FF push-1
68 push
以上机器码将作为特征码进行对比,其余的机器码及汇编指令读者可如法炮制。有了这些线索,PEiD解析编译器版本的流程已经大致清晰,其操作步骤如下:
1)读取分析文件到内存中,并分析出相关PE文件的信息,然后保存。
2)检查oep,计算地址偏移,并修正oep。
3)再次检查oep地址的合法性。
4)将oep处的机器码与特征码进行比较。
5)检查分析文件中是否存在“.rdata”节。
6)根据分析结果获取对应处理函数所在数组中的下标并保存。
7)循环调用处理函数。
8)在处理函数中再次检查。
9)显示编译器版本。
以上是PEiD分析编译器版本的操作流程。至此,PEiD的简单分析就结束了。这里只针对VC++6.0进行了简单分析,此分析方法也可用于其他编译器所生成的PE文件。读者可仿照本节分析流程中所使用的OllyDBG插件功能,找到超级字符串参考选项,定位到特征码校验函数中。以此为线索,从后向前反推程序的执行流程。
[1]国内第一本关于PE的专著(戚利著,书号为978-7-111-35418-5),机械工业出版社于2011年9月出版。