PE 的意思就是 Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行体文件格式。它的一些特性继承自 Unix的 Coff (common object file format)文件格式。"portable executable"(可移植的执行体)意味着此文件格式是跨win32平台的 : 即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。所有 win32执行体 (除了VxD和16位的Dll)都使用PE文件格式,所以熟知PE结构有助于对操作系统的深刻理解。
图1 PE文件结构框架
上图是 PE文件结构的总体层次分布。所有的PE文件都要以一个简单的DOS MZ header开始。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ header 之后的 DOS stub。DOS stub实际上是个有效的 EXE,在不支持 PE文件格式的操作系统中,它将简单显示一个错误提示。
DOS stub后面就是PE header, PE header 是PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header 中找到 PE header 的起始偏移量。因而跳过了 DOS stub 直接定位到真正的文件头 PE header。
PE header 接下来的数组结构 section table(节表)。 每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。节的划分是基于各组数据的共同属性: 而不是逻辑概念。有了节表,就能定位节。
对PE的物理结构有了大致了解之后,再大致了解一下装载PE的文件的步骤:
- 当PE文件被执行,PE装载器检查 DOS MZ header 里的 PE header 偏移量。如果找到,则跳转到 PE header。
- PE装载器检查 PE header 的有效性。如果有效,就跳转到PE header的尾部。
- 紧跟 PE header 的是节表。PE装载器读取其中的节信息,并采用文件映射方法将这些节映射到内存,同时付上节表里指定的节属性。
- PE文件映射入内存后,PE装载器将处理PE文件中类似 import table(引入表)逻辑部分。
1 PE的基本概念
DOS MZ header 又命名为 IMAGE_DOS_HEADER.。其中只有两个域比较重要: e_magic 包含字符串"MZ",e_lfanew 包含PE header在文件中的偏移量。
图2 DOS MZ header结构
1.2 DOS 存根
接下来的DOS stub实际上是个EXE,当当前系统不支持PE文件结构时它能输出一个错误提示“This program requires Windows”
1.3 PE header
PE头是一个IMAGE_NT_HEADERS类型的结构,下面是这个结构在WINNT.H中的定义:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature是一个标志变量,这个就是当我们判断一个文件是否是PE文件时第二步需要判断的,若这个值等于"PE\0\0"时就是一个PE文件。
第二个成员是一个IMAGE_FILE_HEADER类型的对象,通常我们叫它映像文件头,这个结构在头文件中的定义是:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
这里对比较重要的成员做出说明,NumberOfSections这个成员保存了节的数目,SizeOfOptionalHeader这个成员保存了PE头中OptionalHeader这个成员的大小,最后一个成员Characteristics是一个关于文件的标记,即这个文件是EXE还是DLL文件。
1.4 OptionalHeader
它是一个IMAGE_OPTIONAL_HEADER32类型的对象,这个结构的定义是:
术语--RVA 代表相对虚拟地址(Relative virtual address)。
可选头中比价重要的有:
AddressOfEntryPoint:
PE装载器准备运行的PE文件的第一个指令的RVA。若您要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。
ImageBase PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地
址区域已被其他模块占用,那PE装载器会选用其他空闲地址。
SectionAlignment:
内存中节对齐的粒度。例如,如果该值是4096 (1000h),那么每节的起始地址必须是4096的倍数。若第一节从401000h开始且大小是10个字节,
则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。
FileAlignment:
文件中节对齐的粒度。例如,如果该值是(200h),,那么每节的起始地址必须是512的倍数。若第一节从文件偏移量200h开始且大小是10个字节,
则下一节必定位于偏移量400h: 即使偏移量512和1024之间还有很多空间没被使用/定义。
MajorSubsystemVersion
MinorSubsystemVersion :
win32子系统版本。若PE文件是专门为Win32设计的,该子系统版本必定是4.0否则对话框不会有3维立体感。
SizeOfImage:
内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
SizeOfHeaders:
所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。可以以此值作为PE文件第一节的文件偏移量。
Subsystem:
NT用来识别PE文件属于哪个子系统。 对于大多数Win32程序,只有两类值: Windows GUI 和 Windows CUI (控制台)。
DataDirectory:
一IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等。非常重要!!
1.5 section table
节表就相当于书的目录,而书中的各个章节就相当于PE文件结构中的节,通过目录我们能很快找到书中我们感兴趣的内容,同样
通过节表我们很快能找到PE文件中的各个节。(注意:多个数据只要是具有共同属性我们就能把它放在同一节)
Name: 这个成员是一个字节型的数组,这就是节的名字,不过这个数组的上限是8,最多只能保存8个字符,还有就是它不是一个ASCIIZ字符串,因为它不是以null结尾的。VirtualAddress:
本节的RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h。
PointerToRawData:
这是节基于文件的偏移量,PE装载器通过本域值找到节数据在文件中的位置。
Characteristics:
包含标记以指示节属性,比如节是否含有可执行代码、初始化数据、未初始数据,是否可写、可读等。
1.6 输入表
可执行文件使用来自于其他dll的代码或数据时,称为输入。当PE文件装入时,Windows加载器的工作之一就是定位所有被输入的函数和数据,并且让正在被装入的
PE文件可以使用那些地址。这个过程是通过PE文件的输入表(Import Tab 也称之为导入表)完成的,输入表中保存的是函数名和其驻留的dll名等,动态连接所需输信息,
输入表在软件外壳技术上的地位十分重要,因此在研究外壳的技术时一定要掌握这部分知识。
IMAGE_NT_HEADER->IMAGE_OPTIONAL_HEADER32->IMAGE_DATA_DIRECTORY的第二个成员指向输入表,输入表以一个IMAGE_IMPORT_DESCRIPTOR
(简称IDD)开始,(IID) IMAGE_IMPORT_DESCRIPTOR的结构包含如下5个字段:
OriginalFirstThunk, TimeDateStamp, ForwarderChain, Name, FirstThunk
OriginalFirstThunk (INT)
该字段是指向一32位以00结束的RVA偏移地址串,此地址串中每个地址描述一个输入函数,它在输入表中的顺序是不变的。
TimeDateStamp
一个32位的时间标志,有特殊的用处。
ForwarderChain
输入函数列表的32位索引。
Name
DLL文件名(一个以00结束的ASCII字符串)的32位RVA地址。
FirstThunk (IAT)
该字段是指向一32位以00结束的RVA偏移地址串,此地址串中每个地址描述一个输入函数,它在输入表中的顺序是可变的。
1.7 输出表
当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLLs中的真实函数地址来修正主程
序。PE装载器搜寻的是DLLs中的引出函数。
输出表结构如下:
输出表的设计是为了方便PE装载器工作。首先,模块必须保存所有输出函数的地址以供PE装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中,而数组元素数目存放在NumberOfFunctions域中。 因此,如果模块引出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。PE装载器在名字数组中找到匹配名字的同时,它也获取了 指向地址表中对应元素的索引。 而这些索引保存在由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同。
1.8 基址重定位
在32位代码中,涉及到直接寻址的指令都是需要重定位的。对操作系统来说,其任务就是在对可执行程序透明的情况下完成重定位操作。
在现实中,重定位信息是在编译的时候由编译器生成并保留在可执行文件中的,在程序被执行前由操作系统根据重定位表信息修正代码,这样在开发程序的时候就不用考虑重定位问题了。重定位信息在PE文件中被存放在重定位表中。在PE文件没有被加载到预期的imagebase位置时,PE加载器根据下面的重定位表来确定哪些指令数据是需要修改的。
重点就是重定位表的结构和使用方法了。
指令(数据)的指针 = (数据项低12位+VirtualAddress)+ PE文件实际被映射的基址
重定位结果 = 需要重定位数据+( PE文件实际被映射的基址-ImageBase)
1.9 资源
资源一般使用树来保存,通常包含3层,在NT下,最高层是类型,然后是名字,最后是语言。
一个PE文件是否包含资源文件,通常检测块表(Section Table)中是否含有'.rsrc',不过这个方法对有些PE文件无效。
1.10 TLC
线程本地存储TLS(Thread Local Storage)。TLS的作用是能将数据和执行的特定的线程联系起来。实现TLS有两种方法:静态TLS和动态TLS。
第一次写博客,排版什么都比较随意,发现把学过的东西记录下来确实学起来效率高一点。
由于我也是第一次真正接触PE,所以有些概念理解的不是很准确,虽然现在水平还不是很高,但尽力了就行了,希望大家多多包含,有时间大家可以看看原版。
最后附上一张简略的PE结构图:
-------------*-------------------------------------------------*
| DOS Header(IMAGE_DOS_HEADER) | -->64 Byte
DOS头部 --------------------------------------------------
| DOS Stub | -->112 Byte
-------------*-------------------------------------------------*
| "PE"00 (Signature) | -->4 Byte
-------------------------------------------------
| IMAGE_FILE_HEADER | -->20 Byte
PE文件头 --------------------------------------------------
| IMAGE_OPTIONAL_HEADER32 | -->96 Byte
---------------------------------------------------
| 数据目录表 | -->128 Byte
-------------*--------------------------------------------------*
| IMAGE_SECTION_HEADER | -->40 Byte
---------------------------------------------------
块表 | IMAGE_SECTION_HEADER | -->40 Byte
--------------------------------------------------
| IMAGE_SECTION_HEADER | -->40 Byte
-------------*--------------------------------------------------*
|.text | -->512 Byte
---------------------------------------------------
块 |.rdata | -->512 Byte
---------------------------------------------------
|.data | -->512 Byte
-------------*-------------------------------------------------*
| COFF行号 | -->NULL
---------------------------------------------------
调试信息 | COFF符号表 | -->NULL
---------------------------------------------------
| Code View 调试信息 | -->NULL
-------------*--------------------------------------------------*
--------->>>摘自互联网
参考书籍:《加密与解密》、《逆向工程核心原理》