N年没写文章了,不知道句子还能不能写通顺,最近正在看《软件加密技术内幕》,刚看完PE结构那部分内容,所以想起来写篇教程作为读书笔记,既可加强记忆又可帮助别人,何乐而不为呢。
好了,废话少说好戏正式上场,PE是英文Portable Executable(可移植的执行体)的缩写,从缩写可以看出它是跨平台的,即使在非intel的CPU上也能正常运行的。它是 Win32环境自身所带的执行体文件格式。其实不光是EXE文件是PE格式,其它的一些重要文件,例如动态链接库文件(DLL),驱动文件(SYS)等也是PE格式的,所以学好PE格式是非常重要的,以下我把这类文件统称为PE文件。学习PE文件结构不仅可以使我们知道可执行文件是怎样运行的,也可以使我们了解一下windows操作系统的一些工作机制,精通PE是成为计算机高手的必经之路。
其实说白了PE文件格式就是一种文件组织的方式,里面对一些重要信息的存放做了一些规定,比如文件要运行,我们就得先知道入口地址,可是我们从哪去得到入口地址呢,我们必须把保存有入口地址信息的有关数据放在固定的位置,这样不管是哪个文件我们就能从那个固定位置取得入口地址,而这固定位置就是PE标准所规定的。现在我们就来正式学习这套标准,我觉得从整体到部分是个很好的学习方法,先从整体把握全局,然后再重点各个击破,逐渐深入,下面我就以这个思路来介绍PE文件格式,方便大家快速掌握。它总体上由五大部分组成:
1.DOS MZ header(DOS头)
2.DOS stub
3.PE header(PE头)
4.section table(节表)
5.section(各个节)
所有的PE文件必须以一个DOS MZ header开始,其实它是一个IMAGE_DOS_HEADER类型的结构,这个结构的定义我们可以在WINNT.H头文件找到,此结构中有两个重要的成员是我们必须知道的,第一个e_magic,它是一个DWORD类型的变量,这个变量只有一个用处,就是当我们要判断一个文件是不是PE文件时,我们首先需要把这个变量的值与IMAGE_DOS_SIGNATURE比较,相等就完成了判断的第一步,当然后面我们还得判断一个标志,不等的话就说明当前文件不是PE文件。第二个成员就是非常重要的了,可以说只要它一出错我想这个文件就坏了,它就是e_lfanew,这是一个LONG类型的变量,里面存放了PE头在这个文件中的偏移量,它是定位PE头的关键数据,知道这两个成员的意义后DOS头学习基本就完成了。接下来的DOS stub实际上是个EXE,当当前系统不支持PE文件结构时它能输出一个错误提示“This program requires Windows”,不是很重要。接下来是重头戏PE头,PE头是一个IMAGE_NT_HEADERS类型的结构,下面是这个结构在WINNT.H中的定义:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
它有三个数据成员,Signature是一个标志变量,这个就是当我们判断一个文件是否是PE文件时第二步需要判断的,若这个值等于"PE/0/0"时就是一个PE文件,当然我们也可以像上面一样直接与IMAGE_NT_SIGNATURE这个常量比较,其实是等效的,因为IMAGE_NT_SIGNATURE是一个宏定义,它的实际值就是"PE/0/0"。第二个成员是一个IMAGE_FILE_HEADER类型的对象,通常我们叫它文件头,这个结构在头文件中的定义是:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
可以看出它有7个成员,下面我就对其中比较重要的成员做一说明,NumberOfSections这个成员保存了节的数目,至于节是什么东东,后面再说,在这只要知道它的数目保存在文件头的NumberOfSections中就可以了,当我们要遍历节时需要访问这个对象。PointerToSymbolTable,NumberOfSymbols这两个成员在调试时用的着,这里不做过多说明。SizeOfOptionalHeader这个成员保存了PE头中OptionalHeader这个成员的大小,最后一个成员是一个关于文件的标记,即这个文件是EXE还是DLL文件。总的来说文件头中包含了PE文件的物理分布的有关信息,比较重要的就是第二个成员了。接下来我们来学习PE头的第三个成员OptionalHeader,这是PE文件结构中最重要的一个部分,因为大部分重要的数据结构都得通过它去定位,可以说它保存了PE文件逻辑分布的信息,但是这部分也是学习PE结构过程中的一个难点,因为层次结构比较多,我们定位一个结构需要抽丝剥茧般一层层进行下去,在此过程还得保持头脑清醒,对变量类型还得有比较深层次的理解。好了,我们还是那个原则,开始不要先涉入过深,先对这个成员有个总体的印象,它是一个IMAGE_OPTIONAL_HEADER32类型的对象,这个结构的定义是:
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
从这个结构的长度就可看出,它保存的信息不少,肯定很重要,在正式学习这个结构的成员前,我们还要理解一个重要的术语--RVA,RVA 代表相对虚拟地址。它是一个DWORD类型的数据,那RVA到底是什么呢?简单的说它是一个偏移量,这个偏移量是虚拟空间中相对于参考点的偏移,它的值就是偏移的大小,值得一说的是这个值只有当PE文件被PE文件装载器载入内存时才有作用,否则的话我们不能用这个值去直接定位与它相关的数据结构。如果在PE文件没有被PE装载器载入内存,而我们又想在PE文件中直接定位相关数据该怎么办呢,我们需要把这个值转换成文件偏移量,也就是相对于文件开头的偏移量,我们可以用RVAToOffset这个函数去完成地址的相关转换,至于这个函数的实现可以从网上找到。好了,准备工作做好了,下面我们就进入正题吧。由于这个结构成员较多,有些也不是很常用,还有有些我也不是很明白,所以就不说了,下面我就对一些比较常用的一一进行介绍吧。还希望大家不要像我一样一知半解的,要有宁可错杀一千,不可放过一个的学习精神。
AddressOfEntryPoint:
PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。ImageBase PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。
SectionAlignment:
内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。
FileAlignment:
文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用/定义。
MajorSubsystemVersion
MinorSubsystemVersion :
win32子系统版本。若PE文件是专门为Win32设计的,该子系统版本必定是4.0否则对话框不会有3维立体感。
SizeOfImage:
内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
SizeOfHeaders:
所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。
Subsystem:
NT用来识别PE文件属于哪个子系统。 对于大多数Win32程序,只有两类值: Windows GUI 和 Windows CUI (控制台)。
DataDirectory:
一IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等。 可以告诉大家这个成员是PE结构的重中之重了,在这先给大家提个醒,让大家多看它两眼,加深对它的印象。
好了,到此为止,这个PE文件结构在总体划分上就只剩下节表和节没有介绍了,现在我就来说说节表和节吧,在正式介绍它之前,我先说一点它话,当然绝对是有助于你理解节表的,书大家都看过是吧,现在你想一想我们拿到一本新书时,做的第一件事是什么呢?首先我们翻开的是书的目录,然后从书的目录中去搜寻我们感兴趣的东西。可以打个形象的比喻,节表就相当于书的目录,而书中的各个章节就相当于PE文件结构中的节,通过目录我们能很快找到书中我们感兴趣的内容,同样通过节表我们很快能找到PE文件中的各个节。好了,有了定性的认识后我们还得继续升华,还得定量的学习它,这才是科学的学习方法。节表从数据结构的角度来说它是一个结构数组,所谓结构数组就是这个数组的每个成员都是同一种结构类型的变量。它们不光在逻辑上是连续的,在存储介质中也是连续的。同样我们也可在WINNT.H中找到这个结构的定义:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
这个结构总共包含了10个成员变量,由于有些并不是很重要,所以我就对其中比较重要的做个介绍,如果你是那种追根究底的人,你自己完全可以继续查阅相关资料,了解一下其它成员的实际意义,不过要在你对我介绍的都掌握之后,学习把握主次是很重的,舍本逐末只会捡了芝麻丢了西瓜。好了,现在开始介绍吧。
Name[IMAGE_SIZEOF_SHORT_NAME]:
这个成员是一个字节型的数组,很明显它保存的是一个名字,我想你能猜到了吧,这就是节的名字,不过要注意一下,这个数组的上限是8,最多只能保存8个字符,还有就是它不是一个ASCIIZ字符串,因为它不是以null结尾的。
VirtualAddress:
本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h。
这个很重要,定位节时需要它。
SizeOfRawData:
经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。(译者注: 假设一个文件的文件对齐尺寸是0x200,如果前面的 VirtualSize域指示本节长度是0x388字节,则本域值为0x400,表示本节是0x400字节长)。
PointerToRawData:
这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。
Characteristics:
包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。
基本上比较重要的成员都介绍了,我想你看了这么久肯定有点迷糊了,介绍了这么久的节表,那到底什么是节呢?还专门为它建立一个目录好像很重要似的。确实,可以说节才是PE内容的真正载体, 节其实我们可以想象成块,这样更形象点,因为一节就是一块数据,而且这块数据拥有共同的属性,比如是代码还是数据,是只读的还是读写的。学习节要牢牢记住几个字---共同的属性。就是说多个数据只要是具有共同属性我们就能把它放在同一节中,而不用去考虑这些数据是不是在逻辑上有什么关联。真正以数据之间逻辑关联建立的目录是PE头的第三个成员OptionalHeader,我们已经稍微了解了下了。
好了,对PE文件结构的总体构造已经介绍完了,但学习PE文件结构的任务还远远没有完成,因为还有导入表,象DLL还有导出表,还有到底怎么去定位PE中的重要结构等都没有介绍。我只能对你说,不要着急,一口吃不成胖子,学习决非一日之寒,尤其是计算机的学习,它是一个系统的认知过程,知识有很强的连贯性。所以对于决心学好的计算机的人,我有一句良言相赠----找准目标,持之以恒,绝不放弃。上面介绍的内容我参考了《软件加密技术内幕》这本书,由于我也是第一次真正接触PE,所以有些概念理解的不是很准确,希望大家多多包含,有时间大家可以看看原版。当然这只是这个PE教程的第一部分,如果大家不拿鸡蛋砸我的话,我还会写出后续部分。我的知识来自网络,所以我也要回馈网络,虽然现在水平还不是很高,但尽力了就行了。