PE文件格式总结

PE文件格式

0x1 PE

PE是Windows下使用的可执行文件格式,包括exe,dll等

总结性的PE文件格式思维图:

image-20211118153224646

0x2 PE格式

PE头:存储运行所需要的信息(内存加载,运行起始位置等)由DOS头+三个节区头组成,PE头和各节区的尾部有一个NULL填充

DOS头:IMAGE_DOS_HEADER结构体:40字节

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

e_magic:DOS签名(”MZ“,也就是”4D5A“)

e_lfanew:指示NT头(IMAGE_NT_HEADER)的偏移(000000E0)

DOS存根(stub):可选项,大小不固定

NT头:IMAGE_NT_HEADER(NT头):F8字节,有以下三个成员

  • Signature:50450000h(“PE”00)
  • File Header:IMAGE_FILE_HEADER,文件头
1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //计算机的体系结构类型,比如x86的值就是0x014c
WORD NumberOfSections;//节数->文件中存在的节区数量,大于0,数量要和实际节区一致,否则会运行错误
DWORD TimeDateStamp;//文件时间戳的低32位,编译器创建此文件的时间
DWORD PointerToSymbolTable;//符号表的偏移量,以字节为单位,如果不存在 COFF 符号表,则为零。
DWORD NumberOfSymbols;//符号表中的符号数。
WORD SizeOfOptionalHeader;//指出Optional Header结构体的长度
WORD Characteristics;//特征,比如文件是否可运行(0x0002),是否是dll文件(0x2000)等
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
  • Optional Header:IMAGE_OPTIONAL_HEADER,可选头
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
36
37
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //文件的状态,如果是32位结构体就是10B,64就是20B
BYTE MajorLinkerVersion;//链接器的主要版本号
BYTE MinorLinkerVersion;//链接器的次要版本号
DWORD SizeOfCode;//代码段的大小,以字节为单位
DWORD SizeOfInitializedData;//已初始化数据部分的大小(以字节为单位)
DWORD SizeOfUninitializedData;//未初始化数据部分的大小(以字节为单位)
DWORD AddressOfEntryPoint;//指向入口点函数的指针,代码执行的起始地址
DWORD BaseOfCode;//指向代码部分开头的指针
DWORD BaseOfData;//指向数据部分开头的指针
DWORD ImageBase;//文件的优先装入地址。文件加载到内存中时第一个字节的首选地址。该值是64K字节的倍数。DLL的默认值为0x10000000。应用程序的默认值为0x00400000。执行PE时,先创建进程,再载入内存,然后设置EIP寄存器的值为ImageBase+AddressOfEntryPoint
DWORD SectionAlignment;//加载到内存中的节的对齐方式,以字节为单位。指定了节区在内存文件中的最小单位
DWORD FileAlignment;//文件中节的原始数据的对齐方式,以字节为单位。指定了节区在磁盘文件中的最小单位
WORD MajorOperatingSystemVersion;//所需操作系统的主要版本号。
WORD MinorOperatingSystemVersion;//所需操作系统的次要版本号。
WORD MajorImageVersion;//图像的主要版本号。
WORD MinorImageVersion;//图像的次要版本号。
WORD MajorSubsystemVersion;//子系统的主要版本号。
WORD MinorSubsystemVersion;//子系统的次要版本号。
DWORD Win32VersionValue;//保留参数,必须为 0。
DWORD SizeOfImage;//PE image在虚拟内存中所占的空间大小
DWORD SizeOfHeaders;//整个PE头的大小
DWORD CheckSum;//文件校验和
WORD Subsystem;//运行此映像所需的子系统。区分系统驱动文件和普通的可执行文件
WORD DllCharacteristics;// DLL特性
DWORD SizeOfStackReserve;//为堆栈保留的字节数
DWORD SizeOfStackCommit;//为堆栈提交的字节数。
DWORD SizeOfHeapReserve;//为本地堆保留的字节数
DWORD SizeOfHeapCommit;//为本地堆提交的字节数。
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;//用来指定DataDirectory数组的个数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//DataDirectory[0]:EXPORT
//DataDirectory[1]:IMPORT
//DataDirectory[2]:RESOURCE
//DataDirectory[9]:TLS
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

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];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;//加载到内存中时节区的大小
} Misc;
DWORD VirtualAddress;//内存中节区的起始地址(RVA)
DWORD SizeOfRawData;//磁盘文件中节区的大小
DWORD PointerToRawData;//磁盘中节区的起始地址
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;//节区属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

PE文件加载到内存时,每个节区要完成内存地址和文件偏移间的映射:RVA->RAW

过程:

1、查找RVA所在的节区

2、计算文件偏移(RAW-PointerToRawData=RVA-VirtualAddress)

0x3 IAT

导入地址表:保存进程、内存、DLL结构等相关内容

1
2
3
4
5
6
7
typedef struct _IMAGE_IMPORT_DESCRIPTOR {//记录PE文件要导入哪些库文件
DWORD OriginalFirstThunk;//INT(包含导入函数信息的结构体指针数组)的地址(RVA)
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;//库名称字符串的地址
DWORD FirstThunk;//IAT的地址(RVA)
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

当程序导入DLL的时候,一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR结构体,多个结构体形成数组,最后以一个NULL结构体结束

1、PE装载器会读取IID(IMAGE_IMPORT_DESCRIPTOR)的Name成员,获取dll名称字符串

2、使用LoadLibrary加载相应的库

3、读取IID的OriginalFirstThunk成员,获取INT的地址,逐一读取INT数组的值,获取IMAGE_IMPORT_BY_NAME地址

4、使用IMAGE_IMPORT_BY_NAME的Hint或Name项,获取相应函数的起始地址(GetProcAddress)

5、读取FirstThunk成员,获取IAT地址,将上面获得的相应函数的起始地址输入到相应的IAT数组值

IMAGE_IMPORT_DESCRIPTOR不在PE头,在PE体里,IMAGE_OPTIONAL_HEADER头中DataDirectory[1].VirtualAddress的值就是IMAGE_IMPORT_DESCRIPTOR的起始地址

0x4 EAT

是一种核心机制,使不同的应用程序可以调用库文件中提供的函数

用来获取相应库中的导出函数的起始地址

PE文件中只有一个用来说明库EAT的IMAGE_EXPORT_DIRECTORY结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
IMAGE_EXPORT_DIRECTORY STRUCT【导出表,共40字节】{
DWORD Characteristics ;
DWORD TimeDateStamp ; //文件生成时间
WORD MajorVersion ;
WORD MinorVersion ;
DWORD Name ; //库文件名的地址
DWORD Base ; //基数,加上序数就是函数地址数组的索引值
DWORD NumberOfFunctions ; //导出函数的总数
DWORD NumberOfNames ; //以名称方式导出的函数的总数
DWORD AddressOfFunctions ; //导出函数地址数组
DWORD AddressOfNames ; //函数名称地址数组
DWORD AddressOfNameOrdinals ; //导出函数序号地址数组
};IMAGE_EXPORT_DIRECTORY ENDS

**GetProcAddress()**:从库中获取函数地址的API,引用EAT来获取指定API的地址

原理:

1、通过AddressOfNames成员得到函数名称数组

2、通过在函数名称数组中比较(strcmp)指定函数名称,得到指定函数名称的索引

3、通过AddressOfNameOrdinals得到函数序号数组

4、通过指定函数名称的索引获取指定函数的序号

5、通过AddressOfFunctions得到函数地址数组

6、通过指定函数的序号用作函数地址数组的索引,得到指定函数的起始地址