PE文件结构

PE文件结构

PE(Portable Executable)是Windows操作系统下的可执行文件格式

基本结构

PE使用的是一个平面地址空间。

在文件中使用偏移(offset),而在程序中使用VA(Virtual Address)来表示位置。当文件加载到内存的时候,节区的大小和位置会发生变化。每个节区的大小都是“最小基本单位“的整数倍,剩余的空间用NULL来填充。这样可以加快计算机处理文件,内存,网络包的效率。

PE头

PE头是从DOS头到节区头的空间,有许多结构体组成。

DOS头

每个PE文件由DOS头开始

image-20230315230112225

e_magic:DOS签名,0x4D5A => “MZ”

e_lfanew:NT头的偏移

NT头

NT头,IMAGE_NT_HEADERS

image-20230315230521226

  • DWORD Signature:50450000h(“PE”,00)

​ #define IMAGE_NT_SIGNATURE 0x00004550

  • IMAGE_FILE_HEADER FileHerder 映像文件头

    该结构体包含了PE文件的一些基本信息,并指出了IMAGE_OPTIONAL_HEADER的大小

    image-20230315230919579

    • Machine:可执行文件的目标CPU类型

      image-20230315231007409

    • NumerPfSections:区块的数目

    • TimeDateStamp:文件创建时间

    • PointerToSymbolTable:COFF符号表的文件偏移

    • NumberOfSymbols:如果由COFF符号表,则显示其中的符号数目

    • SizeOfOptionHeader:表示数据的大小

    • Characteristics:文件属性

      image-20230315231520779

  • IMAGE_OPTIONAL_HEADER 可选映像头

    32位

    image-20230315231721704

    image-20230315231730212

    64位

    image-20230315231806571

    image-20230315231821107

    • 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:区分系统驱动文件和普通可执行文件。

      image-20230315234227531

    • DllCharacteristics:DllMain()函数何时被调用

    • SizeOfStackReserve:在EXE文件里面为线程保留的站的大小

    • SizeOfStackCommit:在EXE文件里面一开始被委派给栈的内存,默认值是4KB

    • SizeOfHeapReserve:为进程的默认堆保留的内存,默认值是1MB

    • SizeOfHeapCommit:在EXE文件里面一开始被委派给堆的内存,默认值是4KB

    • LoaderFlags:与调试相关,默认值为0

    • NumberOfRvaAndSizes:数据目录的项数

    • DataDirectory[16]:数据目录表,由数个相同的IMAGE_DATA_DIRECTORY结构体组成。指向输出表,输入表,资源块等数据。

      image-20230316160303433

      image-20230316160328679

      ​ PE文件依赖该结构体来定位输出表,输入表和资源等重要数据

区块

在PE文件头和原始数据之间存在区块表(Section Table)

区块表

区块表紧跟在NT头之后,是一个由IMAGE_SECTION_HEADER组成的结构体数组。每个结构体数组包含了其所关联的区块信息

image-20230317163531391

  • Name:块名。一般为8位的ASCII名。

  • VirtualSize:指出实际被使用的区块大小。

  • VirtualAddress: 该块装载到内存中的RVA地址。

  • SizeOfRawData:该块在磁盘中所占的空间。这里的数值计算了被FileAlignment调整的大小,因此为磁盘最小分块的整数倍

  • PointToRawData:该块在磁盘文件中的偏移。

  • PointToRelocations:在EXE文件中无意义。在OBJ文件中表示本块重定位信息的偏移量

  • PointToLinenumbers:

  • Characteristics:块属性。

    image-20230318222714596

常见区块

PE文件一般至少两个区块,代码块与数据块。

以下是常见区块

image-20230318223239272

image-20230318223259236

输入表

可执行文件使用来自其他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的装载顺序:

  1. 读取IID的Name成员,获取库名字字符串

  2. LoadLibrary(“”)

  3. 读取IID的OriginalFirstThunk成员,获取INT地址。

  4. 逐一读取INT中的值,获取对应IMAGE_TMPORT_BY_NAME的RVA

    image-20230321214504809

  5. 使用Hint值或者Name获取对应函数的起始地址。

  6. 读取IID的FirstThunk(IAT)成员,获得IAT地址

  7. 将获得的函数的地址输入相应的IAT数组值

  8. 重复4-7直到INT读取完毕

输出表

DLL为了能使得别的程序和DLL得以调用其中的函数,将输出信息保存在输出表中

输出表的结构

输出表的主要内容是一个包含函数名称,输出序数等的一个表格。

输出表是数据目录的第一个成员,指向IED结构体

image-20230321212317492

  • 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结构体开始

image-20230321220703553

  • VirtualAddress:这组重定位数据的开始RVA地址。
  • SizeOfBlock:当前重定位结构的大小。
  • TypeOffset:数组。每项大小2字节,16位。高4位代表重定位类型,低12位代表重定位地址。该地址+VA就是指向PE映像文件中需要求改的地址数据的指针。

资源

Windows程序的各种界面被称为资源。

资源结构

资源采用类似磁盘目录结构的方式保存。

调试目录

数据目录表的第七个条目指向调试目录。目前,最为常见的储存debug信息的形式是PDB文件。