PE文件格式

0x00 简介

Portable Executable(可移植的执行体)是目前Windows平台上主流的可执行文件格式,exe、dll、sys等都是PE文件。
PE文件是指32位的可执行文件,也称PE32。64位的可执行文件称为PE+或PE32+,是PE32的一种扩展.

0x01 相关概念

  • 虚拟内存地址(Virtual Address, VA)PE文件中的指令被装入内存后的地址
  • 相对虚拟内存地址(Reverse Virtual Address, RVA相对虚拟地址是内存地址相对于映射基址的偏移量。相对于PE文件装入地址的偏移位置,它是一个“相对地址”。例如,假设一个PE文件从地址400000h处装入,并且它的代码节开始于401000h,代码节的RVA将是:目标地址401000h - 装入地址400000h = RVA 1000h。)
  • 装载基址(Image Base):PE文件装入内存的 基地址。默认情况下,EXE文件的基址为0x00400000,DLL文件的基址为0x10000000。
  • 文件偏移地址(File Offset Address, FOA)数据在PE文件中的地址叫文件偏移地址,这是文件在磁盘上存放时相对于文件开头的偏移。
  • 内存中数据节相对于装载基址的偏移量和文件中数据节的偏移量的差异称为节偏移。
1
虚拟内存地址(VA) = 基地址(Image Base) + 相对虚拟地址(RVA)。
1
2
文件偏移地址 = 虚拟内存地址(VA) - 装载基址(Image Base) - 节偏移 
= RVA - 节偏移

PE1.png


下面简单介绍PE文件的构成:

0x02 DOS头

PE文件的第一个字节起始于MS-DOS头部,称作 IMAGE_DOS_HEADER。微软为了考虑DOS兼容性的问题,在PE 头的最前边添加了一个 IMAGE_DOS_HEADER 结构体,用来扩展已有的DOS EXE。Dos结构体的大小为40个字节。结构如下:

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 {      // DOS .EXE header
WORD e_magic; // +0000h - EXE标志,“MZ”
WORD e_cblp; // +0002h - 最后(部分)页中的字节数
WORD e_cp; // +0004h - 文件中的全部和部分页数
WORD e_crlc; // +0006h - 重定位表中的指针数
WORD e_cparhdr; // +0008h - 头部尺寸,以段落为单位
WORD e_minalloc; // +000ah - 所需的最小附加段
WORD e_maxalloc; // +000ch - 所需的最大附加段
WORD e_ss; // +000eh - 初始的SS值(相对偏移量)
WORD e_sp; // +0010h - 初始的SP值
WORD e_csum; // +0012h - 补码校验值
WORD e_ip; // +0014h - 初始的IP值
WORD e_cs; // +0016h - 初始的CS值
WORD e_lfarlc; // +0018h - 重定位表的字节偏移量
WORD e_ovno; // +001ah - 覆盖号
WORD e_res[4]; // +001ch - 保留字00
WORD e_oemid; // +0024h - OEM标识符
WORD e_oeminfo; // +0026h - OEM信息
WORD e_res2[10]; // +0028h - 保留字
LONG e_lfanew; // +003ch - PE头相对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这一部分主要是当初为兼容DOS设计,主要看最后一个字段,指示PE头的位置。

DOS存根

DOS头的下面是DOS存根。DOS存根(stub)为可选,在dos下会运行并结束程序,大小也任意。大小为NT头的偏移量减去DOS头的40个字节。整个DOS Stub是一个字节块,其内容随着链接时使用的链接器不同而不同,PE中并没有与之对应的相关结构。

0x03 PE头

DOS stub后面的是PE头标识Signature。与大部分文件格式的头部结构一样,PE头部信息中有一个四字节的标识,其内容固定,对应的ASCII码的字符串是“PE00”。

PE头IMAGE_NT_HEADERS

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // +0000h - PE文件标识,“PE00”
IMAGE_FILE_HEADER FileHeader; // +0004h - PE标准头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // +0018h - PE扩展头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

标准PE头IMAGE_FILE_HEADER

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // +0004h - 运行平台
WORD NumberOfSections; // +0006h - PE中节的数量
DWORD TimeDateStamp; // +0008h - 文件创建日期和时间
DWORD PointerToSymbolTable; // +000ch - 指向符号表
DWORD NumberOfSymbols; // +0010h - 符号表中的符号数量
WORD SizeOfOptionalHeader; // +0014h - 扩展头结构的长度
WORD Characteristics; // +0016h - PE的属性

}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PE属性可为下面的值:

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 0x004//不包含行号
#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//字节序颠倒的
#define IMAGE FILE 32BIT MACHINE 0x0100//运行于32位平台
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200//不包含调试
#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//只能在单处理器上运行
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000//字节时颠倒的

扩展PE头IMAGE_OPTIONAL_HEADER

对该结构中的某些数值的随意改动可能会造成PE文件的加载或运行失败。

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
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // +0018h - 魔术字107h = ROM Image,10bh = exe Image
BYTE MajorLinkerVersion; // +001ah - 链接器版本号
BYTE MinorLinkerVersion; // +001bh -
DWORD SizeOfCode; // +001ch - 所有含代码的节的总大小
DWORD SizeOfInitializedData; // +0020h - 所有含已初始化数据的节的总大小
DWORD SizeOfUninitializedData; // +0024h - 所有含未初始化数据的节的大小
DWORD AddressOfEntryPoint; // +0028h - 程序执行入口RVA
DWORD BaseOfCode; // +002ch - 代码的节的起始RVA
DWORD BaseOfData; // +0030h - 数据的节的起始RVA
DWORD ImageBase; // +0034h - 程序的建议装载地址
DWORD SectionAlignment; // +0038h - 内存中的节的对齐粒度
DWORD FileAlignment; // +003ch - 文件中的节的对齐粒度
WORD MajorOperatingSystemVersion; // +0040h - 操作系统版本号
WORD MinorOperatingSystemVersion; // +0042h -
WORD MajorImageVersion; // +0044h - 该PE的版本号
WORD MinorImageVersion; // +0046h -
WORD MajorSubsystemVersion; // +0048h - 所需子系统的版本号
WORD MinorSubsystemVersion; // +004ah -
DWORD Win32VersionValue; // +004ch - 未用
DWORD SizeOfImage; // +0050h - 内存中的整个PE映象尺寸
DWORD SizeOfHeaders; // +0054h - 所有头+节表的大小
DWORD CheckSum; // +0058h - 校验和
WORD Subsystem; // +005ch - 文件的子系统
WORD DllCharacteristics; // +005eh - DLL文件特性
DWORD SizeOfStackReserve; // +0060h - 初始化时的栈大小
DWORD SizeOfStackCommit; // +0064h - 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // +0068h - 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // +006ch - 初始化时实际提交的堆大小
DWORD LoaderFlags; // +0070h - 与调试有关
DWORD NumberOfRvaAndSizes; // +0074h - 下面的数据目录结构的项目数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 0078h - 数据目录
}IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;

0x04 目录表IMAGE_DATA_DIRECTORY

定义了PE文件中出现的所有不同类型的数据的目录信息,从Windows NT 3.1操作系统开始到现在,该数据目录中定义的数据类型一直是16种。它是一个数组,有16个成员

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // +0000h - 数据的起始RVA
DWORD Size; // +0004h - 数据块的长度,最后一个是000000表示结束
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

数组编号描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0导出表地址和大小
1导入表地址和大小
2资源表地址和大小
3异常表地址和大小
4属性证书数据地址和大小
5基地址重定位表地址和大小
6调试信息地址和大小
7预留为0
8指向全局指针寄存器的值
9线程局部存储地址和大小
10加载配置表地址和大小
11绑定导入表地址和大小
12导入函数地址表地址和大小
13延迟导入表地址和大小
14CLR运行时头部数据地址和大小
15系统保留

导入表

导入表是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。导入表数据的起始部分是多组导入表描述符结构。每个结构包含PE文件引入函数的一个相关DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。
导入表描述符IMAGE_IMPORT_DESCRIPTOR:

1
2
3
4
5
6
7
8
9
10
IMAGE_IMPORT_DESCRIPTOR {  
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //INT(导入名字表)的地址(RVA)桥1
};
DWORD TimeDateStamp; //时间戳
DWORD ForwarderChain; //链表的前一个结构
DWORD Name; //指向链接库的指针
DWORD FirstThunk; //(IAT)导入地址表的地址 (RVA)桥2
} IMAGE_IMPORT_DESCRIPTOR;
1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // 导入表导入函数的实际内存地址
DWORD Ordinal; // 导入表导入函数的导出序号
DWORD AddressOfData; // 指向IMAGE_IMPORT_BY_NAME结构
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

导入表的双桥结构( 每一个结构IMAGE_IMPORT_DESCRIPTOR都对应的是一个唯一的dll文件。以及dll中的每个函数都可以通过”编号-名称”的方式找到,这就是导入表的双桥结构。):

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint; //函数序号
CHAR Name[1]; //函数名字符串
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

导出表

_IMAGE_DATA_DIRECTORY结构体数组的第1个元素索引处导出表。一般情况下,dll的函数导出供其他人使用,exe将别人的dll的函数导入运行。 所以,一般.exe没有导出表

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //未使用,恒为0x00000000
DWORD TimeDateStamp; //时间戳
WORD MajorVersion; //未使用
WORD MinorVersion; //未使用
DWORD Name; //指向改导出表文件名字符串,即库名称
DWORD Base; //导出表的起始序号
DWORD NumberOfFunctions; //导出函数的个数(更准确来说是AddressOfFunctions的元素数,而不是函数个数)
DWORD NumberOfNames; //以函数名字导出的函数个数
DWORD AddressOfFunctions; //导出函数地址表RVA:存储所有导出函数地址(表元素宽度为4,总大小NumberOfFunctions * 4)
DWORD AddressOfNames; //导出函数名称表RVA:存储函数名字符串所在的地址(表元素宽度为4,总大小为NumberOfNames * 4)
DWORD AddressOfNameOrdinals; //导出函数序号表RVA:存储函数序号(表元素宽度为2,总大小为NumberOfNames * 2)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

0x05 节表项IMAGE_SECTION_HEADER

节表:PE代码和数据的结构数据,指示装载系统代码段在哪里,数据段在哪里等。节表是一个由IMAGE_SECTION_HEADER结构组成的数组,每个结构用来描述一个节

节表类似于目录表,是一种结构体的数组,结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // +0000h - 8个字节节名,如.bss
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; // +0008h - 节区的尺寸
DWORD VirtualAddress; // +000ch - 节区的RVA地址
DWORD SizeOfRawData; // +0010h - 在文件中对齐后的尺寸
DWORD PointerToRawData; // +0014h - 在文件中的偏移
DWORD PointerToRelocations; // +0018h - 在OBJ文件中使用
DWORD PointerToLinenumbers; // +001ch - 行号表的位置(供调试用)
WORD NumberOfRelocations; // +0020h - 在OBJ文件中使用
WORD NumberOfLinenumbers; // +0022h - 行号表中行号的数量
DWORD Characteristics; // +0024h - 节的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

节表后面就是节的内容。

0x06 节数据

  • text:代码段,是在编译或汇编结束时产生的一种块,它的内容全部是指令代码。也有的编译器将该段命名为.code
  • .data:初始化的数据块,是初始化的数据块,包含那些编译时被初始化的变量、字符串
  • .idata:输入表,包含其他外来dll的函数和数据信息,也就是输入表,也有人称之为导入表。
  • .rsrc:资源数据块,包含模块的全部资源数据,如图标、菜单、位图等。
  • .reloc:重定位表,用于保存基址的重定位表。即当装在程序不能按照连接器所指定的地址装载文件是,需要对指令或已经初始化的变量进行调整,该块中也包含了调整过程中所需要的一些数据,如果装载能够正常装在则忽略此段中的数据。
  • .edata:导出表,是pe文件的输出表,以供其他模块使用,并不是每个pe文件都有此数据段,因为有的文件并不需要输出一些函数,该数据段常见于动态连接库文件中。
  • .radata:存放调试目录、说明字符串,该数据块并不常见主要是用于存放一些调试信息
-------------本文结束感谢您的阅读-------------
/*
*/