# PE 文件

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

### **PE 文件结构**

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

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/CG1W0rWhNqdRlHpy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/CG1W0rWhNqdRlHpy-image.png)

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

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

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

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TO1lxL3heWlEGQdb-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TO1lxL3heWlEGQdb-image.png)

### **DOS 头与 DOS Stub**  


PE 文件的第一个头文件总是以 **0x4D5A** (**MZ**) 这 2 个字节为前缀，这 2 个字节表示 DOS 头签名，用于确认正在解析或检查的文件是有效的 PE 文件。DOS头是一个数据结构体，定义如下：

```c++
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](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/krpgCkwd5R3EkHbk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/krpgCkwd5R3EkHbk-image.png)

而 DOS Stub 位于 **0x40** 处，DOS Stub 紧接着 DOS 头，因为 DOS 头占用 0x40 字节 (可以根据上文给出的结构体计算出总大小)。DOS Stub 包含了报错信息：**该程序不能再 DOS 模式中运行**。

### **NT 头**  


NT 头十分重要，其结构体如下，包含了**签名**、**文件头**、**可选头**这 3 个元素，NT 头包含了大量有关 PE 的信息。

```c++
//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](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/GVazBtodgNC2Ne8W-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/GVazBtodgNC2Ne8W-image.png)

#### **文件头 File Header**

File 头的结构体如下：

```c++
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**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/oVJY7EyWHNZnXIfa-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/oVJY7EyWHNZnXIfa-image.png)

**Machine** 表示该 PE 文件所期望运行在的 **CPU 架构**，我们能看到该应用期望在 **AMD64** 架构 CPU 的系统上运行。

**NumberOfSection** 表示当前 PE 文件所包含的 **PE 节数量**，该程序有 **6** 个。

**TimeDataStamp** 表示**文件创建的时间**，能看到是 **2023 年 6 月 19 日**。

**SizeOfOptionalHeader** 表示**可选头的尺寸**，这里是 **240**。

**Characteristics** 表示该二进制文件的特定属性，例如是 **DLL** 还是**可执行的文件**，这里显然是**可执行的 exe** 文件。

#### **可选头 Optional Header**

尽管可选头名称为 Optional Header，但却非常重要，只是有些文件类型 (例如对象文件) 没有它。让我们查看它的结构体：

```c++
//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](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/07El5c4sEvUfsaN1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/07El5c4sEvUfsaN1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/mF7v3r1T1b9xUSVT-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/mF7v3r1T1b9xUSVT-image.png)

一些比较重要的成员如下：

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

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

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/ohFSY1CE8IJytHPE-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/ohFSY1CE8IJytHPE-image.png)

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

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NArl767a2GEYSXjy-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NArl767a2GEYSXjy-image.png)

**BaseOfCode** 表示当 PE 在**内存**中时，**.text 的偏移**，这里是 **0x1000**(因为PE头是固定的，所以一般来说都是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**。这些目录包含许多在运行时可能需要的重要**数据结构体条目**以及**函数**，以方便定位到它们。

```c++
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
```

虽然有 NumberOfRvaAndSize 值为 **16**，但目前实际只有 **15** 个目录条目。

```c++
#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](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/JzjoZLGcJsfOFk4D-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/JzjoZLGcJsfOFk4D-image.png)

这些目录中，比较重要的有**导出目录**、**导入目录**、**导入地址表**。导出目录包含有关从可执行文件导出的函数和变量的信息。它包含导出的函数和变量的**地址**，其他可执行文件可以使用这些地址来访问函数和数据。**导出目录**通常存在于导出函数的 **DLL** 中，例如 user32.dll 导出了 MessageBoxW 函数。**导入目录**包含有关解析可执行文件从其他模块导入的符号所需的不同结构体的信息，此信息有助于可执行文件解析导入函数的地址。**导入地址表** (IAT) 是 PE 中的一种数据结构，包含从其他可执行文件导入的函数的地址信息，这些地址用于访问其他可执行文件中的函数和数据。例如当前文件 从 user32.dll 导入了 MessageBoxW 函数。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/TncP4y3QkKCvvZkH-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/TncP4y3QkKCvvZkH-image.png)

### **PE 节** 

PE 节包含用于创建可执行程序的代码和数据，每个 PE 节都有一个**唯一**的名称，并且通常包含**可执行代码**、**数据**或**资源信息**。 PE 节的数量没有固定值，因为不同的编译器可以根据配置添加、删除或合并节，有些部分也可以在之后手动添加，因此 PE 节是动态的，并且 **IMAGE\_FILE\_HEADER.NumberOfSections** 有助于确定该数量。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/vCn5anZKNImDaC5A-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/vCn5anZKNImDaC5A-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/NUzNP3kh4BifytWM-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/NUzNP3kh4BifytWM-image.png)

节表紧接着 **Optional** 头。那么，我们怎么定位到节表呢？首先，我们在 **0x3C** 处找到 **NT 头的偏移**，当前程序是 **0xF0**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/1NelxNBbsqGkHOr4-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/1NelxNBbsqGkHOr4-image.png)

NT 头的结构体如下，**DWORD** 占用 **4** 个字节，因此 File 头的偏移位于 **0xF0+4=0xF4**。

```c++
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
```

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/OOht8VgXReqpTeMG-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/OOht8VgXReqpTeMG-image.png)

File 头的结构体如下，根据数据类型计算总尺寸：2+2+4+4+4+2+2=**0x14**。

```c++
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;
```

加上目前的偏移 **0xF4**，我们得到**可选头**的偏移 **0x108**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/od7WoB775yXL0ZGk-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/od7WoB775yXL0ZGk-image.png)

根据 File 头中的可选头的尺寸 0xF0，最终得到节表的偏移为 0x108+0xF0=**0x1F8**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/yXzGan3SoZaVwQa1-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/yXzGan3SoZaVwQa1-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Ii89R79qb5ouyoyX-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Ii89R79qb5ouyoyX-image.png)

用公式计算的话，偏移如下：

```c++
Offset = PE[0x3c] + 0x18 + PE[PE[0x3c]+0x14]
```

节表里有多个条目，当前是 6 个，查看其结构体：

```c++
#define IMAGE_SIZEOF_SHORT_NAME              8

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** 表示**节名**，通常以 **.** 开头，但并非必要，非常规的节名可能引起恶意软件分析师的怀疑。**VirtualSize** 是一个 **union** 类型数据，考虑到当前文件是一个**映像文件**，因此表示当载入至**内存**时**节的尺寸**。节头包含了文件在内存和磁盘时的信息，**VirtualAddress** 表示该节的 **RVA**。**PointerToRawData** 表示当文件在**磁盘**时该节的偏移，当前为 **0x400**，是 0x200 的 2 倍。SizeOfRawData 表示的则是在磁盘时该节的尺寸，这里是 0xE00。对于 .text 节，如果 **VirtualSize 的尺寸远大于 SizeOfRawData**，意味着可能被 packer 打包过了。

这些节都是紧挨着一起的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/999DnfkoAx0Tqq1c-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/999DnfkoAx0Tqq1c-image.png)

由于尺寸需要是对齐系数的整数倍，原始数据之后被 **0x00** 填充。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XvrcLA8rmV5h3p8F-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XvrcLA8rmV5h3p8F-image.png)

Characteristics 表示节的属性，可以查看微软文档 [https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-flags](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#section-flags)。

接下来，我们分别讨论一下一些常见节的作用：

**text** 包含了**可执行的代码**，**可读可执行但不可写**。

**data** 包含了**初始化的变量**，可能为全局变量和静态变量，因为这些变量可以在运行时更改，因此该**节可读可写**。

**bss** 包含了**未初始化的变量**，**可读可写**。

**rdata** 包含了**可读的初始化数据**，常量，可能为全局常量或静态变量，**不可写**。根据编译器的配置，**edata** 和 **idata** 节可被整合进 rdata 节。

**edata** 包含了 **EAT**。该节可随着编译器的配置被整合进 **rdata** 节。

**idata** 包含了 **IAT**。该节可随着编译器的配置被整合进 **rdata** 节。下文中我们会知道 **IAT** 会被逐渐更新，而 rdata 节是**只读**的，因为**延迟加载的 DLL** 可以暂时更改页的权限从而允许写。

**reloc** 包含了给程序分配地址的信息。

**rsrc** 包含了**资源信息**，例如图标、字符串等。

### **导入与导出**

接下来，我们着重讨论一下 PE 文件的导入与导出。用 PE Bear 载入一**非托管 DLL**，例如 **user32.dll**。在导出部分，我们能看到该 DLL 导出了大量的函数供其他 PE 文件所用。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/KUsjDrbESfeNVNfI-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/KUsjDrbESfeNVNfI-image.png)

导出目录的结构体如下：

```c++
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // EAT (Export Address Table)的 RVA
    DWORD   AddressOfNames;         // ENPT (Export Name Pointer Table)的 RVA
    DWORD   AddressOfNameOrdinals;  // OT (Ordinal Table)的 RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
```

该表的主要作用是存储其他导出相关表的位置。具体来说，**AddressOfFunctions** 指向**导出地址表** (EAT)，**AddressOfNames** 指向**导出名称指针表** (ENPT)，**AddressOfNameOrdinals** 指向**序数表**。为了能理解这些表，我们需要知道函数是如何被引用的，通过**名称**或者**序数**。

假设是通过**序数**来导入，序数是 **EAT** 的索引，每个条目包含函数的 **RVA**。

如果是使用**名称**来导入，过程会更加复杂一些。在 **ENPT** 中，每个条目都有一个索引，当在**索引 i** 处找到想要的**函数名称的 RVA** (进而得到函数的名称)，在序数表的**索引 i** 处，能得到另一个索引，记为 **j**。而在 EAT 的**索引 j** 处，最终得到了函数的 **RVA**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/Wv60HqB23dkiOTnK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/Wv60HqB23dkiOTnK-image.png)

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/SnOL3BE2rNNOGypW-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/SnOL3BE2rNNOGypW-image.png)

```c++
//通过序数
//在EAT的第i处找到想要的函数
RVA = EAT[i]

//通过名称
//在ENPT的第i处找到想要的函数名称
RVA = EAT[ OT[i] ] //OT[i]=j
```

我们理解了导出的步骤，接下来讨论一下导入。继续以我们上个小节编写的 MessageBox C++ 程序为例。在 PE Bear 中查看导入部分信息：

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/k6gBw2Wq9osW7KAe-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/k6gBw2Wq9osW7KAe-image.png)

图中部分是**导入目录**，每个条目是导入的 DLL，条目的结构体如下：

```c++
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;
        DWORD   OriginalFirstThunk; // ILT/INT (Import Lookup Table)的 RVA
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;
    DWORD   ForwarderChain;
    DWORD   Name;
    DWORD   FirstThunk;             // IAT (Import Address Table)的 RVA
} IMAGE_IMPORT_DESCRIPTOR;
```

**Name** 是 **DLL 名称字符串的 RVA**，**OriginalFirstThunk** 指向特定 DLL 的 **ILT/INT** (**导入查询表**)，**FirstThunk** 指向 **IAT**。在导入的函数名被解析为**地址**之前，ILT 与 IAT 具有相同的内容，并且由每个导入函数的条目组成，条目的数据类型是 **\_IMAGE\_THUNK\_DATA64 结构体**。

IAT 与 ILT 中的条目的结构体如下：

```c++
typedef struct _IMAGE_THUNK_DATA64 {
   union {
      ULONGLONG ForwarderString;  
      ULONGLONG Function;        
      ULONGLONG Ordinal;    // (最高位为1) 函数序数
      ULONGLONG AddressOfData;    // (最高位为0) HintName 表/_IMAGE_IMPORT_BY_NAME 结构体的 RVA
   } u1;
} IMAGE_THUNK_DATA64, *PIMAGE_THUNK_DATA64;

```

每个条目的**最高位**为**序数/名称**标志，用于指定是通过序数还是函数名导入函数。如果该位设置为 **1**，那么一些位会包含**函数的序数值**，反之则是 **Hint/Name 表** 的 RVA。当这些导入函数的地址被解析后，IAT 条目将被**解析的地址**覆盖。

IMAGE\_THUNK\_DATA64 结构体只有 1 个成员，对于可执行文件，该 **ULONGLONG** 成员可以有 2 种含义，分别是 Ordinal，表示**序数**；以及 **AddressOfData**，是 Hint/Name 表的 RVA。Hint/Name 表的数据类型是 **IMAGE\_IMPORT\_BY\_NAME** 结构体：

```c++
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;    // ENPT 的索引
    CHAR   Name[1];  // 导入函数的名称的字符串
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
```

成员 **Hint** 是 **ENPT** 的索引，**Name** 是包含**导入函数名称的字符串**，被用于确认 Hint 位于 ENPT 中的正确位置，或者(如果当 Hint 不对) 寻找正确的位置。因为 ENPT 中的条目会让我们得到序数，并最终在 EAT 中得到函数的 RVA。当 **RVA** 被找到，IAT 表中的条目将被重写为函数的 RVA。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/lV4RbUJ74IZPBSOB-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/lV4RbUJ74IZPBSOB-image.png)

总之，每个载入的 DLL 都以结构体 IMAGE\_IMPORT\_DESCRIPTOR 条目的形式出现在导入目录中，该结构体包含了 DLL 的名称、ILT 以及 IAT 的 RVA。在载入之前，IAT 与 ILT 是相同的，包含了导入函数的引用。当载入时，加载器检查 **IAT** 条目，并通过检查条目的**最高位**是否被设置来确定是按序数导入还是按名称导入。如果导入按序数进行，只需通过检查相应 DLL 的导出数据将序数解析为地址，并用该地址覆盖 IAT 条目。如果导入是按名称完成的，则首先检查条目指向的 **IMAGE\_IMPORT\_BY\_NAME** (Hint/Name 表) 结构体，然后使用 Hint 和函数名称将函数解析为地址并覆盖 IAT 条目。

查看 IAT，因为**序数**为空，因此是通过名称导入的。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-06/scaled-1680-/XYvTKELo1Uk7vacK-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-06/XYvTKELo1Uk7vacK-image.png)

### **原始偏移与 RVA 转换**

我们知道 Offset 表示当 PE 文件存在于磁盘中，某一位置距离文件头的偏移，而 RVA 表示当 PE 文件被加载至内存中，与基址的距离。那么，我们怎么实现将 RVA 转化为 Offset 呢？

首先，我们需要确定给定 RVA 所在的节。以导出表结构体 Name 成员 (不一定每个 PE 文件都有，以一非托管 DLL 为例) 为例，该成员指向当前**模块的名称**，即名称**字符串的 RVA**，我们可以看到 RVA 是 **0x28A2**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/e3DIfYvc7bm7PMZt-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/e3DIfYvc7bm7PMZt-image.png)

查看节表，我们发现该 RVA 在 rdata 节，因为 **28A2 &lt; 2CB8**。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/QERFsRcWhNDjj4Tr-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/QERFsRcWhNDjj4Tr-image.png)

用该 RVA 减去此节的 RVA，这里是 0x2000，得到 0x8A2。最后，我们加上此节的 PointerToRawData，即当该文件在磁盘中时，此节的偏移值，这里是 0x1400。

***Offset = 0x28A2 - 0x2000 + 0x1400 = 0x1CA2***

跳转到 0x1CA2 的原始偏移处，正是该模块的名称。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2023-07/scaled-1680-/WawgXfxZ5enTambQ-image.png)](https://raven-medicine.com/uploads/images/gallery/2023-07/WawgXfxZ5enTambQ-image.png)

综上，公式如下：

```c++
1: 确定该RVA处于哪个节，节的范围为 [_IMAGE_SECTION_HEADER.VirtualAddress, _IMAGE_SECTION_HEADER.VirtualAddress + _IMAGE_SECTION_HEADER.VirtualSize]
2: 原始偏移 = RVA - _IMAGE_SECTION_HEADER.VirtualAddress + _IMAGE_SECTION_HEADER.PointerToRawData
```

### **总结**

总结一下在 PE 文件中一些重要属性的位置，并根据自己需要进一步增加与完善表格的条目。

<table border="1" id="bkmrk-%E5%90%8D%E7%A7%B0-%E5%81%8F%E7%A7%BB-%E5%80%BC-%E5%A4%87%E6%B3%A8-pe-%E5%A4%B4-0-0x" style="border-collapse: collapse; width: 100%; height: 710.4px;"><colgroup><col style="width: 21.8735%;"></col><col style="width: 46.2215%;"></col><col style="width: 31.8803%;"></col></colgroup><tbody><tr style="height: 29.6px;"><td style="height: 29.6px;">**名称**</td><td style="height: 29.6px;">**偏移值**  
</td><td style="height: 29.6px;">**备注**  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">PE 头  
</td><td style="height: 29.6px;">0  
</td><td style="height: 29.6px;">固定值 **0x4D5A**  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">NT 头偏移  
</td><td style="height: 29.6px;">0x3C  
</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">NT 头  
</td><td style="height: 29.6px;">PE \[0x3C\]  
</td><td style="height: 29.6px;">固定值 **0x50450000**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">File 头  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x4  
</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">节数目  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x6</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">可选头尺寸  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x14</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">可选头  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">程序入口  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x28</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">.text 节 RVA  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x2C</td><td style="height: 29.6px;">通常为 **0x1000**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">映像偏好基址  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x30</td><td style="height: 29.6px;">  
</td></tr><tr><td>映像尺寸</td><td>PE\[0x3C\] + 0x50</td><td>  
</td></tr><tr><td>.text 节 Offset</td><td>PE\[0x3C\] + 0x54</td><td>通常为 **0x400**</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">导出目录 RVA x86  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18 + 0x60  
</td><td style="height: 29.6px;">**PE32**。除此行之外皆为 **PE64** 下的情况  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">导出目录 RVA x64  
</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18 + 0x70  
</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">模块名称 x64  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0xC</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">模块基址 x64  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x10</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">函数数量  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x14</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">函数名称数量  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x18</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">EAT 的 RVA  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x1C</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">EAT  
</td><td style="height: 29.6px;">PE\[PE\[PE\[0x3c\] + 0x88\] + 0x1C\]  
</td><td style="height: 29.6px;">当模块被载入内存中时  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">ENPT 的 RVA  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x20</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">ENPT  
</td><td style="height: 29.6px;">PE\[PE\[PE\[0x3c\] + 0x88\] + 0x20\]  
</td><td style="height: 29.6px;">当模块被载入内存中时</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">序数表的 RVA  
</td><td style="height: 29.6px;">PE\[PE\[0x3c\] + 0x88\] + 0x24</td><td style="height: 29.6px;">  
</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">序数表  
</td><td style="height: 29.6px;">PE\[PE\[PE\[0x3c\] + 0x88\] + 0x24\]  
</td><td style="height: 29.6px;">当模块被载入内存中时</td></tr><tr style="height: 29.6px;"><td style="height: 29.6px;">节表</td><td style="height: 29.6px;">PE\[0x3C\] + 0x18 + PE\[ PE\[0x3C\] +0x14\]</td><td style="height: 29.6px;">  
</td></tr></tbody></table>