从现在开始,面试的问题渐渐深入。这次的三个问题,都是对PE格式的不断深入的提问。从最初的概念,到病毒对PE格式的利用,再到最后的壳的问题。这里需要说明的是,由于壳是一个比较复杂的概念,面试中也仅仅只能对原理性的东西进行提问,但是实际上,是要求面试者能够熟练掌握脱壳乃至写壳的技术的。我这里并没有对此进行特别详细的阐述,所以大家如果想深入理解,还是参考相应的资料,并加强实际的动手能力比较好。
以下是问题7——问题9:
**7、请说明PE文件由几部分构成,并解释什么是导入表、导出表以及附加数据。**
答:(以下内容选自**《Windows PE权威指南》**第3.3.3节——程序员眼中的PE结构)
在程序员眼中,PE文件格式是由许多数据结构组成的,数据结构是一系列有组织的数据的集合,如图3-9所示。
![](https://box.kancloud.cn/2016-02-17_56c428a6d639f.jpg)
图3-9 程序员眼中的PE结构
如图所示,一个标准的PE文件一般由四大部分组成:
□DOS头
□PE头(IMAGE_NT_HEADERS)
□节表(多个IMAGE_SECTION_HEADER结构)
□节内容
其中,PE头的数据结构最为复杂。简单来说,PE头包含:
□4个字节的标识符号(Signature)
□20个字节的基本头信息(IMAGE_FILE_HEADER)
□216个字节的扩展头信息(IMAGE_OPTIONAL_HEADER32)
说明:如果按照“头部+身体”的信息组织方式来看:
PE文件头部 = DOS头 + PE头 + 节表
PE文件身体 = 节内容
节内容中会出现各种不同的数据结构,如导入表、导出表、资源表、重定位表等,关于这些数据的组织方式会在后面的章节中陆续接触到。
(以下内容选自**《Windows环境下32位汇编语言程序设计 典藏版》**第17.2.1节——导入表简介)
在Win32编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。
对于存储在磁盘上的PE文件来说,它无法得知这些导入函数会在内存的哪个地方出现,只有当PE文件被装入内存,Windows装载器才将DLL装入,并将调用导入函数的指令和函数实际所处的地址联系起来,这就是“动态链接”的概念。动态链接是通过PE文件中定义的“导入表”(Import Table)来完成的,导入表中保存的正是函数名和其驻留的DLL名等动态链接所必需的信息。
(以下内容选自**《Windows环境下32位汇编语言程序设计 典藏版》**第17.3节——导出表)
当PE文件被执行的时候,Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中的函数导出信息对被执行文件的IAT表进行修正。在这些包含导出函数的DLL文件中,导出信息被保存在导出表中,通过导出表,DLL文件向系统提供导出函数的名称、序号和入口地址等信息,以便Windows装载器通过这些信息来完成动态链接的过程。
扩展名为.exe的PE文件中一般不存在导出表,而大部分的.dll文件中都包含导出表,但是这并不是必然的,比如,用作纯资源的.dll文件就不提供导出函数,文件中也就不存在导出表;另外,偶尔也可以见到包含导出函数和导出表的.exe文件。
(以下内容选自**《加密与解密 第三版》**第13.6节——附加数据)
某些特殊的PE文件在各个区块的正式数据之后还有一些数据,这些数据不属于任何区块。由于PE文件被映射到内存是按区块映射的,因此这些数据是不能被映射到内存中的,这些额外的数据称为附加数据(overlay)。
附加数据的起点可以认为是最后一个区块的末尾,终点是文件末尾。用LordPE查看实例overlay.exe的区块,如图13.54所示。
![](https://box.kancloud.cn/2016-02-17_56c428a6f2e89.jpg)
图13.54 查看区块信息
从图13.54可以计算出最后一个区块末尾的文件偏移值为3200h+600h=3800h。用十六进制工具打开目标文件,跳到3800h,会发现后面还有一段数据,这就是附加数据,如图13.55所示。
![](https://box.kancloud.cn/2016-02-17_56c428a714a50.jpg)
图13.55 附加数据
用PEiD分析实例overlay.exe,会给出结果“Nothing found [Overlay]*”,其中Overlay就表明有附加数据的存在。带有附加数据的文件脱壳时,必须将附加数据粘贴回去,如果文件有访问附加数据的指针,也要修正。
本节实例overlay.exe实际是用UPX加壳了,由于附加数据的存在,干扰了PEiD分析。用OllyDbg打开实例,来到OEP处。
~~~
00401436 55 push ebp
00401437 8BEC mov ebp,esp
00401439 6AFF push -1
~~~
此时,抓取内存映像保存到磁盘中,然后用ImportREC重建输入表,最终文件为dumped_.exe。
运行实例原文件,然后单击菜单“File/Open”,程序将会读取附加数据并在编辑框中显示出来,如图13.56所示。而运行脱壳后的文件dumped_.exe,不能将原来的文字显示出来,如图13.57所示。
![](https://box.kancloud.cn/2016-02-17_56c428a72d4b9.jpg)
图13.56 读取附加数据
![](https://box.kancloud.cn/2016-02-17_56c428a7457d6.jpg)
图13.57 脱壳后读取附加数据
由于附加数据没有被映射到内存里,因此抓取的映像文件里也没有附加数据。现在将原文件的附加数据移到脱壳后的文件里。用十六进制工具打开overlay.exe,将3800h后的附加数据追加到dumped_.exe文件末尾E000h处。
运行已有附加数据的dumped_.exe,但执行“File/Open”仍不能正确读取数据。用OllyDbg分析一下实例是如何读取自身附加数据的。用CreateFileA设断,执行“File/Open”功能后,会中断到这段代码处。
~~~
00401040 push ecx
00401041 push 0
00401043 call dword ptr [<GetModuleFileNameA>] ;取自身文件名
00401049 push 0
……
00401062 call dword ptr [<&CreateFileA>] ;打开自身
00401068 mov dword ptr [ebp-11C],eax
……
004010DC push 0
004010DE push 0
004010E0 push 3800 ;注意这个值
004010E5 mov edx,dword ptr [ebp-11C]
004010EB push edx
004010EC call dword ptr [<SetFilePointer>] ;移动读写指针
……
0040110E call dword ptr [<&ReadFile>]
00401127 mov ecx,dword ptr [ebp-108]
0040112D push ecx
0040112E mov edx,dword ptr [ebp-10C]
00401134 push edx
00401135 call dword ptr [<SetWindowTextA>] ;将附加数据显示到文本框里
~~~
用CreateFileA打开一个文件后,文件指针默认是指向文件的第一个字节的。程序用SetFilePointer设置指针,指向附加数据,然后用ReadFile将附加数据读取出来。这里SetFilePointer函数比较关键,其原型如下:
~~~
DWORD SetFilePointer(
HANDLE hFile, //文件句柄
LONG lDistanceToMove, //移动的距离,这个是低32位
PLONG lpDistanceToMoveHigh, //移动的距离,这个是高32位
DWORD dwMoveMethod //移动方式
);
~~~
由于脱壳后,文件大小发生变化,追加后的附加数据地址已改变,此处变为E000h,因此需要修正SetFilePointer的参数,将其指向附加数据。
~~~
004010DC push 0
004010DE push 0
004010E0 push 0E000 ;将此处指向附加数据E000h
004010E5 mov edx,dword ptr [ebp-11C]
004010EB push edx
004010EC call dword ptr [<SetFilePointer>]
~~~
也就是说,对于带有附加数据的程序,抓取内存映像后,必须将附加数据追加到脱壳文件的最后,同时修正读取附加数据的相应指针。
**知识扩展:**
(以下内容选自**《加密与解密 第三版》**第10.15.1节——文件格式检查)
文件格式可以通过PEheader开始的标志Signature来检测。也许读者会说,检测DOS Header的Magic Mark不是也可以检测此PE文件是否合法吗?这个想法没有错,但是检测Magic Mark不一定能确定就是PE文件,如果某文本文件正好在开始就是“MZ”字符串,就会误判断。
(1)判断文件开始的第一个字段是否为IMAGE_DOS_SIGNATURE,即5A4Dh。
(2)再通过e_lfanew找到IMAGE_NT_HEADERS,判断Signature字段的值是否为IMAGE_NT_SIGNATURE,即00004550h,如果是IMAGE_NT_SIGNATURE,就可以认为该文件是PE格式。
具体实现的代码如下:
~~~
BOOL IsPEFile(LPVOID ImageBase)
{
PIMAGE_DOS_HEADER pDH=NULL;
PIMAGE_NT_HEADERS pNtH=NULL;
if(!ImageBase) //判断映像基址
return FALSE;
pDH=(PIMAGE_DOS_HEADER)ImageBase;
if(pDH->e_magic!=IMAGE_DOS_SIGNATURE) //判断是否为MZ
return FALSE;
pNtH=(PIMAGE_NT_HEADERS32)((DWORD)pDH+pDH->e_lfanew);
if(pNtH->Signature!=IMAGE_NT_SIGNATURE) //判断是否为PE格式
return FALSE
return TRUE;
}
~~~
(以下内容选自**《C++黑客编程揭秘与防范》**第4.2.7节——3种地址的转换)
某数据的文件偏移=该数据所在节的起始文件偏移+(某数据的RVA-该数据所在节的起始RVA)。
除了上面的计算方法以外,还有一种计算方法,把节的起始RVA的值减去节的起始文件偏移值,得到一个差值。然后再用RVA减去这个得到的差值就可以得到其所对应的FileOffset了。
**8、请解释一下病毒一般如何感染PE文件。**
答:(以下内容选自**《C++黑客编程揭秘与防范》**第4.6节——添加节区)
添加节区在很多场合都会用到,比如在加壳中,在免杀中都会经常使用到对PE文件添加一个节区。添加一个节区的方法有4个步骤,第1个步骤是在节表的最后面添加一个IMAGE_SECTION_HEADER,第2个步骤是更新IMAGE_FILE_HEADER中的NumberOfSections字段,第3步是更新IMAGE_OPTIONAL_HEADER中的SizeOfImage字段,最后一步是添加文件的数据。当然了,前3个步骤是没有先后顺序的,但是最后一个步骤一定要明确如何改变。
(以下内容选自**《C++黑客编程揭秘与防范》**第6.2节——简单病毒剖析)
大部分病毒都有感染的功能,病毒会把自身当中的或者需要其他程序来完成的指定功能的代码感染给其他的正常文件。就像人类的流行感冒,办公室中只要有一个人携带感冒病毒,就有可能所有人都会被传染。如果没被传染就说明已经预防过了,因此在机器上安装杀毒软件还是非常有必要的。
前面说了,病毒要感染其他文件也就是把病毒本身的攻击代码或者病毒期望其他程序要完成的功能代码写入到其他程序当中,而想要对其他程序写入代码就必须要有写入代码的空间。除了把代码写入到其他程序中以外,还必须让这些代码有机会被执行到。就上面两个问题而言都是比较容易解决的,下面分别来讨论一下。
病毒要对其他程序写入代码,必须确定目标程序有足够的空间让他把代码写入。通常情况下有两种比较容易实现的方法,第一种在前面的章节介绍过,就是添加一个节区,添加一个节区后就有足够的空间让病毒来写入了。第二种方法是缝隙查找,然后写入代码。何为缝隙?在每个节与节之间,必然有没有使用到的空间,这个空间就叫缝隙。只要确定要写入代码的长度,然后根据这个长度来查找是否有满足该长度的缝隙就可以了。
**9、请说说你对于壳的理解。**
答:(以下内容选自**《加密与解密 第三版》**第12.1.1节——壳的概念)
在自然界中,植物用壳来保护种子,动物用壳来保护身体等。同样,在一些计算机软件里面也有一段专门负责保护软件不被非法修改或反编译的程序。它们附加在原程序上通过Windows加载器载入内存后,先于原程序执行,得到控制权,执行过程中对原始程序进行解密、还原,还原完成后再把控制权交还给原始程序,执行原来的代码的部分。加上外壳后,原始程序代码在磁盘文件中一般是以加密后的形式存在的,只在执行时在内存中还原,这样就可以比较有效地防止破解者对程序文件的非法修改,同时也可防止程序被静态反编译。由于这段程序和自然界的壳在功能上有很多相同的地方,基于命名的规则,就把这样的程序称为“壳”了。
……
加壳软件一般都有良好的操作界面,使用也比较简单。除了一些商业壳,还有一些个人开发的壳,种类较多。壳对软件提供了良好保护的同时,也带来了兼容性问题,选择一款壳保护软件后,要在不同硬件和系统上多测试。由于壳能保护自身代码,因此许多木马或病毒都喜欢用壳来保护和隐藏自己。对于一些流行的壳,杀毒引擎能对目标软件脱壳,再进行病毒检查。而大多数私人壳,杀毒软件不会专门开发解压引擎,而是直接把壳当成木马或病毒处理。
有加壳就一定会有脱壳。一般的脱壳软件多是专门针对某加壳软件而编的,虽然针对性强、效果好,但收集麻烦。因此掌握手动脱壳技术十分必要。
**知识扩展:**
(以下内容选自**《加密与解密 第三版》**第13.1.1节——壳的加载过程)
壳和病毒在某些方面比较类似,都需要比原程序代码更早地获得控制权。壳修改了原程序的执行文件的组织结构,从而能够比原程序的代码提前获得控制权,并且不会影响原程序的正常运行。这里简单说说壳的常见加载过程。
(1)保存入口参数
加壳程序初始化时保存各寄存器的值,外壳执行完毕,再恢复各寄存器内容,最后再跳到原程序执行。通常用pushad/popad、pushfd/popfd指令对来保存与恢复现场环境。
(2)获取壳自己所需要使用的API地址
一般外壳的输入表中只有GetProcAddress、GetModuleHandle和LoadLibrary这几个API函数,甚至只有Kernel32.dll以及GetProcAddress。如果需要其他的API函数,则通过LoadLibraryA(W)或LoadLibraryExA(W)将DLL文件映像映射到调用进程的地址空间中,函数返回的HINSTANCE值用于标识文件映像映射到的虚拟内存地址。
LoadLibrary函数的原型如下:
~~~
HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName //DLL文件名的地址
);
//返回值:成功返回模块的句柄,失败返回NULL。
~~~
如果DLL文件已被映射到调用进程的地址空间里,可以调用GetModuleHandleA(W)函数获得DLL模块句柄。函数的原型如下:
~~~
HMODULE GetModuleHandle(
LPCTSTR lpModuleName //DLL文件名地址
);
~~~
一旦DLL模块被加载,线程就可以调用GetProcAddress函数获取输入函数的地址。函数原型如下:
~~~
FARPROC GetProcAddress(
HMODULE hModule, //DLL模块句柄
LPCSTR lpProcName //函数名
);
~~~
参数hModule是调用LoadLibrary(Ex)或GetModuleHandle函数的返回值。参数lpProcName可以采用两种形式:第一种是以0结尾的字符串地址;第二种形式是调用地址的符号的序号(微软公司非常反对使用序号)。
读者必须熟练掌握这三个函数的用法,外壳中用到其他函数就是用这三个函数来调用的。现在有些壳,为了提高强度,甚至连系统提供的GetProcAddress函数都不用,而是自己写个相同功能的函数代替GetProcAddress,以提高函数调用的隐蔽性。
(3)解密原程序的各个区块数据
壳出于保护原程序代码和数据的目的,一般都会加密原程序文件的各个区块。在程序执行时外壳将会对这些区块数据解密,以让程序能正常运行。壳一般是按区块加密的,那么在解密时也按区块解密,并且把解密的区块数据按照区块的定义放在合适的内存位置。
(4)IAT的初始化
IAT的填写,本来应该由PE装载器实现。但由于加壳时,自己构建了一个输入表,并让PE头中的输入表指针指向了自建的输入表。所以,PE装载器就将对自建的输入表进行了填写。那么原来PE的输入表的填写,只好由外壳程序实现了。外壳要做的就是将这个新输入表结构从头到尾扫描一遍,对每一个DLL引入的所有函数重新获取地址,并填写在IAT表中。
(5)重定位项的处理
文件执行时将被映射到指定的内存地址中,这个初始内存地址称为基址。当然这只是程序文件中声明的,程序运行时能够保证系统一定满足其要求吗?
对于EXE的程序文件来说,Windows系统会尽量满足。例如某EXE文件的基地址为400000h,而运行时Windows系统提供给程序的基地址也同样是400000h。在这种情况下就不需要进行地址“重定位”了。由于不需要对EXE文件进行“重定位”,所以加壳软件把原程序文件中用于保存重定位信息的区块干脆也删除了,这样使得加壳后的文件更加小巧。有些工具提供“Wipe Reloc”的功能,其实就是这个作用。
不过对于DLL的动态链接库文件来说,Windows系统没有办法保证每一次DLL运行时提供相同的基地址。这样“重定位”就很重要了,此时壳中也需要提供进行“重定位”的代码,否则原程序中的代码是无法正常运行起来的。从这点来说,加壳的DLL比加壳的EXE修正时多了一个重定位表。
(6)HOOK-API
程序文件中的输入表的作用是让Windows系统在程序运行时提供API的实际地址给程序使用。在程序的第一行代码执行之前,Windows系统就完成了这个工作
壳一般都修改了原程序文件的输入表,然后自己模仿Windows系统的工作来填充输入表中相关的数据。在填充过程中,外壳就可填充HOOK-API的代码的地址,这样就间接地获得程序的控制权。
(7)跳转到程序原入口点(OEP)
从这个时候起壳就把控制权交还给原程序了,一般的壳在这里会有明显的一个“分界线”。当然现在越来越多的加密壳将OEP一段代码搬到外壳的地址空间里,然后将这段代码清除掉。这种技术称为Stolen Bytes。这样,OEP与外壳间就没那条明显的分界线了,增加了脱壳的难度。
(以下内容选自**《加密与解密 第三版》**第16.3.1节——外壳的加载过程)
Windows的PE加载器加载可执行程序时,首先根据输入表获取所有API调用的地址,并填写到IAT中,再重定位所有的重定位项,最后调用WinMain(HINSTANCE hInstance,HINSTANCEhPrevInstance,PSTR szCmdLine,int iCmdShow)执行;如果是DLL,则调用DllMain(HINSTANCE hInstance,DWORDdwReason,LPVOID lpReserved)。加壳后,这个加载过程就由外壳来模拟了。
**本篇文章参考资料:**
1、戚利,**《Windows PE权威指南》**,机械工业出版社。
2、罗云彬,**《Windows环境下32位汇编语言程序设计(典藏版)》**,电子工业出版社。
3、段钢(主编),Blowfish、沈晓斌、丁益青、单海波、王勇、赵勇、唐植明、softworm、afanty、李江涛、林子深、印豪、冯典、罗翼、林小华、郭春杨(编委),**《加密与解密(第三版)》**,电子工业出版社。
4、冀云,**《C++黑客编程揭秘与防范》**,人民邮电出版社。