PE 文件
可移植可执行 (PE) 文件格式与 Windows 操作系统上能实现代码执行的文件类型所使用,常见的拓展名有 exe、dll、sys 等。PE 格式描述了文件必须遵循的标准结构,以便定位其内容并在执行的各个阶段使用其信息。了解这种格式对于恶意软件分析人员来说尤为重要,因为检查可执行文件的 PE 内容可以提供有关文件的大量信息,可能包括文件的作用。对于我们开发恶意软件的攻击者也同样重要,因为一些免疫检测的技术需要对 PE 文件十分熟悉。
PE 文件结构
PE 格式包含多种文件类型,在最高层次可以分为 COFF 文件与 PE 文件。
通用目标文件格式 (COFF) 文件,也称为对象文件,具有 obj 文件扩展名。它由 Windows 兼容的编译器生成,将源代码转换为机器代码。该文件类型本身不可执行,但可以作为输入传递给链接器,链接器从一个或多个对象文件创建可执行文件。
PE 文件,也称为可执行文件或映像文件,这是链接器生成的文件,包含可执行代码和运行时映射到内存的数据。映像文件还包含另外两种类型,第一种是动态链接库文件,具有 dll 扩展名,包含可以被多个程序同时导入和使用的代码和数据。 尽管 DLL 文件被归类为可执行文件,但它不能独立地直接运行。第二种是可执行文件,具有 exe 扩展名,与 DLL 不同的是它可以独立运行。
PE 格式以许多文件头开始,文件头是位于数据块起始的附加数据,通常包含有关数据块的信息,例如数据块的大小、元素在数据块中的位置,以及其他属性。在 PE 格式中,文件头数据由结构体组织和定义,在 C 和 C 相关的编程语言中,结构体是由不同的元素组成的数据类型,这些元素本身可能有多种数据类型。这些元素由不同的变量名称引用,并按顺序存储在连续的内存块中。PE 格式中使用的结构在名为 winnt.h 的头文件中定义,该文件可以作为 Windows SDK 的一部分下载。
DOS 头与 DOS Stub
PE 文件的第一个头文件总是以 0x4D5A (MZ) 这 2 个字节为前缀,这 2 个字节表示 DOS 头签名,用于确认正在解析或检查的文件是有效的 PE 文件。DOS头是一个数据结构体,定义如下:
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // 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;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; // NT 头的偏移
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
其中,最重要的分别是 e_magic 与 e_lfanew,分别是小端格式的 ASCII 字符 MZ,以及从映像文件起始到 NT 头的偏移。e_lfanew 总是位于 0x3c 处,占 4 个字节。
使用 PE-Bear 打开我们上一小节用 C++ 编写的 MessageBox 程序,我们发现最开始确实是 4D 5A,而在 0x3c 处,值为 0xF0。
而 DOS Stub 位于 0x40 处,DOS Stub 紧接着 DOS 头,因为 DOS 头占用 0x40 字节 (可以根据上文给出的结构体计算出总大小)。DOS Stub 包含了报错信息:该程序不能再 DOS 模式中运行。
NT 头
NT 头十分重要,其结构体如下,包含了签名、File 头、Optional 头这 3 个元素,NT 头包含了大量有关 PE 的信息。
//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;
Signature
在上文,我们知道了 NT 头在 0xF0 偏移处,于是我们定位到 0xF0 处,发现签名元素 0x50450000,是填充了 2 个零字节的字符串 PE,因为该签名占用 4 字节。
File 头
File 头的结构体如下:
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;
比较重要的元素有 Machine,NumberOfSections,TimeDateStamp,SizeOfOptionalHeader,以及 Characteristics。
Machine 表示该 PE 文件所期望运行在的 CPU 架构,我们能看到该应用期望在 AMD64 架构 CPU 的系统上运行。
NumberOfSection 表示当前 PE 文件所包含的 PE 区域数量,该程序有 6 个。
TimeDataStamp 表示文件创建的时间,能看到是 2023 年 6 月 19 日。
SizeOfOptionalHeader 表示 Optional 头的尺寸,这里是 240。
Characteristics 表示该二进制文件的特定属性,例如是 DLL 还是可执行的文件,这里显然是可执行的 exe 文件。
Optional 头
尽管 Optional 头名称为 Optional,但却非常重要,只是有些文件类型 (例如对象文件) 没有它。让我们查看它的结构体:
//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;
以及在 PE-Bear 中查看 Optional Header
一些比较重要的元素如下:
Magic 描述了该映像文件是 32 位还是 64 位的,当前是 64 位的。
AddressOfEntryPoint 表示程序入口的相对虚拟内存地址 (RVA)。RVA 指的是当 PE 在内存中,距离开头(PE 的第一个字节) 的偏移。程序入口通常是 main 函数,默认在 .text 区域。当前是 0x12D0,而当 PE 存在于磁盘中,入口的偏移是 6D0。这是因为 PE 文件在内存中和在磁盘中的展现出的结构有所不同。
BaseOfCode 表示当 PE 在内存中时,.text 的偏移,这里是 0x1000,而在磁盘中 .text 区域的偏移为 0x400。
SizeOfCode 为 .text 区域的大小
ImageBase 为当 PE 文件被载入内存时,偏好的基址,但实际不一定是该值。
SectionAlignment 是当 PE 在内存中时,校准系数的值,是 0x1000。RVA 表示在内存中距离 PE 文件开头的偏移 (虚拟地址 VA=基址+相对虚拟地址RVA),而当 PE 文件分别处于内存和磁盘中时,RVA 与偏移 (Offset) 的差异是因为 SectionAlighment 以及 FileAlignment。
FileAlignment 是当PE 文件在磁盘时,校准系数的值,是 0x200。
SizeOfImage 表示当 PE 在内存中时,整个映像取整 (校准系数 0x1000 的整数倍)后的尺寸
SizeOfHeaders 表示取整后的所有头尺寸,是校准系数 0x200 的整数倍
CheckSum:校验文件完整性的数据
NumberOfRvaAndSize 是 DataDirectory 中条目的数量。
DataDirectory 是一组 IMAGE_DATA_DIRECTORY 结构体类型的数列,每个条目有 2 个属性,VirtualAddress (但实际上是 RVA) 和 Size。这些目录包含许多在运行时可能需要的重要数据结构体条目以及函数,以方便定位到它们。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
虽然有 NumberOfRvaAndSize 值为 16,但目前实际只有 15 个目录条目。
#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
这些目录中,比较
导出目录包含有关解析函数符号所需的不同结构的信息,这些函数由当前 PE 映像导出并可供其他人导入。 此信息允许加载程序找到由其他模块导入的特定函数的地址。 导出通常由 DLL 提供。 在这种情况下,RVA 为“0”,大小为“0”,表示可执行文件没有导出表,不导出任何符号。
另一方面,导入目录包含有关解析可执行文件从其他模块导入的符号所需的不同结构的信息。 此信息有助于可执行文件解析导入函数的地址。 此目录具有 RVA 0x123dc 和大小 0x28。 我们将在以下两个学习单元中更详细地讨论导入和导出。
基本重定位表包含帮助加载程序将可执行文件加载到首选基址以外的地址的信息。 此信息帮助加载程序找到必须根据使用的加载地址重新定位的地址。 我们的可执行文件的重定位表位于 RVA 0x15000。
进口地址表 (IAT) 是包含进口相关信息的特定表。 在磁盘上,它的条目包含帮助加载程序找到导入函数的虚拟地址的数据,一旦找到这些地址,它们就会被相应的地址覆盖。 我们将在后面的部分中更详细地讨论这个问题。