Skip to main content

PE 文件

可移植可执行 (PE) 文件格式与 Windows 操作系统上能实现代码执行的文件类型所使用,常见的拓展名有 exedllsys 等。PE 格式描述了文件必须遵循的标准结构,以便定位其内容并在执行的各个阶段使用其信息。了解这种格式对于恶意软件分析人员来说尤为重要,因为检查可执行文件的 PE 内容可以提供有关文件的大量信息,可能包括文件的作用。对于我们开发恶意软件的攻击者也同样重要,因为一些免疫检测的技术需要对 PE 文件十分熟悉。


PE 文件结构

PE 格式包含多种文件类型,在最高层次可以分为 COFF 文件与 PE 文件

image.png


通用目标文件格式 (COFF) 文件,也称为对象文件,具有 obj 文件扩展名。它由 Windows 兼容的编译器生成,将源代码转换为机器代码。该文件类型本身不可执行,但可以作为输入传递给链接器,链接器从一个或多个对象文件创建可执行文件

PE 文件,也称为可执行文件映像文件,这是链接器生成的文件,包含可执行代码运行时映射到内存的数据。映像文件还包含另外两种类型,第一种是动态链接库文件,具有 dll 扩展名,包含可以被多个程序同时导入和使用的代码和数据。 尽管 DLL 文件被归类为可执行文件,但它不能独立地直接运行。第二种是可执行文件,具有 exe 扩展名,与 DLL 不同的是它可以独立运行。

PE 格式以许多文件头开始,文件头是位于数据块起始附加数据,通常包含有关数据块的信息,例如数据块的大小元素在数据块中的位置,以及其他属性。在 PE 格式中,文件头数据由结构体组织和定义,在 C 和 C 相关的编程语言中,结构体是由不同的元素组成的数据类型,这些元素本身可能有多种数据类型。这些元素由不同的变量名称引用,并按顺序存储在连续的内存块中。PE 格式中使用的结构在名为 winnt.h 的头文件中定义,该文件可以作为 Windows SDK 的一部分下载。

image.png


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。

image.png

而 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 字节

image.png


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;

比较重要的元素有 MachineNumberOfSectionsTimeDateStampSizeOfOptionalHeader,以及 Characteristics

image.png

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

image.png

image.png

一些比较重要的元素如下:

Magic 描述了该映像文件是 32 位还是 64 位的,当前是 64 位的。

AddressOfEntryPoint 表示程序入口相对虚拟内存地址 (RVA)。RVA 指的是当 PE 在内存中,距离开头(PE 的第一个字节) 的偏移。程序入口通常是 main 函数,默认在 .text 区域。当前是 0x12D0,而当 PE 存在于磁盘中,入口的偏移是 6D0。这是因为 PE 文件在内存中和在磁盘中的展现出的结构有所不同。

image.png

当 PE 在磁盘以及内存中时,结构示意图如下所示:

image.png

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

image.png

这些目录中,比较

导出目录包含有关解析函数符号所需的不同结构的信息,这些函数由当前 PE 映像导出并可供其他人导入。 此信息允许加载程序找到由其他模块导入的特定函数的地址。 导出通常由 DLL 提供。 在这种情况下,RVA 为“0”,大小为“0”,表示可执行文件没有导出表,不导出任何符号。

另一方面,导入目录包含有关解析可执行文件从其他模块导入的符号所需的不同结构的信息。 此信息有助于可执行文件解析导入函数的地址。 此目录具有 RVA 0x123dc 和大小 0x28。 我们将在以下两个学习单元中更详细地讨论导入和导出。

基本重定位表包含帮助加载程序将可执行文件加载到首选基址以外的地址的信息。 此信息帮助加载程序找到必须根据使用的加载地址重新定位的地址。 我们的可执行文件的重定位表位于 RVA 0x15000。

进口地址表 (IAT) 是包含进口相关信息的特定表。 在磁盘上,它的条目包含帮助加载程序找到导入函数的虚拟地址的数据,一旦找到这些地址,它们就会被相应的地址覆盖。 我们将在后面的部分中更详细地讨论这个问题。



PE 区域

txt


data


rdata


idata


reloc


rsrc


bss




DLL 文件特性