介绍

Portable Executable (PE) 是 Windows 系统上可执行文件的文件格式。PE 文件扩展名的几个示例包括 .exe.dll.sys.scr。本模块讨论 PE 文件的结构,这对于构建或逆向工程恶意软件非常重要。

请注意,本模块及未来的模块将经常互换使用“可执行文件”(例如 EXE、DLL)和“镜像”(Images)来指代这些文件。

PE结构

下图展示了一个简化的 Portable Executable (PE) 结构。图中显示的每个头部都被定义为一个数据结构,该结构包含关于 PE 文件的信息。本模块将详细解释每个数据结构。

DOS 头 (IMAGE_DOS_HEADER))

PE 文件的第一个头部总是以两个字节开头,分别是 0x4D0x5A,通常称为 **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 头部非常重要,因为它包含了另外两个镜像头部:FileHeaderOptionalHeader,这两个头部包含了关于 PE 文件的大量信息。

与 DOS 头部类似,NT 头部也包含一个用于验证的签名成员。通常,签名元素等于字符串 “PE”,其由 0x500x45 字节表示。但由于签名是 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 文件的头和各个节。这部分内容可能会出现在中级和高级模块中。

⬆︎TOP