PE(Portable Executeable File Format, 可移植的执行体文件格式),是一种用于可执行、目标文件和动态链接库的文件格式,主要是用于windows操作系统,使用该格式的目标是使链接生成的EXE文件能在不同的CPU工作指令下工作。
在Windows中的可执行程序有很多种,例如COM,PIF,SCR,EXE。但这些文件的格式大多继承自PE,其中,EXE是最常见的PE文件,动态链接库(dll)文件也是PE格式。
在Linux中,最常见的文件格式则是ELF文件格式。
在PE文件结构中,一般会涉及四种地址,分别是:
虚拟内存地址:
当PE文件被操作系统载入内存之后,PE对应的进程所对应的虚拟空间,在这个空间中的地址即为虚拟地址,它是抽象地址,不是真实存在的。
基地址:
当PE文件被载入内存后,其相关的动态链接库也会被载入。此时,被载入的文件称为模块(Module),映射文件的起始地址被称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为基地址(ImageBase)
基地址的作用是告诉操作系统应当在哪里开始存储该模块,不同模块的基地址一般是不同的。
相对虚拟内存地址:
RVA是相对于基地址的偏移,即RVA是虚拟内存中用来定位某个特定位置的地址,该地址的值是这个特定位置距离某个模块基地址的偏移量,所以说,RVA是针对于某个模块存在的。
其中,VA = Imagebase + RVA 。
注意:RVA是针对于某个模块存在的,因此RVA是有范围的,从模块开始到模块结束,脱离该范围的RVA是无效的,称为越界。
文件偏移地址:
FOA与内存无关,它是某个位置距离文件头的偏移量。使用WinHex等十六进制编辑器打开PE文件,看到的就是文件FOA。
对齐这个概念在很多文件格式中都有,在PE中,归类了三种对齐方式:数据在内存中的对齐,数据在文件中的对齐,资源文件中资源数据的对齐。
内存对齐
由于Windows的内存管理机制(分页机制),内存一般以页为单位,所以PE文件的节在内存中的对齐单位也必须至少是一个页的大小。对于32位的操作系统来说,这个值是4KB(1000h);对于64位的操作系统来说,这个值是8KB(2000h)
文件对齐
一般情况下,定义的节在文件中的对齐单位要远小于内存对齐的单位。通常会以512字节(200h)来作为对齐的单位
资源数据对齐
在资源文件中,资源字节码部分一般要求以双字(4个字节)的方式对齐。
PE结构简图
程序员眼中的PE结构。
如上图所示,一个标准的PE文件一般由四大部分组成:
其中,PE头的数据结构最为复杂,简单来说,PE头包含:
若是按照“头部+身体”的信息组织方式来看:
PE文件头部 = DOS头 + PE头 + 节表
PE文件身体 = 节内容
节内容中会出现各种不同的数据结构,如导入表、导出表、资源表、重定位表等。
在Windows的PE格式中,DOS MZ头的定义如下:
主要为现代PE文件可以对早期的DOS文件进行良好兼容存在,其结构体为IMAGE_DOS_HEADER。
大小为64字节,其中2个重要的成员分别是:
在DOS MZ 头下面的是DOS Stub(DOS存根)。整个DOS Stub是一个字节块,其内容随着链接时使用的链接器不同而不同,PE中并没有与之对应的相关结构。
实例:
NT头部保存着 Windows 系统加载可执行文件的重要信息。NT头部由IMAGE_NT_HEADERS
定义。
从该结构体的定义名称可以看出,IMAGE_NT_HEADERS由多个结构体组合而成,包括IMAGE_NT_SIGNATRUE
,IMAGE_FILE_HEADER
和 IMAGE_OPTIONAL_HEADER
三部分。
NT头部在PE文件中的位置不是固定不变的,NT头部的位置由DOS头部的e_lfanew
字段给出。
当执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER
结构的e_lfanew
字段里找到NT头的起始偏移量,用其加上基址,得到PE文件头的指针。
定义:
PE文件标识,被定义为00004550h,对应于ASCII码的字符串是**“PE\0\0”**。
即上述结构体中的 Signature
成员,它紧随在DOS Stub的后面,该标识的位置位于 IMAGE_DOS_HEADER.e_lfanew
指向的位置。
如果更改这个文件标识,操作系统就无法把该文件识别成正确的PE文件。
文件头 IMAGE_FILE_HEADER
紧随在PE标识后面,在此位置往后二十个字节的内容为数据结构标准PE头的内容。
该结构在微软的官方文档中被称为 标准通过对象文件格式 (Common Object File Format,COFF)头。它记录了PE的全局属性,如该PE文件运行的平台,PE文件类型(EXE or DLL),文件中存在节的总数等。最重要的是它指出了下一个结构 IMAGE_OPTIONAL_HEADER32
的大小。 其详细定义如下:
以该程序为例:
重点关注下 Machine成员,NumberOfSections成员以及SizeOfOptionalHeader成员和Characteristics成员。
+0004h,单字。
每个CPU都拥有的唯一的Machine码,用来指定PE文件的运行平台。这里是 0x8664,对应的就是AMD64CPU。
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_TARGET_HOST 0x0001 // Useful for indicating we want to interact with the host and not a WoW guest.
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP 0x01a3
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5 0x01a8 // SH5
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2 // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT 0x01c4 // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33 0x01d3
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP 0x01f1
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE 0x0520 // Infineon
#define IMAGE_FILE_MACHINE_CEF 0x0CEF
#define IMAGE_FILE_MACHINE_EBC 0x0EBC // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64 0x8664 // AMD64 (K8)
#define IMAGE_FILE_MACHINE_M32R 0x9041 // M32R little-endian
#define IMAGE_FILE_MACHINE_ARM64 0xAA64 // ARM64 Little-Endian
#define IMAGE_FILE_MACHINE_CEE 0xC0EE
+0006h,单字。
这个成员指出文件中存在的节区数量。
+0008h,双字。
编译器创建此文件的时间戳。
+000Ch,双字。
COFF符号表的文件偏移。
+0010h,双字。
符号表中元素的数目。
+0014h,单字。
指出结构体IMAGE_OPTIONAL_HEADER32(32位系统)的长度。对于32位PE文件,这个域通常是00E0h;对于64位PE32+文件,这个域是00F0h。
注意:用户可以自定义这个值的大小
+0016h,单字。
标识文件属性,文件是否是可运行形态、是否为DLL等,以bit OR形式进行组合。
IMAGE_OPTIONAL_HEADER 结构有 32位 和 64位 的区别。以
typedef struct _IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
+18h WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh)
+1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号
+1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号
+1Ch DWORD SizeOfCode; // 所有含代码的节的总大小
+20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小
+24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小
+28h DWORD AddressOfEntryPoint; // 程序执行入口RVA
+2Ch DWORD BaseOfCode; // 代码的区块的起始RVA
+30h DWORD BaseOfData; // 数据的区块的起始RVA
//
// NT additional fields. 以下是属于NT结构增加的领域。
//
+34h DWORD ImageBase; // 程序的首选装载地址
+38h DWORD SectionAlignment; // 内存中的区块的对齐大小
+3Ch DWORD FileAlignment; // 文件中的区块的对齐大小
+40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号
+42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号
+44h WORD MajorImageVersion; // 可运行于操作系统的主版本号
+46h WORD MinorImageVersion; // 可运行于操作系统的次版本号
+48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号
+4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号
+4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0
+50h DWORD SizeOfImage; // 映像装入内存后的总尺寸
+54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小
+58h DWORD CheckSum; // 映像的校检和
+5Ch WORD Subsystem; // 可执行文件期望的子系统
+5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0
+60h DWORD SizeOfStackReserve; // 初始化时的栈大小
+64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小
+68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小
+6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小
+70h DWORD LoaderFlags; // 与调试有关,默认为 0
+74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16
+78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
// 数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
较为重要的成员:
魔术字,说明文件的类型,如果是010B,则表示该文件为PE32,若是0107,则表示文件是ROM映像;如果为020B,则表示该文件是PE32+,即64位下的PE文件。
程序执行的入口地址。该地址是一个相对虚拟地址,简称 EP (EntryPoint),这个值指向了程序第一条要执行的代码。程序如果被加壳后会修改该字段的值。在脱壳的过程中找到了加壳前该字段的值,就说明找到了原始入口点,原始入口点被称为OEP。
该字段的地址指向的不是 main()函数的地址,也不是WinMain()函数的地址,而是运行库的启动代码的地址。
如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。
该字段指出PE映像的优先装入地址。如果可能的话(该地址没有被占用)那么操作系统会按照这个地址加载机器码到内存中,运行速度会快很多;若是该地址被其他模块占用,载入的文件就需要进行重定位操作。
对于EXE文件,默认的装入地址是0x00400000。而对于DLL文件,默认装入地址是0x00100000。
SectionAlignment字段指定了节被转入内存后的对齐单位。FileAlignment字段指定了字在文件中的对齐单位。
文件中整个PE文件的映射尺寸。以加载到内存中的HelloWorld.exe为例,HelloWorld.exe的文件头占用了1000h字节,三个字节各占用1000h字节,所以文件在内存中占用的空间总大小为4000h,该值可以比实际的值大,却不能笑,而且必须为SectionAlignment字段值的整数倍。
是MS-DOS头部、PE文件头、区块表的总尺寸。
一个标明可执行文件所期望的子系统(用户界面类型)的枚举值。
数据目录的项数。一般为00000010h,即16个。
数据目录结构。这是一个结构体数组,由16个相同的IMAGE_DATA_DIRECTORY结构组成,大小为字节,指向输出表、输入表、资源块等数据。详见下。
IMAGE_OPTIONAL_HEADER32(扩展PE头)结构的最后一个字段为DataDirectory
。该字段定义了PE文件中出现的所有不同类型的数据的目录信息。
如前面所提过的,应用程序中的数据被按照用途分成很多种类,如导出表,导入表,资源,重定位表等。在内存中,这些数据被操作系统以页为单位组织起来,并赋以不同的访问属性;在文件中,这些数据页同余被组织起来,按照不同类别分别放在文件的指定位置。
该结构就是用来描述这些不同类别的数据在文件(和内存)中的位置及大小的。所以这个字段比较重要。
数据目录中定义的数据类型一共是16种,PE就是使用 IMAGE_DATA_DIRECTORY
来定义每种数据的,该结构的定义如下:
俩个字段依次为VirtualAddress 和 isize ,如图所示,总的数据目录就由16个 IMAGE_DATA_DIRECTORY 连续排列一起组成。
如上图3-11所示,如果想要查询特定类型的数据,就要从该结构开始。比如,想要查看PE中都调用了哪些动态链接库的函数,则需要从数据目录表的第二个元素(数组编号为1,)的IMAGE_DATA_DIRECTORY
结构中获取导入表的起始位置和大小,再根据VirtualAddress_1地址指向的位置找到导入表相关的字节码。
从先前的PE结构图中可以知道,节表是由多个节表项,每个节表项记录了PE中与某个特定的节有关的信息,如节的属性,包括不同的特性、访问权限等。节表中,节的数量由 IMAGE_FILE_HEADER
中的 NumberOfSections
决定。
重要成员有4个:
VirtualSize:内存中节区所占大小
VirtualAddress:内存中节区起始地址(RVA)
SizeOfRawData:磁盘文件中节区所占大小
`结构中获取导入表的起始位置和大小,再根据VirtualAddress_1地址指向的位置找到导入表相关的字节码。
参考
Windows PE权威指南
https://www.cnblogs.com/cyx-b/p/13485664.html
https://zhuanlan.kanxue.com/article-10602.htm
https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format#optional-header-image-only