Skip to content →

PE文件格式

PE是Portable Executable的简写,意思是可移植的可执行文件。此文件格式是跨win32平台的,即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器也都能识别和使用该文件格式。PE文件格式可以概括为:

DOS MZ header | DOS stub | PE header | section table | section 1 | ... | section n

DOS依据简单的DOS MZ header来判断执行体是否有效。DOS stub是个有效的实模式残余程序,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示。PE header是PE相关结构IMAGE_NT_HEADERS的简称,包含了许多PE装载器用到的重要域。PE文件的真正内容划分成块,称之为sections(节),每节是一块拥有共同属性的数据。在PE header和sections之间的数组结构是section table(节表),每个表项包含对应节的属性、文件偏移量、虚拟偏移量等。

PE文件加载机制

当PE文件被执行,PE装载器检查DOS MZ header里的PE header偏移量,如果找到,则跳转到PE header。然后PE装载器检查PE header的有效性,若有效,就跳转到PE header的尾部。PE装载器从节表中读取节信息,并采用文件映射方法将这些节映射到内存,同时附上节表里指定的节属性。PE文件映射入内存后,PE装载器将处理PE文件中的逻辑部分。

加载器读取一个PE文件的具体过程如下:

  1. 读入PE文件的DOS头,PE头和Section头。
  2. 判断PE头里ImageBase所定义的优先加载地址是否可用,如果已被其他模块占用,则重新分配一块空间。
  3. 根据Section头部的信息,把文件的各个Section映射到分配的空间,并根据各个Section的属性来修改所映射的页的属性。
  4. 如果文件被加载的地址不是ImageBase所定义的地址,则修正ImageBase的值。
  5. 根据PE文件的导入表加载所需要的DLL到进程空间。
  6. 替换IAT表内的数据为实际调用函数的地址。
  7. 根据PE头内的数据生成初始化的堆和栈。
  8. 创建初始化线程,开始运行进程。

DOS头

DOS头部DOS MZ header又命名为IMAGE_DOS_HEADER,在该结构体中,有两个字段比较重要:第一个字段e_magic,占用两个字节,值为0x5A4D,即字符串“MZ”;最后一个字段e_lfanew,值为PE文件头在PE文件中的偏移量。

typedef struct _IMAGE_DOS_HEADER {
+000h    WORD                e_magic;     // Magic number
+002h    WORD                e_cblp;      // Bytes on last page of file
+004h    WORD                e_cp;        // Pages in file
+006h    WORD                e_crlc;      // Relocations
+008h    WORD                e_cparhdr;   // Size of header in paragraphs
+00Ah    WORD                e_minalloc;  // Minimum extra paragraphs needed
+00Ch    WORD                e_maxalloc;  // Maximum extra paragraphs needed
+00Eh    WORD                e_ss;        // Initial (relative) SS value
+010h    WORD                e_sp;        // Initial SP value
+012h    WORD                e_csum;      // Checksum
+014h    WORD                e_ip;        // Initial IP value
+016h    WORD                e_cs;        // Initial (relative) CS value
+018h    WORD                e_lfarlc;    // File address of relocation table
+01Ah    WORD                e_ovno;      // Overlay number
+01Ch    WORD                e_res[0x4];  // Reserved words
+024h    WORD                e_oemid;     // OEM identifier (for e_oeminfo)
+026h    WORD                e_oeminfo;   // OEM information; e_oemid specific
+028h    WORD                e_res2[0xA]; //Reserved words
+03Ch    LONG                e_lfanew;    // File address of new exe header
} IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;

PE头

PE Header又称为NT头,正式命名是IMAGE_NT_HEADERS。在该结构体中,Signature值为0x00004550,即字符串”PE\x0\x0″;FileHeader包含了关于PE文件物理分布的信息;OptionalHeader包含了关于PE文件逻辑分布的信息。

typedef struct _IMAGE_NT_HEADERS {
+000h    DWORD               Signature;           // PE文件标志
+004h    IMAGE_FILE_HEADER   FileHeader;          // PE文件头
+018h    IMAGE_OPTIONAL_HEADER32 OptionalHeader;  // PE文件可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

PE文件头

typedef struct _IMAGE_FILE_HEADER {
+000h    WORD                Machine;              // 文件运行所要求的CPU
+002h    WORD                NumberOfSections;     // 文件中节的数目
+004h    DWORD               TimeDateStamp;        // 文件创建时间戳
+008h    DWORD               PointerToSymbolTable; // 符号表
+00Ch    DWORD               NumberOfSymbols;      // 符号数
+010h    WORD                SizeOfOptionalHeader; // 文件可选头的大小
+012h    WORD                Characteristics;      // 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PE文件可选头

typedef struct _IMAGE_OPTIONAL_HEADER {
+000h    WORD                Magic;                       // 标识文件格式
+002h    BYTE                MajorLinkerVersion;          // 链接器主版本号
+003h    BYTE                MinorLinkerVersion;          // 链接器次版本号
+004h    DWORD               SizeOfCode;                  // 代码区块大小
+008h    DWORD               SizeOfInitializedData;       // 初始化数据区块大小
+00Ch    DWORD               SizeOfUninitializedData;     // 未初始化数据区块大小
+010h    DWORD               AddressOfEntryPoint;         // 程序执行入口的RVA
+014h    DWORD               BaseOfCode;                  // 内存中代码区块的RVA
+018h    DWORD               BaseOfData;                  // 内存中数据区块的RVA
+01Ch    DWORD               ImageBase;                   // 文件在内存的首选加载地址
+020h    DWORD               SectionAlignment;            // 内存中节对齐的粒度
+024h    DWORD               FileAlignment;               // 文件中节对齐的粒度
+028h    WORD                MajorOperatingSystemVersion; // 操作系统主版本号
+02Ah    WORD                MinorOperatingSystemVersion; // 操作系统次版本号
+02Ch    WORD                MajorImageVersion;           // 程序主版本号
+02Eh    WORD                MinorImageVersion;           // 程序次版本号
+030h    WORD                MajorSubsystemVersion;       // 子系统主版本号
+032h    WORD                MinorSubsystemVersion;       // 子系统次版本号
+034h    DWORD               Win32VersionValue;           // 保留,通常置为0
+038h    DWORD               SizeOfImage;                 // 内存中整个PE映像体的尺寸
+03Ch    DWORD               SizeOfHeaders;               // DOS头+PE头+节表的大小
+040h    DWORD               CheckSum;                    // 校验和
+044h    WORD                Subsystem;                   // 可执行文件期望的子系统
+046h    WORD                DllCharacteristics;          // 标记DLL的特性
+048h    DWORD               SizeOfStackReserve;          // 为线程保留的堆栈大小
+04Ch    DWORD               SizeOfStackCommit;           // 实际堆栈大小
+050h    DWORD               SizeOfHeapReserve;           // 为进程保留的堆大小
+054h    DWORD               SizeOfHeapCommit;            // 实际堆大小
+058h    DWORD               LoaderFlags;                 // 装载标志
+05Ch    DWORD               NumberOfRvaAndSizes;         // 数据目录的项数
+060h    IMAGE_DATA_DIRECTORY DataDirectory[16];          // 数据目录
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

数据目录表

可选头最后一个字段DataDirectory,即数据目录表,是一个IMAGE_DATA_DIRECTORY结构数组,共16个成员,总大小为2*4*16=128字节,包含了PE文件中各个重要数据结构的位置和尺寸信息。

# 预定义值 数据结构
0 ***_EXPORT 导出表(IMAGE_EXPORT_DIRECTORY结构
1 ***_IMPORT 导入表(IMAGE_IMPORT_DESCRIPTOR结构数组
2 ***_RESOURCE 资源(IMAGE_RESOURCE_DIRECTORY结构
3 ***_EXCEPTION 异常处理表(IMAGE_RUNTIME_FUNCTION_ENTRY数组
4 ***_SECURITY WIN_CERTIFICATE结构列表
5 ***_BASERELOC 基址重定位信息
6 ***_DEBUG 调试信息(IMAGE_DEBUG_DIRECTORY结构数组
7 ***_ARCHITECTURE 特定架构(IMAGE_ARCHITECTURE_HEADER结构数组
8 ***_GLOBALPTR 某些架构VirtualAddress是个RVA,用作全局指针
9 ***_TLS 线程局部存储初始化节(Thread Local Store
10 ***_LOAD_CONFIG IMAGE_LOAD_CONFIG_DIRECTORY结构
11 ***_BOUND_IMPORT IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组
12 ***_IAT 第一个导入地址表
13 ***_DELAY_IMPORT 延迟加载信息(CImgDelayDescr结构数组
14 ***_COM_DESCRIPTOR .NET最高级别信息(IMAGE_COR20_HEADER结构
15 未定义  
  注:***表示IMAGE_DIRECTORY_ENTRY

数据目录表的每个成员都是一个IMAGE_DATA_DIRECTORY结构,该结构定位了PE文件中的某个重要数据结构(详见上表),字段VirtualAddress是重要数据结构的RVA,字段Size是重要数据结构的字节数。

typedef struct _IMAGE_DATA_DIRECTORY {
000h    DWORD               VirtualAddress;
004h    DWORD               Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  • 导入表

数据目录表的第二项是导入表,它是一个IMAGE_IMPORT_DESCRIPTOR结构数组,每个结构都对应一个DLL,PE文件从几个不同的DLL中导入函数,导入表就有几个元素,并以一个全0的元素结尾。在PE文件加载时,会根据这个表里的内容加载依赖的DLL,并填充所需函数的地址。

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
+000h    DWORD               Characteristics; 
+000h    DWORD               OriginalFirstThunk; // 某IMAGE_THUNK_DATA数组的RVA
};
+004h    DWORD               TimeDateStamp;      // 导入模块(即dll)的时间戳
+008h    DWORD               ForwarderChain;     // 转发链,若无转发器,该值为-1
+00Ch    DWORD               Name;               // 指向导入模块(即dll)名字的RVA
+010h    DWORD               FirstThunk;         // 某IMAGE_THUNK_DATA数组的RVA
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

字段OriginalFirstThunk和FirstThunk都指向IMAGE_THUNK_DATA结构数组,数组中的每一项对应一个导入函数,字段ForwarderString是转发用的,Function表示导入函数地址,若是按序号导入则Ordinal最高位为1且低16位是导入序号,若是按名字导入则AddressOfData指向名字信息。加载前OriginalFirstThunk与FirstThunk都指向名字信息,加载后FirstThunk指向实际的函数地址。

typedef struct _IMAGE_THUNK_DATA32 {
union {
+000h    DWORD               ForwarderString; // PBYTE
+000h    DWORD               Function;        // PDWORD
+000h    DWORD               Ordinal;
+000h    DWORD               AddressOfData;   // PIMAGE_IMPORT_BY_NAME
};
} IMAGE_THUNK_DATA32, *PIMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
+000h    WORD                Hint;            // 导入函数在Dll中的编号(ordinal)
+002h    BYTE                Name[1];         // 导入函数的符号名称,以\0结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
  • 导出表

一个Windows程序的所有功能,最终几乎都是调用DLL文件提供的API函数来实现的。一个程序要使用其他DLL文件所提供的函数,需要在程序加载时将该DLL文件导入,此时用到的是导入表。而一个DLL文件想让其他程序使用它提供的API函数,必须在文件中包含这些函数的导出信息,此时用到的是导出表,即Export Table,数据目录的第一项定位了该导出表。通过导出表,DLL文件向系统提供了API函数的名称、序号和入口地址等信息。

当PE文件被执行时,PE装载器会将PE文件以及导入表中登记的DLL文件装入内存,再根据这些DLL文件的导出表对被执行文件的IAT表进行修正。

typedef struct _IMAGE_EXPORT_DIRECTORY {
+000h    DWORD               Characteristics;       // 未使用,值为0
+004h    DWORD               TimeDateStamp;         // 输出表创建的时间
+008h    WORD                MajorVersion;          // 主版本号
+00Ah    WORD                MinorVersion;          // 次版本号
+00Ch    DWORD               Name;                  // 指向模块的真实名称的RVA
+010h    DWORD               Base;                  // 基数
+014h    DWORD               NumberOfFunctions;     // 模块中导出函数个数
+018h    DWORD               NumberOfNames;         // 通过名字导出的函数个数
+01Ch    DWORD               AddressOfFunctions;    // 指向所有函数地址的RVA数组
+020h    DWORD               AddressOfNames;        // 指向所有函数名字的RVA数组
+024h    DWORD               AddressOfNameOrdinals; // 指向所有函数序号的RVA数组
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

其中,AddressOfNameOrdinals指向的数组里面,保存的是该DLL文件的导入表中的函数的hint(也就是Ordinal),每个元素都是WORD大小(占2个字节)。导出表中的函数,在AddressOfNameOrdinals数组里的索引,和它在AddressOfName那个数组中的索引是完全一样的。

PE装载器根据函数名称查找导出函数入口地址的整个过程:定位到DLL文件的PE Header,从数据目录读取导出表的虚拟地址,定位导出表,获取名字数目NumberOfNames,并行遍历 AddressOfNames数组和AddressOfNameOrdinals数组,如果在 AddressOfNames数组的第m个元素找到匹配,就提取AddressOfNameOrdinals数组的第m个元素n,于是读取AddressOfFunctions数组的第n个元素,此值就是导出函数的RVA,最后计算入口地址:

API’s Address = AddressOfFunctions[n-1] + the BaseAddress of Kernel32
              = the VA of AddressOfFunctions + (n-1)*4 + the BaseAddress of Kernel32

节表

节表Section Table是一个结构数组,每个元素都是一个IMAGE_SECTION_HEADER结构,包含了对应节的属性、文件偏移量、虚拟偏移量等。

typedef struct _IMAGE_SECTION_HEADER {
+000h    BYTE Name[8]; //节名称,如".text"
union {
+008h    DWORD               PhysicalAddress;      // 物理地址
+008h    DWORD               VirtualSize;          // 节长度
} Misc;
+00Ch    DWORD               VirtualAddress;       // RVA
+010h    DWORD               SizeOfRawData;        // 对齐后的节尺寸
+014h    DWORD               PointerToRawData;     // 基于文件的偏移
+018h    DWORD               PointerToRelocations; // 重定位的偏移
+01Ch    DWORD               PointerToLinenumbers; // 行号表的偏移
+020h    WORD                NumberOfRelocations;  // 重定位项数目
+022h    WORD                NumberOfLinenumbers;  // 行号表中行号数目
+024h    DWORD               Characteristics;      // 节属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

PE装载器把节映射到内存的具体过程:

  1. 定位节表:读取IMAGE_FILE_HEADER->NumberOfSections,作为节的数目;读取IMAGE_OPTIONAL_HEADER->SizeOfHeaders,作为节表的文件偏移量。然后遍历节表中的每个结构:
  2. 读取IMAGE_SECTION_HEADER->PointerToRawData得到对应节的文件偏移量offset,读取IMAGE_SECTION_HEADER->SizeOfRawData得到对应节的字节数n。
  3. 读取IMAGE_OPTIONAL_HEADER->ImageBase得到文件装载到虚拟地址空间的基地址base,读取IMAGE_SECTION_HEADER->VirtualAddress得到对应节的相对虚拟地址VA。
  4. 把文件偏移offset开始的n个字节映射到内存的 base + VA 位置,并根据IMAGE_SECTION_HEADER->Characteristics设置属性。

Published in 未分类