第12章 PE文件格式

1. 介绍

PE文件是Window操作系统下使用的可执行文件格式.PE文件是指32位的可执行文件,也称为PE32.64位的可执行文件称为PE+或PE32+,是PE(pe32)文件的一种扩展形式.

2. PE文件格式

种类 主扩展名
可执行系列 EXE / SCR
库系列 DLL / OCX / CPL / DRV
驱动程序系列 SYS / VXD
对象文件序列 OBJ

OBJ(对象)文件之外的所有文件都是可执行文件,DLL / SYS 文件等虽然不能直接在Shell(Explorer.exe)中运行,但可以使用其它方法执行.

下面以打开notepad.exe为例,notepad.exe文件运行需要的所有信息都存储在PE头中,如果加载到内存,从何处开始运行,运行中需要什么DLL,有哪些堆栈内存等,大量信息以结构体形式存储在PE头中, 学习PE文件格式就是学习PE头中的结构体.
notepad.exe文件

2.1 基本结构

从 DOS 头(Dos header)到节区头(Section header)是PE头部分,其下的节区合称PE体.文件中使用偏移(offset),内存中使用VA(Virtual Address ,虚拟地址)来表示位置.文件加载到内存时,情况就会发生变化(节区的大小,位置等).文件的内容一般可分为代码(.text) / 数据(.data) / 资源(.rsrc)节,分别保存.

各节区头定义了各节区在文件或内存中的大小,位置,属性等.

PE头与各节区的尾部存在一个区域,称为NULL填充(NULL padding).在计算机中,为了提高处理文件,内存,网络包的效率.文件 / 内存中各节区的起始位置应该在各个文件 / 内存 最小单位的倍数位置上,空白区域将用NULL填充.

notepad.exe加载到内存中的情形

2.2 VA & RVA

VA指的是进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)指从某个基准位置(ImageBase)开始的相对地址.VA 与 RVA 满足下面的换算关系.
RVA + ImageBase = VA
PE头内部信息大多以RVA形式存在.原因在于,PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件(DLL).此时必须通过重定位(Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问,因此使用RVA来定位信息,即是发生了重定位,只要相对于基准位置的相对地址没有变化,就能正常访问到指定信息,不会出现任何问题.

3. PE头

3.1 DOS 头

在PE头的最前面添加了一个IMAGE_DOS_HEADER结构体,用来扩展已经有的DOS EXE头.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAE_DOS_HEADER { //DOS .EXE header 位置
WORD e_magic; //Magic number; 0x00
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; //File address of new exe header 0x3C
} IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;

IMAGE_DOS_HEADER 结构体的大小为40个字节,从该结构体必须知道两个重要成员,第一个字段e_magic与最后一个字段e_lfanew.

e_magic : DOS签名(signature, 4D5A=>ASCII值”MZ”)
e_lfanew : 指示NT头的偏移(根据不同文件拥有可变值).

所有PE文件在开始部分(e_magic)都有DOS签名(“MZ”).e_lfanew值指向NT头所在位置(NT头的名称在IMAGE_DOS_HEADERS).

使用 Hex Editor 打开 notepad.exe ,查看IMAGE_DOS_HEADERS结构体.

IMAGE_DOS_HEADERS

根据 PE 规范,文件开头的2个字节为4D5A,e_lfanew值为000000E0,假若修改了这些值,可以发现程序无法正常运行,因为根据PE的规范它已经不是PE文件了.

3.2 DOS 存根

DOS存根(stub)在DOS头下方,是个可选项,且大小不固定(即使没有DOS存根,文件也能正常运行).DOS存根由代码与数据混合而成.

DOS存根

文件偏移40~4D区域为16位的汇编指令.

打开命令窗口,输入debug c:\windows\notepad.exe

3.3 NT 头

1
2
3
4
5
6
typedef struct _IMAGE_DOS_HEADER
{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_ OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER32;

IMAGE_DOS_HEADERS结构体由三个成员组成, 第一个成员的签名为(Signature)结构体,其值为50450000h(“PE”00).另外两个成员分别为文件头(File Header)与可选头(Optional Header)结构体.
IMAGE_DOS_HEADERS

3.4 NT头 : 文件头

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_DOS_HEADER
{
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER32;

IMAGE_DOS_HEADERS结构体中有以下四种重要成员(若设置不对,不能正常运行.)

  • Machine
    每个CPU都拥有唯一的Machine码,兼容32位 Intel x86 芯片的Machine码为14C.
  • NumberOfSections
    NumberOfSections用来指出文件中存在的节区数量.该值一定要大于0,且当定义的节区数量与实际节区不同时,将发生运行错误.

  • SizeOfOptionalHeader
    IMAGE_DOS_HEADERS结构体的最后一个成员为IMAGE_DOS_HEADER32结构体.SizeOfOptionalHeader成员用来指定出IMAGE_DOS_HEADER32结构体的长度.IMAGE_DOS_HEADER32结构体由C语言编写而成,故其大小已确认.但是Windows的PE装载器需要查看IMAGE_DOS_HEADER的SizeOfOptionalHeader值,从而识别出IMAGE_DOS_HEADER32结构体的大小.

  • Characteristics

该字段用来标识文件的属性,文件是否可以运行状态,是否为DLL文件等信息.以 bit OR 形式组合起来.

3.5 NT头 : 可选头

在IMAGE_DOS_HEADER32结构体中需要关注以下成员,这些值文件是必须运行的,设置错误将导致文件无法正常运行.

  • Magic
    为IMAGE_DOS_HEADER32结构体时,Magic码为10B;为IMAGE_DOS_HEADER64结构体时,Magic码为20B;

  • AddressOfEntryPoint
    AddressOfEntryPoint持有EP的RVA值,该值指出程序最先执行的代码起始地址,相当重要.

  • ImageBase
    进程虚拟内存的范围是 0~FFFFFFFF(32位系统).PE文件被加载到如此大的内存中时,ImageBase指出文件的优先载入地址.

EXE,DLL文件被装载到用户内存的0~7FFFFFFF中,SYS文件被载入内核内存的80000000~FFFFFFFF中.

  • SectionAlignment,FileAlignment
    PE文件的Body部分划分为若干节区,这些节存储着不同类别的数据.FileAlignment指定了节区在磁盘文件中的最小单位,而SectionAlignment则指定了节区在内存中最小单位.磁盘文件或内存的节区大小必定为FileAlignment或SectionAlignment值的整数倍.

  • SizeOfImage
    加载到PE文件到内存时,SizeOfImage指定了PE Image在虚拟内存中所占空间的大小.

  • SizeOfHeader
    SizeOfHeader用来指定整个PE头的大小,该值也必须是FileAlignment的整数倍.

  • SubSystem

该SubSystem值用来区分系统驱动文件(.sys)与普通的可执行文件(.exe,*.dll).Subsystem成员可拥有的值如表.

含义 备注
1 Driver文件 系统驱动(如:ntfs.sys)
2 GUI文件 窗口应用程序(如:notepad.exe)
3 CUI文件 控制台应用程序(如:cmd.exe)
  • NumberOfRvaAndSize
    NumberOfRvaAndSize用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体的最后一个成员)数组的个数.

  • DataDirectory
    DataDirectory是由IMAGE_DATA_DIRECTORY结构体组成的数组,数组的每项都要被定义的值.

IMAGE_OPTIONAL_HEADER
notepad.exe的IMAGE_OPTIONAL_HEADER

HEX Editor 描述的是notepad.exe的IMAGE_OPTIONAL_HEADER结构体区域.

3.6 节区头

把具有相拟属性的数据统一保存在一个被称为”节区”的地方,然后需要把各节区属性记录在节区头中.

需要为每个code/data/resource分别设置不用的特性,访问权限等.
类别| 访问权限
—|—
code| 执行,读取权限
data | 非执行,读取权限
resource | 非执行,读取权限

IMAGE_SECTION_HEADER
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区.

4. RVA TO RAW

PE文件加载到内存时,每个节区都要能准确完成内存地址与文件偏移间的映射.这种映射一般称为 RVA to RAW.

  • 查找RVA所在节区.
  • 换算公式
    RAW - PointtoRawData = RVA - VirtualAddress
    RAW = RVA - VirtualAddress + PointtoRawData

5. IAT

IAT (Import Address Table,导入地址表).IAT保存的内容与Windows操作系统的核心进程,内存,DLL结构等有关.简单理解 : IAT是一种表格,用来记录程序正在使用那些库中的那些函数.

5.1 DLL

  • 不要把库包含到程序中,单独组成DLL文件,需要时调用即可.
  • 内存映射技术使加载后的DLL代码,资源在多个进程中实现共享.
  • 更新库时只要替换相关DLL文件即可.

加载DLL文件时有两种方式 : 一种是”显式链接”(Explicit Linking),程序使用DLL时加载,使用完毕后释放内存;另一种是”隐式链接(Implicit Linking)”,程序开始时即一同加载DLL,程序终止时再释放占用的内存.

5.2 IMAGE_IMPORT_DESCRIPTOR

IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入那些库文件.

提示
Import : 导入,向库提供服务(函数)
Export : 导出,从库向其他PE文件提供服务(函数)