PE文件结构
PE文件结构
PE(Portable Executable)是Windows操作系统下的可执行文件格式
基本结构
PE使用的是一个平面地址空间。
在文件中使用偏移(offset),而在程序中使用VA(Virtual Address)来表示位置。当文件加载到内存的时候,节区的大小和位置会发生变化。每个节区的大小都是“最小基本单位“的整数倍,剩余的空间用NULL来填充。这样可以加快计算机处理文件,内存,网络包的效率。
PE头
PE头是从DOS头到节区头的空间,有许多结构体组成。
DOS头
每个PE文件由DOS头开始
e_magic:DOS签名,0x4D5A => “MZ”
e_lfanew:NT头的偏移
NT头
NT头,IMAGE_NT_HEADERS
- DWORD Signature:50450000h(“PE”,00)
#define IMAGE_NT_SIGNATURE 0x00004550
IMAGE_FILE_HEADER FileHerder 映像文件头
该结构体包含了PE文件的一些基本信息,并指出了IMAGE_OPTIONAL_HEADER的大小
Machine:可执行文件的目标CPU类型
NumerPfSections:区块的数目
TimeDateStamp:文件创建时间
PointerToSymbolTable:COFF符号表的文件偏移
NumberOfSymbols:如果由COFF符号表,则显示其中的符号数目
SizeOfOptionHeader:表示数据的大小
Characteristics:文件属性
IMAGE_OPTIONAL_HEADER 可选映像头
32位
64位
Magic:标记文件类型
32位:10B 64位:20B
MajorLinkerVersion:
MinorLinkerVersion:
SizeOfCode:
SizeOfUninitializedData:未初始化数据块的大小,通常在.bss段中
AddressOfEntryPoint:程序入口RVA。
BaseOfCode:代码段的起始RVA。代码段一般在PE头之后,数据块之前,在Microsoft链接器生成的可执行文件中一般为0x1000
ImageBase:文件在内存中的首选载入地址。一般来说,EXE和DLL会被加载入用户内存0~7FFFFFFFh中,而SYS文件则会加载入80000000h~FFFFFFFFh的内核内存中
PEloder装载程序的时候,首先创建进程,将文件载入内存,然后将EIP寄存器设置为ImageBase+AddressOfEntryPoint
SectionAlignment:载入内存的区块对齐大小
FileAlignment:磁盘上PE文件内的区块对齐大小
MajorOperatingSystemVersion:
MinorOperartingSystemVersion:
MinorImageVersion:
MajorImageVersion:
MajorSubsystemVersion:
MinorSubsystemVersion:
Win32VersionValue:
SizeOfImage:加载PE文件时,指定PE Image在内存中所占空间的大小
SizeOfHeaders:指出整个PE头的大小。该值一定是FileAlignment的整数倍。
CheckSum:
Subsystem:区分系统驱动文件和普通可执行文件。
DllCharacteristics:DllMain()函数何时被调用
SizeOfStackReserve:在EXE文件里面为线程保留的站的大小
SizeOfStackCommit:在EXE文件里面一开始被委派给栈的内存,默认值是4KB
SizeOfHeapReserve:为进程的默认堆保留的内存,默认值是1MB
SizeOfHeapCommit:在EXE文件里面一开始被委派给堆的内存,默认值是4KB
LoaderFlags:与调试相关,默认值为0
NumberOfRvaAndSizes:数据目录的项数
DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构体组成。指向输出表,输入表,资源块等数据。
PE文件依赖该结构体来定位输出表,输入表和资源等重要数据
区块
在PE文件头和原始数据之间存在区块表(Section Table)
区块表
区块表紧跟在NT头之后,是一个由IMAGE_SECTION_HEADER组成的结构体数组。每个结构体数组包含了其所关联的区块信息
Name:块名。一般为8位的ASCII名。
VirtualSize:指出实际被使用的区块大小。
VirtualAddress: 该块装载到内存中的RVA地址。
SizeOfRawData:该块在磁盘中所占的空间。这里的数值计算了被FileAlignment调整的大小,因此为磁盘最小分块的整数倍
PointToRawData:该块在磁盘文件中的偏移。
PointToRelocations:在EXE文件中无意义。在OBJ文件中表示本块重定位信息的偏移量
PointToLinenumbers:
Characteristics:块属性。
常见区块
PE文件一般至少两个区块,代码块与数据块。
以下是常见区块
输入表
可执行文件使用来自其他DLL的代码或者数据的行为被成为输入。当PE文件被载入的时候,通过Import Table来定位被输入的函数和数据的地址。
输入函数的调用
一般来说,被导入的函数在程序中只会保留相关的信息,例如函数名和DLL名,而不会直接储存相关的代码。
因此在PE文件里面,利用INT(Import Name Table)来记录程序索要调用东方输入函数的名字,利用IAT(Import Address Table)来记录程序所要调用的输入函数的地址。
输入表的结构
NT头的数据目录第二个成员指向输入表。每个输入表由IID数组开始
- OriginalFirstThunk(CHaracteristics):包含指向输入名称表(INT)的RVA。
- TimeDataStamp:32位时间戳
- ForwarderChain:
- Name:DLL名字的指针,包含输入的DLL名
- FirstThunk:包含指向输入地址表(IAT)的RVA。
IAT的装载顺序:
读取IID的Name成员,获取库名字字符串
LoadLibrary(“”)
读取IID的OriginalFirstThunk成员,获取INT地址。
逐一读取INT中的值,获取对应IMAGE_TMPORT_BY_NAME的RVA
使用Hint值或者Name获取对应函数的起始地址。
读取IID的FirstThunk(IAT)成员,获得IAT地址
将获得的函数的地址输入相应的IAT数组值
重复4-7直到INT读取完毕
输出表
DLL为了能使得别的程序和DLL得以调用其中的函数,将输出信息保存在输出表中
输出表的结构
输出表的主要内容是一个包含函数名称,输出序数等的一个表格。
输出表是数据目录的第一个成员,指向IED结构体
- Characteristics:
- TimeDateStamp:
- MajorVersion:
- MinorVersion:
- Name:指向ASCII字符串的RVA。该字符串是与输出函数相关联的DLL的名字
- Base:
- NumberOfFunctions:EAT表中的条目数量。为零代表美术代码或者数据被输出
- NumberOfNames:输出函数名称表ENT中的条目数量
- AddressOfFuncticons:EAT的RVA。EAT是一个RVA数组,数组中的每一个非零的RVA都对应一个被输出的符号。
- AddressOfNames:ENT的RVA。ENT是一个指向ASCII字符串的RVA数组。
- AddressOfNameOrdinals:输出序数表的RVA。这个表将ENT中的数组索引映射到相应的输出地址条目
基址重定位
一般来说,当链接器生成一个PE文件的时候,会假设这个文件被装载到默认的基地地址处。但当PE文件加载的时候,如果该地址已被占用,则PE文件需要用重定位表来进行调整。
对于EXE文件来说,每个文件总是使用独立的虚拟地址空间,所以EXE文件一般不需要重定位信息。然而Windows系统自带的ASLR随机基址的安全机制使得EXE文件每次加载的地址不尽相同。
基址重定位表
基址重定位表(Base Relocation Table)位于.reloc区块内。
以一个IMAGE_BASE_RELOCATION结构体开始
- VirtualAddress:这组重定位数据的开始RVA地址。
- SizeOfBlock:当前重定位结构的大小。
- TypeOffset:数组。每项大小2字节,16位。高4位代表重定位类型,低12位代表重定位地址。该地址+VA就是指向PE映像文件中需要求改的地址数据的指针。
资源
Windows程序的各种界面被称为资源。
资源结构
资源采用类似磁盘目录结构的方式保存。
调试目录
数据目录表的第七个条目指向调试目录。目前,最为常见的储存debug信息的形式是PDB文件。