0%

PE文件格式解析(1)

简介

PE(Portable Executable)格式,是微软Win32环境可执行文件(如exe、dll、sys等)的标准格式。
PE格式继承自COFF(Common Object File Format)文件格式。

具体结构

具体的PE格式结构体在 winnt.h 头文件中有定义,格式分布如下图所示

PE格式

DOS头(DOS_HEADER)

在PE文件最开始的位置,包含有DOS执行体的信息,当该程序在不支持 PE文件格式的操作系统中,它将
显示一个错误提示,大多数情况下它是由汇编器/编译器自动生成,即:简单调用中断21h服务来显示
字符串”This program cannot run in DOS mode”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // [0x00] Magic number // 0x5A4D
WORD e_cblp; // [0x02] Bytes on last page of file
WORD e_cp; // [0x04] Pages in file
WORD e_crlc; // [0x06] Relocations
WORD e_cparhdr; // [0x08] Size of header in paragraphs
WORD e_minalloc; // [0x0A] Minimum extra paragraphs needed
WORD e_maxalloc; // [0x0C] Maximum extra paragraphs needed
WORD e_ss; // [0x0E] Initial (relative) SS value
WORD e_sp; // [0x10] Initial SP value
WORD e_csum; // [0x12] Checksum
WORD e_ip; // [0x14] Initial IP value
WORD e_cs; // [0x16] Initial (relative) CS value
WORD e_lfarlc; // [0x18] File address of relocation table
WORD e_ovno; // [0x1A] Overlay number
WORD e_res[4]; // [0x1C] Reserved words
WORD e_oemid; // [0x24] OEM identifier (for e_oeminfo)
WORD e_oeminfo; // [0x26] OEM information; e_oemid specific
WORD e_res2[10]; // [0x28] Reserved words
LONG e_lfanew; // [0x3C] File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS_HEADER 中关键的信息有两处,其一是 e_magic 信息,必为 0x5A4D 值,其二是 e_lfanew 信息,
表示 NT_HEADER 的偏移位置。使用VS编译的EXE程序多数为 0xF8 值。

标识信息

PE头(NT_HEADER)

NT_HEADER 中共包含三部分信息,其中 Signature 必为 0x4550 值,FILE_HEADER 在x86和x64系统中
大小是一样的,而 OPTIONAL_HEADER 则有一些细微的差别,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
// 32位程序的NT_HEADERS
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // [0x00] PE标识 // 0x4550
IMAGE_FILE_HEADER FileHeader; // [0x04] 文件基本信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // [0x18] 文件扩展信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
// 64位程序的NT_HEADERS64
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature; // [0x00] PE标识 // 0x4550
IMAGE_FILE_HEADER FileHeader; // [0x04] 文件基本信息
IMAGE_OPTIONAL_HEADER64 OptionalHeader; // [0x18] 文件扩展信息
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

FILE_HEADER 结构体如下,其中 Machine 的值,在x86平台是 0x014C,在x64平台是 0x8664

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // [0x04] 运行平台
WORD NumberOfSections; // [0x06] 区段的数量
DWORD TimeDateStamp; // [0x08] 文件创建时间
DWORD PointerToSymbolTable; // [0x0C] 符号表指针
DWORD NumberOfSymbols; // [0x10] 符号的数量
WORD SizeOfOptionalHeader; // [0x14] 扩展头大小
WORD Characteristics; // [0x16] 文件属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

文件属性 Characteristics 的取值,在 winnt.h 中定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define IMAGE_FILE_RELOCS_STRIPPED         0x0001  // 文件不包含重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件是可执行的
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 文件不包含行号信息
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 文件不包含符号信息
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // 强制裁剪工作组
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 应用程序能够处理超过2GB的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // CPU的低字节是颠倒的
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 文件运行于32位平台
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // 不包含.dbg调试信息
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 从交换区中运行在移动介质中的文件
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 从交换区中运行在网络中的文件
#define IMAGE_FILE_SYSTEM 0x1000 // 系统文件
#define IMAGE_FILE_DLL 0x2000 // DLL(动态链接库)文件
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 只能运行在单处理器(Uniprocessor)中
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // CPU的低字节是颠倒的
扩展头(OPTIONAL_HEADER)

在x86平台下的 OPTIONAL_HEADER32 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef struct _IMAGE_OPTIONAL_HEADER32 {
// 标准域
WORD Magic; // [0x18] 标志位 // 32位(0x10B)
BYTE MajorLinkerVersion; // [0x1A] 链接器主版本号
BYTE MinorLinkerVersion; // [0x1B] 链接器子版本号
DWORD SizeOfCode; // [0x1C] 所有代码节的总大小
DWORD SizeOfInitializedData; // [0x20] 所有已初始化节总大小
DWORD SizeOfUninitializedData; // [0x24] 所有未初始化节总大小
DWORD AddressOfEntryPoint; // [0x28] 程序执行入口RVA
DWORD BaseOfCode; // [0x2C] 代码节起始RVA
DWORD BaseOfData; // [0x30] 数据节起始RVA
// NT附加域
DWORD ImageBase; // [0x34] 程序默认载入基地址 // 4字节
DWORD SectionAlignment; // [0x38] 内存中的节对齐值
DWORD FileAlignment; // [0x3C] 文件中的节对齐值
WORD MajorOperatingSystemVersion; // [0x40] 系统主版本号
WORD MinorOperatingSystemVersion; // [0x42] 系统子版本号
WORD MajorImageVersion; // [0x44] 映像主版本号
WORD MinorImageVersion; // [0x46] 映像子版本号
WORD MajorSubsystemVersion; // [0x48] 子系统主版本号
WORD MinorSubsystemVersion; // [0x4A] 子系统子版本号
DWORD Win32VersionValue; // [0x4C] 保留,通常为0x00
DWORD SizeOfImage; // [0x50] 内存中映像总大小
DWORD SizeOfHeaders; // [0x54] 各个文件头的总大小
DWORD CheckSum; // [0x58] 影像文件校验和
WORD Subsystem; // [0x5C] 文件子系统
WORD DllCharacteristics; // [0x5E] DLL标志位
DWORD SizeOfStackReserve; // [0x60] 初始化预留栈大小 // 4字节
DWORD SizeOfStackCommit; // [0x64] 初始化提交栈大小 // 4字节
DWORD SizeOfHeapReserve; // [0x68] 初始化预留堆大小 // 4字节
DWORD SizeOfHeapCommit; // [0x6C] 初始化提交堆大小 // 4字节
DWORD LoaderFlags; // [0x70] 调试相关,默认0x00
DWORD NumberOfRvaAndSizes; // [0x74] 数据目录表的数量
IMAGE_DATA_DIRECTORY DataDirectory[0x10]; // [0x78] 数据目录表 // 通常为0x10个
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

在x64平台下的 OPTIONAL_HEADER64 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
typedef struct _IMAGE_OPTIONAL_HEADER64 {
// 标准域
WORD Magic; // [0x18] 标志位 // 64位(0x20B)
BYTE MajorLinkerVersion; // [0x1A] 链接器主版本号
BYTE MinorLinkerVersion; // [0x1B] 链接器子版本号
DWORD SizeOfCode; // [0x1C] 所有代码节的总大小
DWORD SizeOfInitializedData; // [0x20] 所有已初始化节总大小
DWORD SizeOfUninitializedData; // [0x24] 所有未初始化节总大小
DWORD AddressOfEntryPoint; // [0x28] 程序执行入口RVA
DWORD BaseOfCode; // [0x2C] 代码节起始RVA
DWORD BaseOfData; // [0x30] 数据节起始RVA
// NT附加域
ULONGLONG ImageBase; // [0x34] 程序默认载入基地址 // 8字节
DWORD SectionAlignment; // [0x3C] 内存中的节对齐值
DWORD FileAlignment; // [0x40] 文件中的节对齐值
WORD MajorOperatingSystemVersion; // [0x44] 系统主版本号
WORD MinorOperatingSystemVersion; // [0x46] 系统子版本号
WORD MajorImageVersion; // [0x48] 映像主版本号
WORD MinorImageVersion; // [0x4A] 映像子版本号
WORD MajorSubsystemVersion; // [0x4C] 子系统主版本号
WORD MinorSubsystemVersion; // [0x4E] 子系统子版本号
DWORD Win32VersionValue; // [0x50] 保留,通常为0x00
DWORD SizeOfImage; // [0x54] 内存中映像总大小
DWORD SizeOfHeaders; // [0x58] 各个文件头的总大小
DWORD CheckSum; // [0x5C] 影像文件校验和
WORD Subsystem; // [0x60] 文件子系统
WORD DllCharacteristics; // [0x62] DLL标志位
ULONGLONG SizeOfStackReserve; // [0x64] 初始化预留栈大小 // 8字节
ULONGLONG SizeOfStackCommit; // [0x6C] 初始化提交栈大小 // 8字节
ULONGLONG SizeOfHeapReserve; // [0x74] 初始化预留堆大小 // 8字节
ULONGLONG SizeOfHeapCommit; // [0x7C] 初始化提交堆大小 // 8字节
DWORD LoaderFlags; // [0x84] 调试相关,默认0x00
DWORD NumberOfRvaAndSizes; // [0x88] 数据目录表的数量
IMAGE_DATA_DIRECTORY DataDirectory[0x10]; // [0x8C] 数据目录表 // 通常为0x10个
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

其中关于 Subsystem 的定义如下,表示该程序期望在什么环境下运行

1
2
3
4
5
6
7
8
#define IMAGE_SUBSYSTEM_UNKNOWN        0 // Unknown subsystem
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem
数据目录表(DATA_DIRECTORY)

OPTIONAL_HEADER 中存储着 DATA_DIRECTORY 信息,结构体如下,

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 虚拟地址
DWORD Size; // 大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

数据目录表通常是 0x10 个,实际数量由 NumberOfRvaAndSizes 指定,内容如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
节头(SECTION_HEADER)

在数据目录表的后边,就是用来描述每个节信息的 SECTION_HEADER,结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[0x08]; // 节名称
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;

节的属性 Characteristics 比较常见的定义如下,更多信息见 winnt.h 的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define IMAGE_SCN_CNT_CODE               0x00000020  // 此节包含代码
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 此节包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 此节包含未初始化数据
#define IMAGE_SCN_LNK_INFO 0x00000200 // 此节包含注释或其他类型信息
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // 此节不会成为映像的一部分
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // 此节包含COM数据
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // 在此节的TLB项中重置异常控制位
#define IMAGE_SCN_GPREL 0x00008000 // 此节可以访问GP相关内容
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // 此节包含扩展重定位信息
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // 此节可以被丢弃(比如重定位.reloc节)
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // 此节不可以缓存
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // 此节不可以分页
#define IMAGE_SCN_MEM_SHARED 0x10000000 // 此节是共享的
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 此节是可执行的
#define IMAGE_SCN_MEM_READ 0x40000000 // 此节是可读的
#define IMAGE_SCN_MEM_WRITE 0x80000000 // 此节是可写的

虽然每个节的名称可以随意定义,但是也有一些约定俗成的名称,如下

名称 描述
.text 代码段,Borland C++编译器代码段为code
.data 可读写的数据段,存放全局变量或静态变量
.rdata 只读数据段,存放常量信息
.idata 导入数据段,存放导入表信息
.edata 导出数据段,存放导出表信息
.rsrc 资源段,存放图标、菜单等资源信息
.bss 未初始化数据段
.crt 存放用于支持C++运行时库(CRT)所添加的数据
.tls 存放用于支持通过_declspec(thread)声明的线程局部存储数据
.reloc 存放重定位信息
.sdata 存放可被全局指针定位的可读写数据
.srdata 存放可被全局指针定位的只读数据
.pdata 存放异常表,结构体为IMAGE_RUNTIME_FUNTCION_ENTRY
.debug$S 存放OBJ文件中Codeview格式符号
.debug$T 存放OBJ文件中Codeview格式类型符号
.debug$P 存放使用预编译头时的一些信息
.drectve 存放编译时的一些链接命令
.didat 存放延迟装入的数据

节数据对齐

节的对齐共有两种粒度,一种是在内存中对齐,由 SectionAlignment 参数决定,通常为 0x1000 值,
另一种是在文件中对齐,由 FileAlignment 参数决定,通常为 0x200 值。操作系统在加载映像时,
会依照内存对齐粒度的整数倍来分配空间。

关于内存地址与文件地址之间的转换,我们需要先理解两个概念

名称 描述
虚拟内存地址(Virtual Address,VA) 指PE文件被装入内存之后的地址
相对虚拟地址(Relative Virtual Address,RVA) 指相对于映像基址的内存地址偏移

计算方式为:虚拟内存地址(VA) = 映像基址(Image Base) + 相对虚拟地址(RVA)

以某代码段为例,虽然由于粒度的不同导致内存地址与文件地址有很大偏差,但是整个代码段的内容是不会变得,
所以某一段代码的地址相对于该代码段的起始地址是不变的,根据这一特点就可以进行地址换算。

关系式为:文件偏移地址 = 所在节的起始文件偏移 + (虚拟内存地址 - 映像基址 - 所在节的起始虚拟内存偏移)

节头中的 VirtualAddress 表示该节的 起始虚拟内存偏移PointerToRawData 表示该节的 起始文件偏移