PE 文件结构全解析:从地址类型到头部结构详解

PE文件

地址

PE中涉及的地址有四类:

  1. 虚拟内存地址(VA)

    PE被加载到内存空间,在这个内存空间中定位的地址称为虚拟地址。

  2. 相对虚拟内存地址(RVA)

    ​ 一个进程被加载到虚拟内存空间后,与其相关的动态链接库也会被加载。这些同时加载到内存空间的文件称为模块。每个模块在加载时都有一个基地址,也就是它会预先告诉操作系统,它会占用4G空间的哪一部分。不同模块的基地址是不同的,如果两个模块的基地址相同,就由操作系统来决定这两个模块在虚拟地址空间中的具体位置。

    ​ 相对虚拟地址,是相对于模块基地址的偏移。即RVA是虚拟内存中用来定位某个特定位置的地址,该地址的值,是这个特定位置距离某个模块基地址的偏移量。所以说,RVA 是相对于某个模块而存在的。

  3. 文件偏移地址(FOA)

    ​ 文件偏移地址和内存无关,它指的某个位置距离文件头的偏移地址。我们用一些HEX文件编辑工具或者静态调试工具(IDA)时候,显示的地址一般是文件偏移地址。只有这个模块加载到内存后,内存中的地址才是虚拟地址。

  4. 特殊地址

    ​ 在PE结构中还有一类特殊地址,其计算方法并不是从头算起,也不是从内存的某个模块的基地址算起,而是从某个特定的位置算起。这种地址在PE结构中很少见,在资源表里就出现过这样的地址。

数据目录

目前已经定义的数据目录有15种,包括导出表,导入表,资源表,异常表,属性证书表,重定位表,调试数据,Architecture,Global Ptr,线程局部存储,加载配置表,绑定导入表,IAT,延迟导入表,CLR运行时头部。

节(section)

节就是存放不同类型的数据(比如代码、数据、常量、资源)的地方,不同的节具有不同的访问权限。节是PE文件中存放代码或者数据的基本单元。一个节中的所有原始数据必须被加载到连续的内存空间中。

从操作系统加载的角度来看,节是相同属性的数据的组合。与数据目录不同的是,尽管有些数据类型不同,分别属于不同的数据目录,但由于其访问属性相同,便被归类到一个节中。这个节最终可能会占用一个或者多个页(虚拟内存页);但是不论有多少个页,所有页都会被赋予相同的属性。这些属性包括,只读、只写、可读、可写。

汇编语言中,一些以.开头的伪指令,其实就是在声明不同的数据类型。

出于节约资源考虑,操作系统允许节在磁盘和在内存中的对齐尺寸不一样,这就直接造成了PE在文件中和在内存中的大小也不一致。通常PE在内存中的尺寸要比在磁盘文件中的尺寸大,用户可以自定义这些对齐的值。

注意:如果内存对齐被定义为小于操作系统页的大小,则文件对齐和内存对齐的值必须一致。

结构

7ae6021541423133662d1b530c820930.jpg

Dos头结构

c 复制
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;                  // 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;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

NT头结构

c 复制
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
(1)PE文件标准PE头(用于判断PE文件是exe还是dll,得到节的总量)
c 复制
typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine; //运行平台 
    WORD    NumberOfSections; //PE中节的数量
    DWORD   TimeDateStamp; //文件创建日期和时间
    DWORD   PointerToSymbolTable; //指向符号表(用于调试)
    DWORD   NumberOfSymbols; //符号表中的符号数量(用于调试)
    WORD    SizeOfOptionalHeader; //扩展头结构长度
    WORD    Characteristics; //文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
(2)PE扩展头(虽然是扩展头,但更像真正的PE头,非常重要,包含各种属性)
c 复制
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //
    
    WORD    Magic; //魔幻字  0x10b 32位 0x20b 64位
    BYTE    MajorLinkerVersion; //链接器版本号
    BYTE    MinorLinkerVersion; //链接器版本号
    DWORD   SizeOfCode; //所有含代码节的总大小
    DWORD   SizeOfInitializedData; //初始化的数据长度。
    DWORD   SizeOfUninitializedData; //未初始化的数据长度。
    DWORD   AddressOfEntryPoint; //程序入口的RVA
    DWORD   BaseOfCode; //代码段起始地址的RVA
    DWORD   BaseOfData; //数据段起始地址的RVA

    //
    // NT additional fields.
    //

    DWORD   ImageBase; //映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。
    DWORD   SectionAlignment; //内存中节对齐粒度,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。
    DWORD   FileAlignment; //文件中节对齐粒度,SectionAlignment必须大于或等于FileAlignment。
    WORD    MajorOperatingSystemVersion; //所需操作系统的版本号
    WORD    MinorOperatingSystemVersion; //
    WORD    MajorImageVersion; //映象的版本号,这个是开发者自己指定的,由连接器填写。
    WORD    MinorImageVersion; //
    WORD    MajorSubsystemVersion; //所需子系统版本号
    WORD    MinorSubsystemVersion; //
    DWORD   Win32VersionValue; //保留,必须为0。
    DWORD   SizeOfImage; //映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。
    DWORD   SizeOfHeaders; //所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。
    DWORD   CheckSum; //映象文件的校验和。
    WORD    Subsystem; //运行该PE文件所需的子系统
    WORD    DllCharacteristics; //DLL的文件属性,只对DLL文件有效
    DWORD   SizeOfStackReserve; //运行时为每个线程栈保留内存的大小。
    DWORD   SizeOfStackCommit; //运行时为每个线程栈保留内存的大小。
    DWORD   SizeOfHeapReserve; //运行时为进程堆保留内存大小。
    DWORD   SizeOfHeapCommit; //运行时进程堆初始占用内存大小。
    DWORD   LoaderFlags; //保留,必须为0。
    DWORD   NumberOfRvaAndSizes; //数据目录的项数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //数据目录,这是一个数组,定义了PE文件中所有出现的不同类型的数据目录信息
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

本头的第一个字节相对于nt头的偏移量为 0x18h,数据目录(DataDirectory)开始字节相对于nt头的偏移量为0x78h,所以除去数据目录(大小可变)之外的部分大小为 0x60,即96个字节大小。

数据目录项

总的数据目录一共由16个相同的_IMAGE_DATA_DIRECTORY组成。每一个代表一个类型的数据。

结构体
c 复制
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress; //数据的起始RVA          
    DWORD   Size; //数据块的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
节表项的数据结构
c 复制
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME]; //8个字节的节名
    union {
		DWORD   PhysicalAddress; //节区大小
		DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress; //节区RVA地址
    DWORD   SizeOfRawData; //在文件中对齐后的地址
    DWORD   PointerToRawData; //在文件中的偏移
    DWORD   PointerToRelocations; //在OBJ文件中使用
    DWORD   PointerToLinenumbers; //行号表的位置(调试用)
    WORD    NumberOfRelocations; //OBJ文件中使用
    WORD    NumberOfLinenumbers; //行号表中行号的数量
    DWORD   Characteristics; //节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

节表的大小为 0x0028h,40个字节大小。

c 复制
#define IMAGE_FILE_MACHINE_UNKNOWN           0       // 适用于任何机器    
#define IMAGE_FILE_MACHINE_TARGET_HOST       0x0001  
#define IMAGE_FILE_MACHINE_I386              0x014c  // intel386处理器或后续兼容处理器
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS小尾处理器
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS小尾WCEv2处理器
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // Hitachi SH3处理器
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3  // Hitachi SH3 DSP处理器
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // Hitachi SH4处理器
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // Hitachi SH5处理器
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM小尾处理器
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM或Thumb处理器
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARMv7(或更高)处理器的Thumb模式
#define IMAGE_FILE_MACHINE_AM33              0x01d3  // MatsushitaAM33处理器
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // PowerPC小尾处理器
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1  // 带浮点支持的PowerPC处理器
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel Itanium处理器系列
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS16处理器
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // 带FPU的MIPS处理器
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // 带FPU的MIPS16处理器
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI字节码处理器
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // x64处理器
#define IMAGE_FILE_MACHINE_M32R              0x9041  // MatsushitaM32R小尾处理器
#define IMAGE_FILE_MACHINE_ARM64             0xAA64  // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

IMAGE_NT_HEADERS

这个结构是广义上的PE头,在标准PE文件中其大小为456个字节。4 + 20 + 96 + 16 x 8 = 248。

IMAGE_NT_HEADERS = 4个字节的pe标识 + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER32

image-20240904155142876.png

详细的结构定义如下:

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