导入表PE文件结构
从本章开始就要学习PE相关的内容。
可执行文件
在了解PE之前,我们需要知道什么是可执行文件,从字面理解可执行文件就是可以由操作系统进行加载执行的文件。
Windows平台下的可执行文件的格式,我们称之为PE(Portable Executable)文件结构;Linux平台下的可执行文件格式,我们称之为ELF(Executable and Linking Format)文件结构。
仔细的人可能会发现PE的全称是Portable Executable,其中文意思就是便携的可执行,而ELF的全称Executable and Linking Format就是可执行可链接格式,那么两者之间的差距就出现了,Windows平台下的PE文件结构是便携的,也就表示其在Windows下是通用兼容的,例如你在Windows7下的可执行文件也可以在Windows8、10系统下运行,而Linux则不一样,不同内核编译的可执行文件在不同内核的环境下是无法使用的。
在这些领域下会用到PE文件格式:
病毒和反病毒;
外挂和反外挂;
加壳和脱壳(保护与破解);
无源码的情况下修改功能、汉化软件...
识别PE文件
你想要识别一个文件是不是PE文件,或者说是不是一个可执行文件,可以根据PE指纹来识别:首先你需要找到一个可以以16进制打开PE文件的工具(010 Editor),然后找到一个PE文件,用该工具打开PE文件,在文件的开始位置有一个0x5A4D(十进制:MZ),接着在0x003C位置向后有一个0x100,接着我们再去寻找0x100位置就会出现一个0x4550(十进制:PE),那么当你用这个方法可以顺利的走通整个流程找到PE,就表示这是一个PE文件,同样这也是一个PE指纹:
如上示例中我使用的是exe后缀的文件,但即使不是exe后缀的文件,例如.sys、.dll后缀的文件,实际上你通过这种方式会发现它们也是PE文件,所以我们不要只看后缀名来认定是不是PE文件,而要具体去看文件中的指纹。
PE文件的整体结构
如上所述中我们可以了解到通过PE指纹的方式识别PE文件,但是我又是如何知道这是否是一个PE文件的呢?这是因为PE文件结构有一个规范和定义,如下图所示就是PE文件的整体结构:
32位
64位
如上图所示众可以发现PE文件有很多结构,其结构格式图可以见附件: PE格式图.pdf(看上去很多,但不需要害怕,一步一步学下去还是非常容易理解的)
PE文件的两种状态
主要结构体
上文中,我们了解了PE文件的整体结构,我们可以看见其中有很多结构体:
这几个主要结构体分别对应的宽度如下所示:
结构体
宽度(字节)
IMAGE_DOS_HEADER (DOS MZ头)
64
IMAGE_FILE_HEADER (PE文件头)
20
IMAGE_OPTIONAL_HEADER32 (PE 可选表头)
224
IMAGE_SECTION_HEADER (PE 节表)
40
这些结构体你都可以在Microsoft Visual Studio\VC98\Include\WINNT.H头文件中看见。
文件分析
这些结构体的具体细节,在之后的章节会详细了解,现在我们只需要按照PE文件的整体结构来看一个PE文件(使用010 Editor打开文件)。
DOS部分
首先来看一下DOS部分,首先是DOS MZ文件头IMAGE_DOS_HEADER结构,这个结构占64字节,文件前四行就是了(类似010 Editor这种编辑器,单行都是16字节):
接着是DOS块,这个大小是不固定的,但是在上文中,我们了解到可以根据某个值定位到PE文件头,我们可以先找到PE文件头,这样夹在他们之间的就是DOS块了,在这里就是IMAGE_DOS_HEADER结构体的e_lfanew成员,如上图所示这里对应的值是0xF8:
所以如下图所示中,绿色框标记的部分就是DOS块:
PE文件头
接着来看PE文件头,其第一个是PE文件头标志,这里占4字节,也就是上文图中所示的0x4550(PE标识是不能修改的),所以在这不赘述了;PE文件头第二部分就是PE文件表头IMAGE_FILE_HEADER结构,这个结构占20字节,我们也称之为标准PE头:
继续看PE文件头的第三个部分PE文件表头可选部分,我们也称之为扩展PE头,其就是IMAGE_OPTIONAL_HEADER32结构,默认情况下它在32位下是224字节,在64位下是240字节,你也可以通过IMAGE_FILE_HEADER结构的成员去获取/修改扩展PE头的宽度:
在这里也就对应着如下图中的0xF0(因为当前系统和文件都是64位的):
也就表示在这里扩展PE头的宽度就是240字节:
扩展PE头之所以数据宽度较大,是因为其有一个成员是结构体数组:
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
这个成员的宽度就是16个IMAGE_DATA_DIRECTORY结构体的宽度。
节表、节数据
节表很重要,其决定节数据的相关属性,而节数据是我们真正存储数据的地方,其数量和节表是对应的。
节表就是N个IMAGE_SECTION_HEADER结构体组成的,该结构体数据宽度是40字节:
我们可以在该PE文件中看一下有多少个IMAGE_SECTION_HEADER结构体,如下图用不同颜色标记的就是每个节,其实通过编辑器右边的内容你就可以大致知道每个节的表示什么类型了:
在当前PE文件中我们可以知道有6个节表,那也就表示在文件中存储数据的也就有6个部分,在节表之后的就是编译器的插入的数据,而编译器又是如何知道从哪开始插入数据呢?这实际上取决于一个扩展PE头的一个成员SizeOfHeaders:
该成员用来表示DOS头、PE头与节表加起来按照文件对齐以后的大小。这个真正的大小实际上取决于另外一个成员FileAlignment,SizeOfHeaders存储的数值一定是FileAlignment的整数倍,默认情况下该成员的值为0x200。
假设当前DOS头、PE头与节表加起来的宽度为302,而成员FileAlignment的值为200,这时候成员SizeOfHeaders的值按FileAlignment的值进行文件对齐就应该是400,而之所以需要文件对齐是为了提高执行效率,这是一个牺牲空间换时间的一种策略,我们可以在当前PE文件中查看这两个成员:
这两个成员刚好与我们假设的值是一样的,所以这里DOS头、PE头与节表加起来按照文件对齐以后的大小就是400,但这样确实比实际大小要多出一些空间,这些空间默认会用0x00填充,但也有可能这些空间会被编译器插入一些信息,接着在400地址之后的就是节数据了。
静动态差异
PE文件在运行前(静态,存储在磁盘上)和运行时(动态,运行在内存中)的格式是有差异的,这种差异对于我们理解PE文件是如何执行的来说很重要。
我们在之前的文件分析过程中实际上所看到的是静态的内容,其大小是要根据FileAlignment的值进行文件对齐的,但是在运行时则整体按照扩展PE头的成员SectionAlignment的值进行内存对齐,默认情况下该值为0x100:
我们可以实际观察一下在内存中的PE文件,首先打开记事本,然后在Winhex中这样选择:
然后找到对应的扩展PE头的成员SectionAlignment的值,这里就是默认的0x1000:
PE文件整体结构解析
之前我们已经按照PE文件的整体结构对实际的PE文件进行了大致上的了解了,现在我们需要来看看每个结构的意义和作用。
DOS头
在之前,我们已经了解过PE文件的整体结构了,并且我们进行了静动态差异的文件分析,其开头部分就是DOS部分,包含了DOS MZ文件头和DOS块,那么我们来了解一些DOS部分的结构和其相关意义。
DOS MZ文件头
DOS MZ文件头就是一个结构体IMAGE_DOS_HEADER,其定义如下所示:
typedef struct _IMAGE_DOS_HEADER // DOS .EXE header(DOS 可执行文件头)
{
WORD e_magic; // 魔术数字,标志为'MZ' (0x5A4D),用于识别该文件为 DOS 可执行文件
WORD e_cblp; // 文件最后一页的字节数(不满 512 字节的剩余部分)
WORD e_cp; // 文件页数(每页512字节),文件大小 = (e_cp - 1) * 512 + e_cblp
WORD e_crlc; // 重定位项数量(重定位表中条目个数)
WORD e_cparhdr; // 头部大小,以段落(16 字节)为单位,通常用于跳过头部获得代码起始
WORD e_minalloc; // 程序执行时需要的最小额外段落数(内存需求)
WORD e_maxalloc; // 程序可使用的最大额外段落数(最大内存需求)
WORD e_ss; // 初始 SS(栈段)寄存器值(相对于加载地址)
WORD e_sp; // 初始 SP(栈指针)值
WORD e_csum; // 校验和(DOS 不强制使用,通常为 0)
WORD e_ip; // 初始 IP(指令指针)值(程序入口地址)
WORD e_cs; // 初始 CS(代码段)值(相对于加载地址)
WORD e_lfarlc; // 重定位表偏移(从文件开头起)
WORD e_ovno; // Overlay 编号(用于多层可执行文件,一般为 0)
WORD e_res[4]; // 保留字段,未使用(保留供未来使用)
WORD e_oemid; // OEM 标识符(用于 e_oeminfo 说明其含义)
WORD e_oeminfo; // OEM 特定信息(取决于 e_oemid 的值)
WORD e_res2[10]; // 保留字段,未使用(保留供未来使用)
LONG e_lfanew; // 新 EXE 头的文件偏移(即 NT 头 IMAGE_NT_HEADERS 的起始位置)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
它有很多成员,但