1. 主页 > 公会学院 >

PE文件解析(一)

基本概念

什么是PE文件

PE文件的全程:Portable Executable,即可移植的可执行文件

常见的PE文件:EXE文件、DLL文件、OCX文件、SYS文件、COM文件

PE文件通常是指32位的,而64位的PE文件通常称为PE32+、PE+、PE64

文件偏移地址、虚拟地址与相对虚拟地址

文件偏移地址:PE文件存储在磁盘中时,某个数据的位置相对于文件头部的偏移量,通常将其称为文件偏移地址(File

Offset Address)或物理地址(RAW Offset)

虚拟地址:在Windows系统中,PE文件会被系统加载器映射到内存中,而每个PE文件都有其自己的独立的虚拟空间,这个虚拟空间的内存地址就被称为虚拟地址(Virtual

Address)

相对虚拟地址:当PE文件映射到内存之后,某个数据相对于文件载入点地址(即基地址,ImageBase)的偏移量,通常称其为相对虚拟地址(Relative

Virtual Address),虚拟地址与相对虚拟地址存在如下关系:虚拟地址(VA) =

基地址(ImageBase) + 相对虚拟地址(RVA)

PE结构图

PE文件框架结构

PE文件的详细结构

PE文件磁盘结构与内存结构(对齐原因)

PE Headers解析

首先需要明确的是,严格意义上的PE文件头是指IMAGE_NT_HEADERS,但为了方便解析,此处将

IMAGE_DOS_HEADER(DOS头)

IMAGE_NT_HEADERS(NT头)

IMAGE_FILE_HEADER(映像文件头)

IMAGE_OPTIONAL_HEADER(可选映像头)

IMAGE_SECTION_HEADER(区块表)

这五个部分都视作PE的头部部分一并进行解析

IMAGE_DOS_HEADER

MS-DOS头部,大小为64字节,每个PE文件都是以一个DOS程序开始的,且DOS可以识别出一个文件是不是一个有效的执行体,若其首部的e_magic被置为0x5A4D(即ASCII的

“MZ”,该值对应于winnt.h文件中的一个宏定义,IMAGE_DOS_SIGNATURE),那么该文件就是一个DOS可执行文件

123456789101112131415161718192021typedef struct _IMAGE_DOS_HEADER { WORD e_magic; // DOS可执行文件标记 "MZ" WORD e_cblp; WORD e_cp; WORD e_crlc; WORD e_cparhdr; WORD e_minalloc; WORD e_maxalloc; WORD e_ss; WORD e_sp; WORD e_csum; WORD e_ip; // DOS代码入口IP WORD e_cs; // DOS代码入口CS WORD e_lfarlc; WORD e_ovno; WORD e_res[4]; WORD e_oemid; WORD e_oeminfo; WORD e_res2[10]; LONG e_lfanew; // 偏移地址,指向IMAGE_NT_HEADERS,"PE00"(0x00004550) } IMAGE_DOS_HEADER

其中比较重要的两个字段分别是e_magic与e_lfanew,前者的作用已经解释过,而e_lfanew是真正的PE文件头IMAGE_NT_HEADERS的相对偏移(lfanew

= long file address of new exe)

用十六进制编辑器打开exe文件可以发现,起始位置e_magic字段的值为”MZ”,而e_lfanew的值为”0x000000F0”,在相对文件起始位置0x000000F0的位置我们可以找到真正的PE文件头标记”PE00”

我们可以观察到在e_lfanew和真正的PE头之间还有一些数据,这部分数据被称为DOS

stub(即DOS块),DOS

stub实际上是一个有效的exe,在不支持PE文件格式的操作系统中,它将显示一个错误提示,即”This

program cannot be run in DOS mode”,DOS

stub的数据大多由编译器自动生成,可根据自己的需要修改其中的内容,我们将IMAGE_DOS_HEADER与DOS

stub合称为DOS文件头

IMAGE_NT_HEADERS

紧跟着DOS

stub的就是真正的PE文件头了,这部分也被称为NT映像头,在一个有效PE文件中,其Signature字段被置为0x00004550(即ASCII的”PE00”,该值对应于winnt.h文件中的一个宏定义,IMAGE_NT_SIGNATURE),而紧跟在Signature字段之后的就是IMAGE_FILE_HEADER映像文件头,在此之后紧跟的是IMAGE_OPTIONAL_HEADER可选映像头

12345typedef struct _IMAGE_NT_HEADERS { DWORD Signature; // PE文件标识 IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32

在十六进制编辑器中,NT影响头的结构如下图所示,首个字段即为”PE00”标记,紧跟其后红色框所示部分就是映像文件头,紧跟映像文件头之后的蓝色框所示的部分就是可选映像头

IMAGE_FILE_HEADER

映像文件头中包含PE文件的一些基本信息,大小为20字节,其中较为重要的两个字段为NumberOfSections字段与SizeOfOptionalHeader字段,前者指出了区块Section的数量(同时也指明了IMAGE_SECTION_HEADER区块表的数量,因为每一个区块表记录了对应区块的相关信息),后者指出了IMAGE_OPTIONAL_HEADER可选映像头的大小

123456789typedef struct _IMAGE_FILE_HEADER { WORD Machine; // 运行平台 WORD NumberOfSections; // 区块数 DWORD TimeDateStamp; // 文件创建的日期和时间 DWORD PointerToSymbolTable; // 指向符号表(用于调试) DWORD NumberOfSymbols; // 符号表中的符号的个数(用于调试) WORD SizeOfOptionalHeader; // 可选映像头的大小 WORD Characteristics; // 文件属性} IMAGE_FILE_HEADER

这里对字段进行详细的解释:

1.

Machine:可执行文件的目标CPU类型,因为不同平台上指令集不同,因此需要该字段标识运行的平台,如Inter

i386及其之后的处理器,该字段的值都为0x14C

2. NumberOfSections:区块数

3.

TimeDateStamp:文件创建的时间,将该值翻译为易读字符串需要使用_ctime函数

4.

PointerToSymbolTable:COFF符号表的文件偏移位置(FOA),现较为少见

5.

NumberOfSymbols:如果有文件符号表,其指出了文件符号表中符号的数目

6.

SizeOfOptionalHeader:可选映像头的大小,其大小通常依赖于文件是32位还是64位的,若是32位文件,这个值默认为0x00E0,若是64位文件,这个值默认为0x00F0,这表示了选映像头大小的最小值,因此该值是可以修改的

7.

Characteristics:文件属性,其结果为若干个有效值的和,有效值在winnt.h定义

123456789101112131415#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 不存在重定位信息#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可执行 若为0,通常是链接时出问题#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 行号信息被移除#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符号信息被移除#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Aggressively trim working set#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 应用程序可以处理超过2GB的地址,因为大部分数据库服务器需要很大的内存,而NT仅提供2GB给应用程序,因此从NT SP3开始,可以通过设置此参数,使应用程序分配2 ~ 3GB区域的地址(此部分原本为系统内存区)#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // 处理器的低位字节是相反的#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 目标平台为32为机器#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // .DBG文件的调试信息被移除#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 如果映像文件在可移动介质中,则先复制到交换文件中再运行#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 如果映像文件在网络中,则先复制到交换文件后再运行#define IMAGE_FILE_SYSTEM 0x1000 // 系统文件#define IMAGE_FILE_DLL 0x2000 // DLL文件#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 文件只能运行在单处理上#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // 处理器的高位字节是相反的

IMAGE_OPTIONAL_HEADER

虽然称为可选映像头,但该结构是必不可少的,其中定义了更多的数据,32位下最小大小为E0

123456789101112131415161718192021222324252627282930313233typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; // 标志字 BYTE MajorLinkerVersion; // 链接器主版本号 BYTE MinorLinkerVersion; // 连接器次版本号 DWORD SizeOfCode; // 所有含有代码的区块的大小 DWORD SizeOfInitializedData; // 所有初始化数据区块大小 DWORD SizeOfUninitializedData; // 所有未初始化数据区块大小 DWORD AddressOfEntryPoint; // 程序执行入口RVA DWORD BaseOfCode; // 代码区块起始RVA DWORD BaseOfData; // 数据区块起始RVA DWORD ImageBase; // 程序默认载入基地址 DWORD SectionAlignment; // 内存中块的对齐值 DWORD FileAlignment; // 磁盘文件中块的对齐值 WORD MajorOperatingSystemVersion; // 操作系统主版本号 WORD MinorOperatingSystemVersion; // 操作系统次版本号 WORD MajorImageVersion; // 用户自定义主版本号 WORD MinorImageVersion; // 用户自定义次版本号 WORD MajorSubsystemVersion; // 所需子系统主版本号 WORD MinorSubsystemVersion; // 所需子系统此版本号 DWORD Win32VersionValue; // 保留,通常设置为0 DWORD SizeOfImage; // 映像载入内存后的总大小 DWORD SizeOfHeaders; // DOS头、PE文件头、区块表的总大小 DWORD CheckSum; // 映像校验和 WORD Subsystem; // 文件子系统 WORD DllCharacteristics; // 显示DLL特性的旗标 DWORD SizeOfStackReserve; // 初始化时栈的大小 DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小 DWORD SizeOfHeapReserve; // 初始化时保留的堆大小 DWORD SizeOfHeapCommit; // 初始化时实际保留的堆大小 DWORD LoaderFlags; // 调试相关,默认值为0 DWORD NumberOfRvaAndSizes; // 数据目录项的数量 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表数组} IMAGE_OPTIONAL_HEADER32

这里对一些较为关键的字段进行详细的解释:

1.

Magic:标志字,ROM映像为0x107,32位可执行映像为0x010B,64位可执行映像位0x020B

2.

SizeOfCode:所有含有IMAGE_SCN_CNT_CODE属性的区块的总大小,必须是FileAlignment的整数倍,由编译器填写,通常情况下,大多数文件只有一个Code块,所以该字段与.text块的大小匹配

3.

SizeOfUninitializedData:所有未初始化数据区块大小,装载程序需要在虚拟地址空间中位这些数据分配空间,这些块在磁盘文件中不占空间,在程序开始运行时没有指定值,未初始化数据通常在.bss块中

4.

AddressOfEntryPoint:程序执行入口RVA。对于DLL,这个入口点在进程初始化和关闭时与线程创建和销毁时被调用,在大多数可执行文件中,这个地址不直接指向Main、WinMain或DllMain,而是指向运行时的库代码,并由它来调用上述函数

5.

ImageBase:程序默认载入基地址,如果PE文件在这个地址载入,加载器将会跳过应用基址重定位的步骤

6.

SectionAlignment:载入内存时,内存中块的对齐值,也就是说每个区块被载入的地址必定是本字段指定数值的整数倍,默认的对齐尺寸是目标CPU的页尺寸(通常是0x10000,也就是4KB)

7.

FileAlignment:磁盘文件中块的对齐值,区块在磁盘文件中存储的首地址必定是本字段指定数值的整数倍,对于x86可执行文件,这个值常为0x200或0x1000,这是为了保证块总是从磁盘的扇区开始,该值必须是2的幂

8.

SizeOfImage:映像载入内存后的总大小,即从ImageBase到最后一个块结束,且按照SectionAlignment对齐的大小

9.

SizeOfHeaders:DOS头、PE文件头、区块表的总大小,按FileAlignment对齐

10.

CheckSum:映像校验和,CheckSumMappedFile函数可以计算该值,通常情况下,普通的EXE文件该值为0,但内核模式的驱动程序和系统DLL必须有一个校验和

11. NumberOfRvaAndSizes:数据目录项的数量,该值至今一直为16

12.

DataDirectory[16]:数据目录数组,由数个相同的IMAGE_DATA_DIRECTORY结构组成,其具体的结构如下

1234typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; // 数据块的RVA DWORD Size; // 数据块的大小} IMAGE_DATA_DIRECTORY

数据目录表成员的结构如下所示

序号

表名

结构

0

Export Table

IMAGE_DIRECTORY_ENTRY_EXPORT

1

Import Table

IMAGE_DIRECTORY_ENTRY_IMPORT

2

Resources Table

IMAGE_DIRECTORY_ENTRY_RESOURCE

3

Exception Table

IMAGE_DIRECTORY_ENTRY_EXCEPTION

4

Security Table

IMAGE_DIRECTORY_ENTRY_SECURITY

5

Base Relocation Table

IMAGE_DIRECTORY_ENTRY_BASERELOC

6

Debug

IMAGE_DIRECTORY_ENTRY_DEBUG

7

Copyright

IMAGE_DIRECTORY_ENTRY_COPYRIGHT

8

Global Ptr

IMAGE_DIRECTORY_ENTRY_GLOBALPTR

9

Thread Local Storage (TLS)

IMAGE_DIRECTORY_ENTRY_TLS

10

Load Configuration

IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG

11

Bound Import

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT

12

Import Address Table (IAT)

IMAGE_DIRECTORY_ENTRY_IAT

13

Delay Import

IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT

14

COM Descriptor

IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR

15

保留,必须为0

-

IMAGE_SECTION_HEADER

区块表中记录了区块的具体信息,每个区块表分别指向了不同的区块实体,紧跟在

IMAGE_OPTIONAL_HEADER 之后,每个区块表大小都是40字节

123456789101112131415typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8字节大小的块名 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; // 实际被使用的区块的大小,未对齐 DWORD VirtualAddress; // 该块装载到内存中的RVA DWORD SizeOfRawData; // 在磁盘中区块的大小,已对齐 DWORD PointerToRawData; // 该块在磁盘中的偏移FOA DWORD PointerToRelocations; // 在EXE中无意义,在OBJ文件中表示本块重定位信息表的偏移 DWORD PointerToLinenumbers; // 调试信息,行号表在文件中的偏移 WORD NumberOfRelocations; // 在EXE中无意义,在OBJ文件中表示本块在重定位表中重定位数量 WORD NumberOfLinenumbers; // 该块在行号表中的行号数量 DWORD Characteristics; // 块属性} IMAGE_SECTION_HEADER

块属性中的一些重要字段值如下所示

12345678#define IMAGE_SCN_CNT_CODE 0x00000020 // 包含代码,通常与0x10000000一起设置#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 包含已初始化数据#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 包含未初始化数据#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // 该块可被丢弃,因为它一旦被载入,进程就不再需要它了,常见的可丢弃块是.reloc(重定位块)#define IMAGE_SCN_MEM_SHARED 0x10000000 // 该块为共享块#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 该块可执行,通常当0x00000020标志被设置时,该标志也被设置#define IMAGE_SCN_MEM_READ 0x40000000 // 该块可读,可执行文件中总是设置该标志#define IMAGE_SCN_MEM_WRITE 0x80000000 // 该块可写,若PE文件中没有设置该标志,装载程序就会将内存映像页标记为可读或可执行

在十六进制编辑器中的区块表信息如下图所示,可以观察到该exe文件包含4个区块表,其中四个区块的信息名称分别为

.text

.rdata

.data

.rsrc

区块解析

首先需要注意的是,区块名称只是为了方便辨识,但对于操作系统来说是无关紧要的,如当寻找输出表、输入表信息时,不应该默认到.text和.rdata区块中寻找,而是要严格依据数据目录数组DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]中的信息进行查找,常见区块如下

名称

描述

.text

默认的代码区块,其中的内容全是指令代码

.data

默认的读、写区块,全局变量、静态变量通常放在此处

.rdata

默认的只读数据区块,程序较少用到该块中的数据,但至少有两种情况会用到,一是在Microsoft链接器产生的exe文件中,用于存放调试目录;二是用于存放说明字符串,如果程序的DEF文件中指定了DESCRIPTION,字符串就会在出现在该块中

.idata

输入表,包含其他外来DLL的函数及数据信息,通常将其合并到其他区块中,如.rdata

.edata

输出表,当创建一个输出API或数据的可执行文件时(如DLL),链接器会创建一个.exp文件,.exp文件将会包含一个.edata区块,并加入到最后的可执行文件中,通常将.edata合并到其他块中,如.text区块中

.rsrc

资源,包含模块的全部资源,例如图标、菜单、位图等,该区块是只读的,无论如何都不应该命名为为.rsrc以外的名字,也不能被合并到其他区块中

.reloc

可执行文件的基址重定位,通常只是DLL需要,而exe不需要,通常在Release模式下,链接器不会给exe文件加上基址重定位