什么是PE文件?

PE(Portable Execute)文件是Windows下可移植可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关。本文要说的PE文件结构,就是指的Window可移植可执行文件结构。

PE文件是使用的是平面地址空间,所有代码和数据都合并在一起,组成了一个很大的结构。

PE文件不是作为单一内存映射文件被载入内存,Windows加载器 (PE装载器) 遍历PE文件并决定哪一部分文件被映射。

PE文件结构

PE文件具有较强的移植性;
PE结构是一种数据组织方式;
PE结构主要应用于windows系统;
具有PE结构的文件称为PE文件;
EXE、DLL都是PE文件;

一个完整的PE文件主要有4个部分组成:DOS头,PE头,节表以及节数据
1.Dos部分主要用来对非FE格式文件的处理,DOS时代遗留的产物,是PE文件的一个遗传基因;
2.PE头部分用于宏观上记录文件的一些信息,,运行平台,大小,创建日期,属性等。
3.节表部分用于对各中类型的数据进行定义分段;
4.节数据不言而喻就是文件的数据部分,实际上我们编写程序的过程中就是对该部分的数据进行编写。而其他的部分则是由编译器依照我们编写的部分进行相应的填写而得到的。

基地址

当PE文件载入内存中后,内存的版本称为模块(Module).映射文件的起始地址称为模块句柄(hModule),可以通过其他模块句柄访问内存的其他数据结构。这个初始内存地址也被叫做基地址(ImageBase)

内存的模块代表进程将这个可执行文件所需的代码、数据、资源、输入表、输出表及其他有用的数据结构放在了一个连续的内存块中。

VA&RVA

VA是进虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址),指从某个基地址(ImageBase)开始的相对地址

转化公式:RVA + ImageBase = VA

PE头内部信息主要以RVA的形式进行存储,主要原因是PE文件(主要是DLL)加载到进程虚拟内存的特定位置时, 该位置可能已经加载了其他PE文件(DLL)。此时需要进行重定位将其加载到其他的空白位置,保证程序的正常运行。

RVA To RAW

PE文件从磁盘到内存的映射:

  1. 查找RVA所在节区

  2. 使用简单的公式计算文件偏移:

    RAW - PointerToRawData = RVA - ImageBase

    RAW = RVA - ImageBase + PointerToRawData

example:ImageBase为0x10000000,节区为.text,文件中起始地址为0x00000400,内存中的起始地址为0x01001000,RVA = 5000,RAW = 5000 - 1000 + 400 = 4400。

大体框架

PE结构DOS头,PE头,区块,输入输出表。

文件中使用偏移(offset),内存中使用VA(Virtual Address,虚拟地址)来表示位置。文件加载到内存时,情况就会发生变化(节区大小、位置等)。文件的内容一般可分为代码(.text)数据(.data)资源(.rsrc)节,分别保存。PE头与各节区的尾部存在一个区域,成为NULL填充。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数上,空白区域使用NULL进行填充。

PE头

1.DOS头

主要为现代PE文件可以对早期的DOS文件进行良好兼容存在,其结构体为IMAGE_DOS_HEADER.

大小为64字节,其中2个重要的成员分别是:

  • e_magic:DOS签名(4D5A | MZ)占一个字节
  • e_lfanew:真正的PE文件头的相对偏移(RVA),指出真正的PE头的头文件偏移位置,占四个字节位于(3Ch)处,指示NT头的偏移(文件不同,值不同)

DOS存根

DOS stub(DOS块),位于DOS头下方,可选,大小不固定,由代码和命令混合。可以把DOS MZ头与DOS stud 合称为DOS头

2.PE文件头(PE Header)

PE header是PE相关结构NT映像头的简称,当运行时PE装载器将从IMAGE_DOS_HEADER结构的e_lfanew字段里找到PE header的起始偏移量,加上基址,得到PE文件头的指针。

PNTHeader = ImageBase + DOSHeader -> e_lfanew

有两个版本的IMAGE_NT_HEADER结构,一个是PE32 另一个是PE32+,它又被叫NT头.

NT头

结构体为IMAGE_NT_HEADERS,大小为F8,由三个成员组成

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;              //4个字节的PE标志
IMAGE_FILE_HEADER FileHeader;      //文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader;//可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  • Signature(签名数字体)值为50450000h(“PE”00),它是PE文件头的开端,MS_DOS头部的e_lfanew字段正是指向”PE\0\0”的。
  • IMAGE_FILE_HEADER(文件头),表现文件大致属性,
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //机器型号,每个CPU都拥有的唯一的Machine码,作用是区别这个exe是哪个CPU可以跑的.重要.
WORD NumberOfSections;//节区的数量 (可以理解为汇编中区的个数)现在我们有两个,一个.rdata 一个.text
DWORD TimeDateStamp;  //程序的编译时间,参考用,没有实际作用
DWORD PointerToSymbolTable;//符号表地址 我们使用的PDB文件(里面有函数吗什么的)都存放在这个表中,不过微软是单独生成的PDB文件,所以这个字段没用,主要是给别人用
DWORD NumberOfSymbols;    //符号表大小
WORD SizeOfOptionalHeader; //指出结构体IMAGE_OPTIONAL_HEADER32(32位系统)的长度,可选头大小,这个字段很重要.因为要通过这个字段,才知道可选头是多大,而不懂PE的人求选项头都是用sizeof()求出来的.所以真正的选项头大小要靠这个字段
WORD Characteristics;    //文件属性,描述文件信息的.
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

4.Characteristics:标识文件属性,文件是否是可运行形态、是否为DLL等,以bit OR形式进行组合注没有看结构体

  • 可选头结构体为IMAGE_OPTIONAL_HEADER,重要的成员有9个:

    typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD Magic;
    BYTE MajorLinkerVersion;
    BYTE MinorLinkerVersion;
    DWORD SizeOfCode;
    DWORD SizeOfInitializedData;
    DWORD SizeOfUninitializedData;
    DWORD AddressOfEntryPoint;
    DWORD BaseOfCode;
    DWORD BaseOfData;
    DWORD ImageBase;
    DWORD SectionAlignment;
    DWORD FileAlignment;
    WORD MajorOperatingSystemVersion;
    WORD MinorOperatingSystemVersion;
    WORD MajorImageVersion;
    WORD MinorImageVersion;
    WORD MajorSubsystemVersion;
    WORD MinorSubsystemVersion;
    DWORD Win32VersionValue;
    DWORD SizeOfImage;
    DWORD SizeOfHeaders;
    DWORD CheckSum;
    WORD Subsystem;
    WORD DllCharacteristics;
    DWORD SizeOfStackReserve;
    DWORD SizeOfStackCommit;
    DWORD SizeOfHeapReserve;
    DWORD SizeOfHeapCommit;
    DWORD LoaderFlags;
    DWORD NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
    • Magic:IMAGE_OPTIONAL_HEADER32为10B,IMAGE_OPTIONAL_HEADER64为20B
    • AddressOfEntryPoint:持有EP的RVA值,指出程序最先执行的代码起始地址
    • ImageBase:指出文件的优先装入地址(32位进程虚拟内存范围为:0~7FFFFFFF)
    • SectionAlignment,FileAlignment:前者制定了节区在内存中的最小单位,后者制定了节区在磁盘文件中的最小单位
    • SizeOfImage:指定了PE Image在虚拟内存中所占空间的大小
    • SizeOfHeaders:指出整个PE头的大小
    • Subsystem:区分系统驱动文件和普通可执行文件
    • NumberOfRvaAndSize:指定DataDirectory数组的个数
    • DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组
    IMAGE_DATA_DIRECTORY	STRUCT
    VirtualAddress DWORD ? ;数据块的起始RVA
    Size DWORD ? ;数据块的长度
    IMAGE_DATA_DIRECTORY ENDS

4.区块

PE文件头与原始数据之间存在一个区块表(Section Table),区块表包含每个块在映像中的信息,分别指向不同实体。

区块表中定义了各区块的属性,包括不同的特性、访问权限等,结构体为IMAGE_SECTION_HEADER,重要成员有5个:

typedef struct _IMAGE_SECTION_HEADER 
{
+0h BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text”
//IMAGE_SIZEOF_SHORT_NAME=8
union
+8h {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一
// 般是取后一个
} Misc;
+ch DWORD VirtualAddress; // 节区的 RVA 地址
+10h DWORD SizeOfRawData; // 在文件中对齐后的尺寸
+14h DWORD PointerToRawData; // 在文件中的偏移量
+18h DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移
+1ch DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)
+1eh WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目
+20h WORD NumberOfLinenumbers; // 行号表中行号的数目
+24h DWORD Characteristics; // 节属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
  • VirtualSize:内存中节区所占大小
  • VirtualAddress:内存中节区起始地址(RVA)
  • SizeOfRawData:磁盘文件中节区所占大小
  • Charateristics:节区属性(bit OR)

5.输入表

可执行文件使用来自其他DLL的代码或数据的动作被叫做输入,当PE文件被加载时,Windows加载器的工作之一就是定位所有被输入的函数和数据,并让正在加载的文件可以使用这些地址,这些过程是通过PE文件的输入表完成的。

所有输入函数,API的DLL的指向函数指针,这些指针叫做输入地址表(IAT)

用IT调用比IAT调用多花费jmp指令,这么复杂是因为编译器无法区分普通函数和调用函数

IAT,导入地址表(Import Address Table),保存了与windows操作系统核心进程、内存、DLL结构等相关的信息。

PE装载器把导入函数输入至IAT的顺序

  1. 读取IID的Name成员,获取库名称字符串(eg:kernel32.dll)

  2. 装载相应库:LoadLibrary(“kernel32.dll”)

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

  4. 逐一读取INT中数组的值,获取相应IMAGE_IMPORT_BY_NAME地址(RVA)

  5. 使用IMAGE_IMPORT_BY_NAME的Hint(ordinal)或Name项,获取相应函数的起始地址:

    GetProcAddress(“GetCurrentThreadld”)

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

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

  8. 重复以上步骤4~7,直到INT结束(遇到NULL)

IMAGE_IMPORT_DESCRIPTOR结构体中记录着PE文件要导入哪些库文件,因为在执行一个程序时需要导入多个库,所以导入了多少库,就会存在多少IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体组成数组,数组最后以NULL结构体结束。

绑定输入

当一个可执行文件被绑定时,IAT的IMAGE_THUNK_DATA数组并用输入函数的实际地址改写了。在磁盘的可执行文件中存放的是与DLL输出函数的相关的实际内存地址,可以让程序更快的初始化。

执行程序是Bind程序进行两个假设

  • 当进程初始化时,需要的DLL实际上加载到了他们的首选基地址。
  • 自从绑定操作执行以来,DLL输入表中引用的符号位置一直没有变。

绑定目录表(DataDirectorty)的第12个成员指向绑定输入,以一个IMAGE_BOUND_IMPORT_DESCRIPTOR结构的数组开始

6.输出表

创建一个DLL时,实际上创建了一组能让EXE或其他DLL调用函数,此时PE装载器根据DLL文件中输入的信息修正被执行文件的IAT。当一个DLL函数能被EXE或另一个DLL文件使用时,它就输出了。输出信息被保存在输入表里,DLL文件通过输入表向系统提供输入函数名,序号和入口地址等信息。

输出表的第一个成员指向IMAGE_EXPORT_DIRECTORY(简称IED)结构

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

具体每一项的含义如下:

  1. Characteristics:现在没有用到,一般为0。
  2. TimeDateStamp:导出表生成的时间戳,由连接器生成。
  3. MajorVersion,MinorVersion:看名字是版本,实际貌似没有用,都是0。
  4. Name:模块的名字。
  5. Base:序号的基数,按序号导出函数的序号值从Base开始递增。
  6. NumberOfFunctions:所有导出函数的数量。
  7. NumberOfNames:按名字导出函数的数量。
  8. AddressOfFunctions:一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。
  9. AddressOfNames:一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。
  10. AddressOfNameOrdinals:一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。

基址重定位

当链接器生成一个PE文件时,会装载到默认的基址下,把code和data的相关地址都写入PE文件。如果PE文件被装到虚拟内存的另一个地址中,链接器记录的就是错误的,需要重定位表来调整,用”.reloc”表示

资源

Windows的各种界面叫做资源包括加速键(Accelerator)、位图(Bitmap)、光标(Cursor)、对话框(Dialog Box)、图标(Icon)、菜单(Menu)、串表(String Table)、工具栏(Toolbar)和版本信息(Version Information)等

资源目录结构
数据目录表中的 IMAGE DIRECTORY ENTRY RESOURCE条目(第三项)包含资源的RVA和大小。资源目录结构中的每一个节点都是由 IMAGE RESOURCE DIRECTORY结构和紧跟其后的数个 IMAGE RESOURCE DIRECTORY ENTRY结构组成的。

IMAGE_RESOURCE_DIRECTORYIMAGE_RESOURCE_DIRECTORY_ENTRY 结构体定义如下:

typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //属性,一般为0
DWORD TimeDateStamp; //资源的产生时刻,一般为0
WORD MajorVersion; //主版本号,一般为0
WORD MinorVersion; //次版本号,一般为0
WORD NumberOfNamedEntries; //以名称(字符串)命名的资源数量
WORD NumberOfIdEntries; //以ID(整型数字)命名的资源数量
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

1. 第一层

第一层起始于一个 IMAGE_RESOURCE_DIRECTORY 头,后面紧接着是 IMAGE_RESOURCE_DIRECTORY_ENTRY 数组。数组个数 = NumberOfNamedEntries + NumberOfIdEntries

IMAGE_RESOURCE_DIRECTORY_ENTRY 使用的是 Name 与 OffsetToDirectory,分别代表了资源类型与第二层的数据偏移地址。Name 与资源类型的匹配如下: