基本概念
什么是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文件加上基址重定位
