8.Portable Executable Format 可移植可执行格式
介绍
Portable Executable (PE) 是 Windows 系统上可执行文件的文件格式。PE 文件扩展名的几个示例包括 .exe
、.dll
、.sys
和 .scr
。本模块讨论 PE 文件的结构,这对于构建或逆向工程恶意软件非常重要。
请注意,本模块及未来的模块将经常互换使用“可执行文件”(例如 EXE、DLL)和“镜像”(Images)来指代这些文件。
PE结构
下图展示了一个简化的 Portable Executable (PE) 结构。图中显示的每个头部都被定义为一个数据结构,该结构包含关于 PE 文件的信息。本模块将详细解释每个数据结构。
DOS 头 (IMAGE_DOS_HEADER))
PE 文件的第一个头部总是以两个字节开头,分别是 0x4D
和 0x5A
,通常称为 **MZ
**。这两个字节代表 DOS 头部的签名,用于确认正在解析或检查的文件是一个有效的 PE 文件。
DOS 头部 是一个数据结构,其定义如下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // Offset to the NT header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
结构体中最重要的成员是 e_magic
和 **e_lfanew
**。
e_magic
是一个 2 字节的值,固定值为0x5A4D
或 MZ。e_lfanew
是一个 4 字节的值,保存 NT 头部的起始偏移量。注意,**e_lfanew
** 总是位于偏移量0x3C
处。
DOS 存根
在进入 NT 头部结构之前,有一个 DOS stub,这是一个错误消息,打印内容为 “This program cannot be run in DOS mode
“(该程序无法在 DOS 模式下运行),当程序在 DOS 模式或 “磁盘操作模式” 下加载时会显示此消息。
值得注意的是,这个错误消息可以由程序员在编译时更改。虽然这不是 PE 头部的一部分,但了解它是有益的。
NT 头 (IMAGE_NT_HEADERS)
NT 头部非常重要,因为它包含了另外两个镜像头部:FileHeader 和 OptionalHeader,这两个头部包含了关于 PE 文件的大量信息。
与 DOS 头部类似,NT 头部也包含一个用于验证的签名成员。通常,签名元素等于字符串 “PE”,其由 0x50
和 0x45
字节表示。但由于签名是 DWORD
数据类型,签名会表示为 0x50450000
,虽然被填充了两个空字节,仍然表示为 “PE”。
NT 头部可以通过 DOS 头部中的成员 e_lfanew
来访问,如图,0xf0 正是 NT头 的偏移
NT 头部的结构会根据机器的架构而有所不同。
- 32 位版本:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- 64 位版本:
typedef struct _IMAGE_NT_HEADERS64 { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER64 OptionalHeader; } IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
唯一的区别在于数据结构 OptionalHeader
,是 IMAGE_OPTIONAL_HEADER32
或 **IMAGE_OPTIONAL_HEADER64
**,分别用于 32 位和 64 位架构。
文件头 (IMAGE_FILE_HEADER)
接下来是可以从之前的 NT 头部数据结构访问的头部:
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;
最重要的结构体成员是:
NumberOfSections
- PE 文件中的段数量(稍后讨论)。Characteristics
- 指定可执行文件某些属性的标志,比如它是否是动态链接库(DLL)或控制台应用程序。SizeOfOptionalHeader
- 后续可选头部的大小。
关于文件头部的更多信息可以在官方文档页面中找到。official documentation page.
可选头部 (IMAGE_OPTIONAL_HEADER)
可选头部非常重要,尽管它被称为 “可选” ,但对 PE 文件的执行至关重要。之所以称它为“可选”,是因为某些文件类型没有这个头部。
可选头部有两个版本,分别用于 32 位和 64 位系统。两个版本的数据结构几乎相同,主要区别在于某些成员的大小。**ULONGLONG
** 用于 64 位版本,**DWORD
** 用于 32 位版本。此外,32 位版本中有一些在 64 位版本中不存在的成员。
32 位版本
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; 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;
64位版本
typedef struct _IMAGE_OPTIONAL_HEADER64 { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; ULONGLONG 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; ULONGLONG SizeOfStackReserve; ULONGLONG SizeOfStackCommit; ULONGLONG SizeOfHeapReserve; ULONGLONG SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
可选头部包含了大量有用的信息。以下是一些常用的结构体成员:
Magic
- 描述镜像文件的状态(32 位或 64 位镜像)。MajorOperatingSystemVersion
- 所需操作系统的主要版本号(例如 11, 10)。MinorOperatingSystemVersion
- 所需操作系统的次要版本号(例如 1511, 1507, 1607)。SizeOfCode
- 代码段的大小(稍后讨论)。AddressOfEntryPoint
- 文件的入口点偏移量(通常为main
函数的偏移)。BaseOfCode
- 代码段起始位置的偏移量。SizeOfImage
- 镜像文件的大小,以字节为单位。ImageBase
- 指定应用程序在执行时要加载到内存中的首选地址。然而,由于 Windows 的内存保护机制(例如地址空间布局随机化,ASLR),很少见到镜像映射到其首选地址,因为 Windows PE 加载器会将文件映射到不同的地址。Windows PE 加载器进行的这种随机分配会在实现未来技术时导致问题,因为某些被认为是常量的地址发生了变化。之后,Windows PE 加载器会通过 PE 重定位来修复这些地址。DataDirectory
- 可选头部中最重要的成员之一。它是一个IMAGE_DATA_DIRECTORY
的数组,包含 PE 文件中的目录(稍后讨论)。
数据目录 Data Directory
可以通过可选头的最后一个成员来访问数据目录(Data Directory)。该成员是一个 IMAGE_DATA_DIRECTORY
数据类型的数组,其数据结构如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 虚拟地址
DWORD Size; // 大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
数据目录数组的大小由常量 IMAGE_NUMBEROF_DIRECTORY_ENTRIES
决定,其值为 16。数组中的每个元素表示一个特定的数据目录,包含了某个 PE 节(section)或数据表(Data Table)的一些数据(即存储 PE 具体信息的地方)。
可以通过数组的索引来访问特定的数据目录:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
下面的两节将简要提到两个重要的数据目录:导出目录(Export Directory)和导入地址表(Import Address Table)。
导出目录 Export Directory
PE 文件的导出目录是一个数据结构,包含关于从可执行文件中导出函数和变量的信息。它包含导出函数和变量的地址,其他可执行文件可以使用这些地址来访问这些函数和数据。导出目录通常存在于导出函数的 DLL 中(例如从 kernel32.dll
中导出 **CreateFileA
**)。
导入地址表 Import Address Table
导入地址表是 PE 文件中的一个数据结构,包含关于从其他可执行文件导入的函数地址的信息。这些地址用于访问其他可执行文件中的函数和数据(例如从 kernel32.dll
中导入 CreateFileA
到 **Application.exe
**)。
PE Sections
PE 段包含了用于创建可执行程序的代码和数据。每个 PE 段都有一个唯一的名称,通常包含可执行代码、数据或资源信息。PE 段的数量不是固定的,因为不同的编译器可以根据配置添加、删除或合并段。有些段也可以手动添加,因此这个数量是动态的,IMAGE_FILE_HEADER.NumberOfSections
用于确定段的数量。
以下是最重要的 PE 段,几乎每个 PE 文件中都有这些段:
.text
- 包含可执行代码,即编写的代码。.data
- 包含已初始化的数据,即代码中已初始化的变量。.rdata
- 包含只读数据。这些是以.const
为前缀的常量变量。.idata
- 包含导入表。这些是与代码中调用的函数相关的信息表。Windows PE 加载器使用这些表来确定需要加载到进程中的 DLL 文件,以及每个 DLL 中使用的函数。.reloc
- 包含关于如何修正内存地址的信息,以便程序可以无误地加载到内存中。.rsrc
- 用于存储资源,如图标和位图。
每个 PE 段都有一个IMAGE_SECTION_HEADER
数据结构,包含有关该段的有价值信息。这些结构保存在 PE 文件的 NT 头部下,并且彼此堆叠,每个结构代表一个段。
回想一下,**IMAGE_SECTION_HEADER
** 结构如下:
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;
每个元素都非常有价值和重要:
Name
- 段的名称(例如.text
,.data
,.rdata
)。PhysicalAddress
或VirtualSize
- 段在内存中的大小。VirtualAddress
- 段在内存中起始位置的偏移量。
其它参考资料
如果需要对某些段进行进一步的澄清,强烈推荐阅读 0xRick’s Blog 上的以下博客文章:
总结
第一次接触 PE 头时,理解它可能会比较困难。幸运的是,基本模块中并不需要深入了解 PE 结构。然而,如果想让恶意软件执行更复杂的技术,就需要更深入的理解,因为某些代码需要解析 PE 文件的头和各个节。这部分内容可能会出现在中级和高级模块中。