PE文件结构(一)
0x01 基本概念
PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。PE文件是指32位可执行文件,也称PE32。64位可执行文件称为PE+或PE32+,是PE(PE32)的一种扩展形式(注意不是PE64)
PE文件的结构一般来说如下图所示:从起始位置开始依次是DOS头,NT头,节表以及具体的节。
DOS头
DOS头是用来兼容MS-DOS操作系统的,64字节,共四行,目的是当这个文件在MS-DOS上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode.还有一个目的,就是指明NT头在文件中的位置。
4字节(共4行)的DOS头,第一个成员2个字节是可执行文件的标志信息;最后一个成员4字节是PE头的偏移地址为00000100H,我们可以根据00000100H来获取PE头的地址。而DOS头和PE头中间的空余位置是一些垃圾值以及编译器填充的一些“is program cannot be run in DOS mode.”或“This program must be run under Win32”等信息。
NT头
NT头包含windows PE文件的主要信息,其中包括一个‘PE’字样的签名,PE文件头(IMAGE_FILE_HEADER)和PE可选头(IMAGE_OPTIONAL_HEADER32)。
节表
节表:是PE文件后续节的描述,windows根据节表的描述加载每个节。
节:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。
VA和RVA
PE文件使用偏移(offset),内存中使用VA(Virtual Address)来表示位置。
VA指虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)是指从某基准位置(Image Base)开始的相对地址。
PE头内部信息大多是以RVA形式存在,原因在于PE文件(主要是DLL)加载到进程虚拟内存的特定位置时,该位置可能已经加载了其他PE文件(DLL),此时必须通过重定向( Relocation)将其加载到其他空白的位置,若PE头信息使用的是VA,则无法正常访问。因此使用RVA来重定向信息,即使发生了重定向,只要相对于基准位置的相对位置没有变化,就能正常访问到指定信息,不会出现任何问题。
当PE文件被执行时,PE装载器会为进程分配4G的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4G的虚拟地址空间(ImageBase)中,一般情况下,会映射到虚拟地址空间中的0x400000的位置。
VA与RVA满足以下换算关系:
RVA+ImageBase=VA
0x02 PE头结构
PE头结构:DOS头+NT头
DOS头(分Header和DOS存根)
Header结构(00000000 - 0000003F,共64个字节)
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部
USHORT e_magic; // DOS签名“MZ-->Mark Zbikowski(设计了DOS的工程师)” -> 4D 5A
USHORT e_cblp; // 文件最后页的字节数 -> 00 90 -> 144
USHORT e_cp; // 文件页数 -> 00 30 -> 48
USHORT e_crlc; // 重定义元素个数 -> 00 00
USHORT e_cparhdr; // 头部尺寸,以段落为单位 -> 00 04
USHORT e_minalloc; // 所需的最小附加段 -> 00 00
USHORT e_maxalloc; // 所需的最大附加段 -> FF FF
USHORT e_ss; // 初始的SS值(相对偏移量) -> 00 00
USHORT e_sp; // 初始的SP值 -> 00 B8 -> 184
USHORT e_csum; // 校验和 -> 00 00
USHORT e_ip; // 初始的IP值 -> 00 00
USHORT e_cs; // 初始的CS值(相对偏移量) -> 00 00
USHORT e_lfarlc; // 重分配表文件地址 -> 00 40 -> 64
USHORT e_ovno; // 覆盖号 -> 00 00
USHORT e_res[4]; // 保留字 -> 00 00 00 00 00 00 00 00
USHORT e_oemid; // OEM标识符(相对e_oeminfo) -> 00 00
USHORT e_oeminfo; // OEM信息 -> 00 00
USHORT e_res2[10]; // 保留字 -> 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
LONG e_lfanew; // 指示NT头的偏移(根据不同文件拥有可变值) -> 00 00 00 C0 -> 192'
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
#注意Win+Intel的电脑上大部分采用”小端法”,字节在内存中储存方式是倒过来的。
重要参数为e_magic
和e_lfanew
DOS存根(00000040 - 000000BF,共128字节)
DOS存根则是一段简单的DOS程序,主要用来输出类似“This program cannot be run in DOS mode.”的提示语句。即使没有DOS存根,程序也能正常执行。
NT头(PE最重要的头)
- IMAGE_NT_HEADERS32
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // 类似于DOS头中的e_magic -> 00 00 45 50 -> PE
IMAGE_FILE_HEADER FileHeader; // IMAGE_FILE_HEADER是PE文件头,定义如下
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- IMAGE_FILE_HEADER:其中有4个重要的成员,若设置不正确,将会导致文件无法正常运行。
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // 每个CPU拥有唯一的Machine码 -> 4C 01 -> PE -> 兼容32位Intel X86芯片'
WORD NumberOfSections; // 指文件中存在的节段(又称节区)数量,也就是节表中的项数 -> 00 04 -> 4
// 该值一定要大于0,且当定义的节段数与实际不符时,将发生运行错误。'
DWORD TimeDateStamp; // PE文件的创建时间,一般有连接器填写 -> 38 D1 29 1E
DWORD PointerToSymbolTable; // COFF文件符号表在文件中的偏移 -> 00 00 00 00
DWORD NumberOfSymbols; // 符号表的数量 -> 00 00 00 00
WORD SizeOfOptionalHeader; // 指出IMAGE_OPTIONAL_HEADER32结构体的长度。-> 00 E0 -> 224字节
// PE32+格式文件中使用的是IMAGE_OPTIONAL_HEADER64结构体,
// 这两个结构体尺寸是不相同的,所以需要在SizeOfOptionalHeader中指明大小。'
WORD Characteristics; // 标识文件的属性,二进制中每一位代表不同属性 -> 0F 01
// 属性参见https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.2'} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
- IMAGE_OPTIONAL_HEADER:其中有9个重要参数,设置错误会导致文件无法运行
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107'
BYTE MajorLinkerVersion; // 链接器的主版本号 -> 05
BYTE MinorLinkerVersion; // 链接器的次版本号 -> 0C
DWORD SizeOfCode; // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 40 00 04 00
DWORD SizeOfInitializedData; // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 40 00 0A 00
DWORD SizeOfUninitializedData; // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
DWORD AddressOfEntryPoint; // 指出程序最先执行的代码起始地址(RVA) -> 00 00 10 00'
DWORD BaseOfCode; // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 10 00
DWORD BaseOfData; // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 40 00 20 00
// 在64位文件中此处被并入紧随其后的ImageBase中。
DWORD ImageBase; // 当加载进内存时,镜像的第1个字节的首选地址。
// WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
// 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
// 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint'
// PE文件的Body部分被划分成若干节段,这些节段储存着不同类别的数据。'
DWORD SectionAlignment; // SectionAlignment指定了节段在内存中的最小单位, -> 00 00 10 00'
DWORD FileAlignment; // FileAlignment指定了节段在磁盘文件中的最小单位,-> 00 00 02 00
// SectionAlignment必须大于或者等于FileAlignment'
WORD MajorOperatingSystemVersion;// 主系统的主版本号 -> 00 04
WORD MinorOperatingSystemVersion;// 主系统的次版本号 -> 00 00
WORD MajorImageVersion; // 镜像的主版本号 -> 00 00
WORD MinorImageVersion; // 镜像的次版本号 -> 00 00
WORD MajorSubsystemVersion; // 子系统的主版本号 -> 00 04
WORD MinorSubsystemVersion; // 子系统的次版本号 -> 00 00
DWORD Win32VersionValue; // 保留,必须为0 -> 00 00 00 00
DWORD SizeOfImage; // 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
// 一般文件大小与加载到内存中的大小是不同的。 -> 00 00 50 00'
DWORD SizeOfHeaders; // 所有头的总大小,向上舍入为FileAlignment的倍数。
// 可以以此值作为PE文件第一节的文件偏移量。-> 00 00 04 00'
DWORD CheckSum; // 镜像文件的校验和 -> 00 00 B4 99
WORD Subsystem; // 运行此镜像所需的子系统 -> 00 02 -> 窗口应用程序
// 用来区分系统驱动文件(*.sys)与普通可执行文件(*.exe,*.dll),
// 参考:https://blog.csdn.net/qiming_zhang/article/details/7309909#3.2.3'
WORD DllCharacteristics; // DLL标识 -> 00 00
DWORD SizeOfStackReserve; // 最大栈大小。CPU的堆栈。默认是1MB。-> 00 10 00 00
DWORD SizeOfStackCommit; // 初始提交的堆栈大小。默认是4KB -> 00 00 10 00
DWORD SizeOfHeapReserve; // 最大堆大小。编译器分配的。默认是1MB ->00 10 00 00
DWORD SizeOfHeapCommit; // 初始提交的局部堆空间大小。默认是4K ->00 00 10 00
DWORD LoaderFlags; // 保留,必须为0 -> 00 00 00 00
DWORD NumberOfRvaAndSizes; // 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。 -> 00 00 00 10
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; '// 数据目录数组。详见下文。'
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- DataDirectory[] 数据目录数组:数组每项都有被定义的值,不同项对应不同数据结构。重点关注的IMPORT和EXPORT,它们是PE头中的非常重要的部分,其它部分不怎么重要,大致了解下即可。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory ''#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory