PE文件格式学习笔记

概述

Win32平台上(包括Windows 9x/NT/2000/XP/Server 2003/Vista/CE/7/10),可执行文件格式是PE。
PE是“Portable Executable File Format”(可移植的执行体)的缩写。PE格式是目前Windows平台上的主流可执行文件格式,常见的有 DLL,EXE,OCX,SYS 等。它是微软在 UNIX 平台的 COFF(通用对象文件格式)基础上制作而成。最初设计用来提高程序在不同操作系统上的移植性,但实际上这种文件格式仅用在 Windows 系列操作系统下。PE文件是指 32 位可执行文件,也称为PE32。64位的可执行文件称为 PE+ 或 PE32+,是PE(PE32)的一种扩展形式(请注意不是PE64),没有新的结构加入,只是简单地将以前32位字段扩展成64位。对于C++代码,Windows文件头的配置使其拥有不明显的区别。
事实上,一个文件是否是 PE 文件与其扩展名无关,EXE文件和DLL文件的区别完全是语义上的。它们使用完全相同的PE格式,唯一的区别就是用一个字段标识出这个文件是EXE还是DLL。

基本概念

PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成一个很大的结构。文件的内容被分割为不同的区块(Section,又称区段、节等),区块中包含代码或数据,各个区块按页边界来对齐,区块没有大小限制,是一个连续结构。每个块都有它自己在内存中的一套属性,比如:这个块是否包含代码、是否只读或可读/写等。
认识PE文件不是作为单一内存映射文件被装入内存是很重要的。Windows加载器(又称PE装载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。当磁盘文件一旦被装入内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。这样如果知道在磁盘的数据结构中寻找一些内容,那么几乎都能在被装入到内存映射文件中找到相同的信息。但数据之间的相对位置可能改变,其某项的偏移地址可能区别于原始的偏移位置,不管怎样,所有表现出来的信息都允许从磁盘文件偏移到内存偏移的转换。加载到内存中的映像的部分偏移地址之所以不同于磁盘中的偏移地址,是因为在磁盘中,区块的存储是连续的,而在内存中是按页对齐的,因此在PE头和各区块的尾部存在一个区域称为NULL填充。
如图所示:

(一)基地址

当PE文件通过Windows加载器被装入内存后,内存中的版本被称作模块(Module)。映射文件的起始地址被称为模块句柄(hModule),可以通过模块句柄访问内存中其他的数据结构。这个初始内存地址也称为基地址(ImageBase)。
内存中的模块代表着进程从这个可执行文件中所需要的代码、数据、资源、输入表、输出表及其他有用的数据结构所使用的内存都放在一个连续的内存块中,编程人员只要知道装载程序文件映像到内存后的基地址即可。PE文件剩下的其他部分可以被读入,但是可能不映射。比如当调试信息放到文件尾部的时候,PE的一个字段会告诉系统把文件映射到内存需要多少内存,不能被映射的数据将被放置在文件的尾部。
在32位Windows系统中可以直接调用GetModuleHandle以取得指向DLL的指针,通过指针访问该DLL Module的内容。
例如:HMODULE GetmoduleHandle(LPCTSRT lpModuleName);
当调用该函数时,传递一个可执行文件或DLL文件名字符串,如果系统找到文件,则返回该可执行文件或DLL文件映像加载到的基地址。也可调用GetModuleHandle,传递NULL参数,则返回调用的可执行文件的基地址。
基地址的值是由PE文件本身设定的。按照默认设置,用Visual C++建立的EXE文件基地址是00400000h,DLL文件基地址是10000000h。但是,可以在创建应用程序的EXE文件时改变这个地址,方法是在链接应用时使用链接程序的/BASE选项,或者链接后通过REBASE应用程序进行设置。

(二)虚拟地址

在Windows系统中,PE文件被系统加载器映射到内存中。每个程序都有自己的虚拟空间,这个虚拟空间的内存地址称为虚拟地址(Virtual Address,VA)

(三)相对虚拟地址

在可执行文件中,有许多地方需要制定内存中的地址。例如,引用全局变量时,需要指定它的地址。PE文件尽管有一个首选的载入地址(基地址),但是它们可以载入到进程空间的任何地方,所以不能依赖于PE的载入点。由于这个原因,必须有一个方法来指定地址(不依赖PE载入点的地址)。
为了避免在PE文件中出现绝对内存地址引入了相对虚拟地址(Relative Virtual Address,RVA)的概念。RVA只是内存中的一个简单的、相对于PE文件载入地址的偏移地址,它是一个“相对”地址(或称偏移量)、例如,假设一个EXE文件从400000h处载入,而且它的代码区块开始于401000h处,代码区块的RVA计算方法如下:
目标地址401000h-载入地址400000h=RVA1000h
将一个RVA转换成真实的地址只是简单地翻转这个过程,即用实际的载入地址加上RVA,得到实际的内存地址。它们之间的关系如下:
虚拟地址(VA)=基地址(ImageBase)+相对虚拟地址(RVA)

(四)文件偏移地址

当PE文件储存在磁盘上时,某个数据的位置相对于文件头的偏移量,称为文件偏移地址(File Offset)或物理地址(RAW Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0。用十六进制工具(例如Hex Workshop、WinHex等)打开文件所显示的地址就是文件偏移地址。

整体介绍

(一)PE文件种类

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

(二)PE文件结构简图

对于同样的结构,不同的人有不同的称呼,比如section,有的资料翻译成块,有的资料翻译成节,不要在名称上纠结,主要是把握关键字段的含义,了解其整体框架和工作流程。

(三)PE文件的执行顺序

  1. 当一个 PE 文件 被执行时,PE 装载器 首先检查 DOS header 里的 PE header 的偏移量。如果找到,则直接跳转到 PE header 的位置。
  2. 当 PE装载器 跳转到 PE header 后,第二步要做的就是检查 PE header 是否有效。如果该 PE header 有效,就跳转到 PE header 的尾部。
  3. 紧跟 PE header 尾部的是节表。PE装载器执行完第二步后开始读取节表中的节段信息,并采用文件映射( 在执行一个PE文件的时候,Windows并不在一开始就将整个文件读入内存,而是采用与内存映射的机制,也就是说,Windows装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系,只有真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系 )的方法将这些节段映射到内存,同时附上节表里指定节段的读写属性。
  4. PE文件映射入内存后,PE装载器将继续处理PE文件中类似 import table (输入表)的逻辑部分。

(四)PE文件结构说明

  1. DOS头 是用来兼容 MS-DOS 操作系统的,目的是当这个文件在 MS-DOS 上运行时提示一段文字,大部分情况下是:This program cannot be run in DOS mode. 还有一个目的,就是指明 NT 头在文件中的位置。
  2. NT头 包含 windows PE 文件的主要信息,其中包括一个 'PE' 字样的签名,PE文件头(IMAGE_FILE_HEADER)和 PE可选头(IMAGE_OPTIONAL_HEADER32)。
  3. 节表:(也称块表)是 PE 文件后续节的描述,windows 根据节表的描述加载每个节。
  4. 节:(也称块)每个节实际上是一个容器,可以包含 代码、数据 等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义。

PE结构

(一)DOS头

DOS头分为两部分,分别是“MZ头部”和“DOS存根”。

(1)MZ头

MZ头部是真正的DOS头部,由于其开始处的两个字节为“MZ”,因此DOS头也可以叫作MZ头。该部分用于程序在DOS系统下加载,它的结构被定义为IMAGE_DOS_HEADER。
其中,值得注意的是e_magic字段和e_lfanew字段
e_magic字段占两个字节,值为0x5A4D,对应ASCII码值为'MZ',"MZ"其实是MS-DOS的创建者之一Mark Zbikowski名字的缩写。
e_lfanew字段占4个字节,位于从文件开始偏移3Ch字节处,用于指出NT头的偏移地址

   typedef struct _IMAGE_DOS_HEADER {
     WORD e_magic; //魔术数字
     WORD e_cblp;  //文件最后页的字节数
     WORD e_cp;    //文件页数
     WORD e_crlc;  //重定义元素个数
     WORD e_cparhdr;  //头部尺寸,以段落为单位
     WORD e_minalloc; //所需的最小附加段
     WORD e_maxalloc; //所需的最大附加段
     WORD e_ss;    //初始的SS值(相对偏移量)
     WORD e_sp;    //初始的SP值
     WORD e_csum;  //校验和
     WORD e_ip;    //初始的IP值
     WORD e_cs;    //初始的CS值(相对偏移量)
     WORD e_lfarlc; //重分配表文件地址
     WORD e_ovno;   //覆盖号
     WORD e_res[4]; //保留字
     WORD e_oemid;  //OEM标识符(相对e_oeminfo)
     WORD e_oeminfo;  //OEM信息
     WORD e_res2[10]; //保留字
     LONG e_lfanew;   //新exe头部的文件位置
   } IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

MZ头的结构和大小是固定的,我们可以用一小段代码来检查其长度

#include<Windows.h>
#include<stdio.h>

int main()
{
   int dosheader_size;
   dosheader_size = sizeof(IMAGE_DOS_HEADER);

   printf("%d",dosheader_size);
   return 0;
}


最终求得MZ头部的大小为64字节

(2)DOS存根

DOS 残留是一段简单的程序,主要用于输出“This program cannotbe run in DOS mode.”类似的提示字符串。为什么PE结构的最开始位置有这样一段DOS头部呢?关键是为了该可执行程序可以兼容DOS系统。通常情况下,Win32下的PE程序不能在DOS下运行,因此保留了这样一个简单的DOS程序用于提示“不能运行于DOS模式下”。

(二)NT头

有些资料上也叫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文件头的指针。

(1)起始地址

起始地址由MZ头中的最后两个字节给出

从图中可以得知,这两个字节的内容为0x00000080(小端存储,低字节在低地址),从而可以在该偏移地址处找到NT头的起始位置。

(2)NT头

NT头总的结构定义在IMAGE_NT_HEADERS这个结构体中,其中可以分为三个部分,分别为“签名”、“文件头”和“可选头”

    typedef struct _IMAGE_NT_HEADERS {
      DWORD IMAGE_NT_SIGNATURE; // 签名50450000h
      IMAGE_FILE_HEADER FileHeader; // 文件头
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;  // 可选头
    } IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;

(3)签名

IMAGE_NT_SIGNATURE
在NT头的开始处是一个32位的标识信息,PE\0\0,MZ头部的e_lfanew字段正是指向PE\0\0的。

如上图所示,内容为0x00004550。
#define IMAGE_NT_SIGNATURE 0x00004550

(4)NT头:文件头

IMAGE_FILE_HEADER
IMAGE_FILE_HEADER(映像文件头)结构包含了PE文件的一些基本信息,最重要的是其中一个域指出了IMAGE_OPTIONAL_HEADER的大小。
这是一个COFF格式的文件头,指明在何种机器上运行,多少个节在里面,连接的时间,是否是可执行文件或者DLL等。DLL和可执行文件的区别:DLL不能够启动,只可以被其他可执行文件使用,一个可执行文件不能够连接到另一个可执行文件。
文件头的大小为20字节:
#define IMAGE_SIZEOF_FILE_HEADER 20
文件头的结构如下:

    typedef struct _IMAGE_FILE_HEADER {
      WORD Machine;   //运行平台
      WORD NumberOfSections;  //文件的区块数
      DWORD TimeDateStamp;    //文件创建日期和时间
      DWORD PointerToSymbolTable;   //指向符号表(用于调试)
      DWORD NumberOfSymbols;        //符号表中符号的个数(用于调试)
      WORD SizeOfOptionalHeader;    //IMAGE_OPTIONAL_HEADER32结构的大小
      WORD Characteristics;         //文件属性
    } IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;


主要掌握①②⑥⑦,③④⑤了解即可。
① Machine:可执行文件的目标CPU类型。PE文件可以在多种机器上使用,不同平台上指令的机器码不同。
我打开的这个程序中,对应的Machine字段为0x8664
在winnt.h的第16827到第16858行定义了对应值的含义:

#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

②NumberOfSections:区块(Section)的数目,块表紧跟在IMAGE_NT_HEADERS后面。

在图中对应的值为0x0004表示有4个区块。
可以使用Visual Studio 2019中自带的dumpbin工具查看。
指令为:dumpbin /exports 文件名
比如:dumpbin /exports E:\娱乐\网易云音乐\crack\ncmdump\main.exe

如上图所示,正是4个区块。
③TimeDateStamp:表明文件是何时被创建的。这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是一个比文件系统的日期/时间更精确的文件创建时间指示器。将这个值翻译为易读的字符串的方法是用_ctime函数(它是时区敏感型的),另一个对此字段计算有用的函数是gmtime。
但不知为何在我打开的这个可执行文件中,对应的字段为0。不过这个不重要。
④PointerToSymbolTable:COFF符号表的文件偏移位置。因为已采用了较新的debug格式,所以COFF符号表在PE文件中较少见。在Visual Studio .NET之前,COFF符号表可以通过设置链接器开关/DEBUGTYPE:COFF来创建。COFF符号表几乎总能在目标文件中找到,如果没有符号表存在,将此值设为0。
⑤NumberOfSymbols:如果有COFF符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF符号表的结束处,这个域是需要的。
⑥SizeOfOptionalHeader:紧跟着IMAGE_FILE_HEADER后面的数据大小。在PE文件中,这个数据结构叫IMAGE_OPTIONAL_HEADER,其大小依赖于是32位还是64位文件。对于32位PE文件,这个域通常是00E0h;对于64位PE32+文件,这个域是00F0h。不管怎么样,这些是要求的最小值,较大的值可能也会出现。

在这个程序中,SizeOfOptionalHeader的值为00F0h,也就是对于64位PE32+文件而言的最小值。
⑦Characteristics:文件属性,有选择地通过几个值的运算得到,这些标志的有效值是定义于winnt.h内的IMAGE_FILE_xxx值。

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved external references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Aggressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.


使用方法是将这个字段的值,以二进制的形式来看,比如在下图这个例子中,特征值为0223h,对应的二进制为0000 0010 0010 0011

也就是说0001h、0002h、0020h、0200h对应的值为1
换句话说,就是特征值与表中的某个值通过与操作后,若结果不为0,则说明文件具有对应属性。比如,如果Characteristics & 0x2000 = 0x2000,那么表明这是一个DLL文件。
对比特征值含义表,可以得知此文件中不存在重定位信息、文件可执行等等

(5)NT头:可选头

IMAGE_OPTIONAL_HEADER
紧跟在文件头的后面是IMAGE_OPTIONAL_HEADER,尽管名字是可选,但是该头部不是一个可选的,而是一个必须存在的头,不可以没有。该头被称作“可选头”的原因是在该头的数据目录数组中,有的数据目录项是可有可无的,数据目录项部分是可选的,因此称为“可选头”。其中包含关于如何精确处理PE文件的信息。实际上,IMAGE_OPTIONAL_HEADER是对IMAGE_FILE_HEADER的一个扩充,IMAGE_FILE_HEADER结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据,完全不必考虑两个结构区别在哪里,两者连起来就是一个完整的“PE文件头结构”。
可选头紧挨着文件头。NT头又或者叫PE头从PE文件标识符0x00004550开始,4个字节,然后是20个字节的文件头部分,因此可选头起始于从NT头开始的第25字节处,当然NT头的起始地址不固定,因为DOS存根的大小不固定,这个起始地址由DOS头的e_lfanew字段给出。而可选头的大小在文件头的SizeOfOptionalHeader字段中给出,从而可以确定出可选头的结束位置。假设可选头的大小是00F0h,也就是240字节,可选头的起始位置为0x00000098,那么结束位置便是
0x00000098+0x00F0-1=0x00000187
可选头定位技巧
可选头的定位除了计算之外还有特别的技巧,起始位置比较简单,在对应ASCII码处找到PE字样或者在16进制区域找到0x00004550。以该地址为起点往后的第25字节即为可选头起始位置。而找可选头的结束位置的技巧在于通常情况下(注意这里是指通常情况下,不是手工构造的PE文件),可选头的结尾后面跟的是第一项节表(或称区块表)的名称。比如下图中,该节表名称为.text。从而找到可选头的最后一个字节在0x00000187位置处。

可选头的数据结构
IMAGE_OPTIONAL_HEADER结构有32位和64位之分。
在winnt.h中,分别定义为IMAGE_OPTIONAL_HEADER32IMAGE_OPTIONAL_HEADER64,如下所示:

#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32            PIMAGE_OPTIONAL_HEADER;
#define IMAGE_NT_OPTIONAL_HDR_MAGIC         IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif

IMAGE_OPTIONAL_HEADER32定义为:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    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;

IMAGE_OPTIONAL_HEADER32不同的是,IMAGE_OPTIONAL_HEADER64没有BaseOfData字段,
并且 ImageBase/SizeOfStackReserve/SizeOfStackCommit/SizeOfHeapReserve/SizeOfHeapCommit的数据类型是ULONGLONG,也就是unsigned __int64类型。
IMAGE_OPTIONAL_HEADER64的定义为:

typedef struct _IMAGE_OPTIONAL_HEADER64 {
    WORD        Magic;
    BYTE        MajorLinkerVersion;
    BYTE        MinorLinkerVersion;
    DWORD       SizeOfCode;
    DWORD       SizeOfInitializedData;
    DWORD       SizeOfUninitializedData;
    DWORD       AddressOfEntryPoint;
    DWORD       BaseOfCode;
    ULONGLONG   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;
    ULONGLONG   SizeOfStackReserve;
    ULONGLONG   SizeOfStackCommit;
    ULONGLONG   SizeOfHeapReserve;
    ULONGLONG   SizeOfHeapCommit;
    DWORD       LoaderFlags;
    DWORD       NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

各字段的含义

typedef struct _IMAGE_OPTIONAL_HEADER { 
            //
            // Standard fields.                 // 标准域
            //
  +18H      WORD    Magic;                     '// 魔数 32位为0x10B,64位为0x20B,ROM镜像为0x107'
  +1AH      BYTE    MajorLinkerVersion;         // 链接器的主版本号 -> 05h
  +1BH      BYTE    MinorLinkerVersion;         // 链接器的次版本号 -> 0Ch
  +1CH      DWORD   SizeOfCode;                 // 代码节大小,一般放在“.text”节里,必须是FileAlignment的整数倍 -> 0x00000200
  +20H      DWORD   SizeOfInitializedData;      // 已初始化数大小,一般放在“.data”节里,必须是FileAlignment的整数倍 -> 0x00000400
  +24H      DWORD   SizeOfUninitializedData;    // 未初始化数大小,一般放在“.bss”节里,必须是FileAlignment的整数倍 -> 00 00 00 00
  +28H      DWORD   AddressOfEntryPoint;       '// 指出程序最先执行的代码起始地址(RVA) -> 0x00001000
  +2CH      DWORD   BaseOfCode;                 // 代码基址,当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍 -> 0x00001000
 
  +30H      DWORD   BaseOfData;                 // 数据基址,当镜像被加载进内存时数据节的开头RVA。必须是SectionAlignment的整数倍 -> 0x00002000
                                                // 在64位文件中此处被并入紧随其后的ImageBase中。
            //
            // NT additional fields.            // 以下是属于NT结构增加的领域。
            //
  +34H      DWORD   ImageBase;                  // 当加载进内存时,镜像的第1个字节的首选地址。
                                                // WindowEXE默认ImageBase值为00400000,DLL文件的ImageBase值为10000000,也可以指定其他值。
                                                // 执行PE文件时,PE装载器先创建进程,再将文件载入内存,
                                                // 然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint
 
                                                // PE文件的Body部分被划分成若干区块,这些区块储存着不同类别的数据。
  +38H      DWORD   SectionAlignment;           // SectionAlignment指定了内存中的区块的对齐大小
  +3CH      DWORD   FileAlignment;              // FileAlignment指定了在磁盘文件中的区块的对齐大小
                                                // SectionAlignment必须大于或者等于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;                // 当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
                                                // 一般文件大小与加载到内存中的大小是不同的。
 
  +54H      DWORD   SizeOfHeaders;              // 所有头的总大小,向上舍入为FileAlignment的倍数。
                                                // 可以以此值作为PE文件第一节的文件偏移量。
 
  +58H      DWORD   CheckSum;                   // 镜像文件的校验和
 
  +5CH      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。
        DWORD   SizeOfStackCommit;          // 初始提交的堆栈大小。默认是4KB 
        DWORD   SizeOfHeapReserve;          // 最大堆大小。编译器分配的。默认是1MB 
        DWORD   SizeOfHeapCommit;           // 初始提交的局部堆空间大小。默认是4K 
        DWORD   LoaderFlags;                // 与调试有关,默认为 0 
 
        DWORD   NumberOfRvaAndSizes;       '// 指定DataDirectory的数组个数,由于以前发行的Windows NT的原因,它只能为16。
        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;


(1)Magic:是一个标记字,说明文件是ROM映像(0107h),还是普通可执行的映像(010Bh),一般是010Bh,如是PE32+,则是020Bh。

(2)MajorLinkerVersion:链接程序的主版本号。
(3)MinorLinkerVersion:链接程序的次版本号。
(4)SizeOfCode:所有带有IMAGE_SCN_CNT_CODE属性区块的总共大小(只入不舍),也就是代码块的大小。如果有多个代码块的话,该值是所有代码块大小的总和(通常只有一个代码块),这个值是向上对齐FileAlignment的整数倍。例如,本例是200h,即对齐的是一个磁盘扇区字节数(200h)的整数倍。通常情况下,多数文件只有一个Code块,所以这个字段和.text块的大小匹配。
(5)SizeOfInitializedData:已初始化数据块的大小,即在编译时所构成的块的大小(不包括代码段)。但这个数据并不太准确。
(6)SizeOfUninitializedData:未初始化数据块的大小,装载程序要在虚拟地址空间中为这些数据约定空间。这些块在磁盘文件中不占空间,就像“UninitializedData”这一术语所暗示的一样,这些块在程序开始运行时没有指定值。未初始化数据通常在.bss块中。
(7)AddressOfEntryPoint:程序执行的入口地址。该地址是一个相对虚拟地址,简称 EP (EntryPoint),这个值指向了程序第一条要执行的代码。程序如果被加壳后会修改该字段的值。在脱壳的过程中找到了加壳前该字段的值,就说明找到了原始入口点,原始入口点被称为OEP。该字段的地址指向的不是 main()函数的地址,也不是WinMain()函数的地址,而是运行库的启动代码的地址。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。对于DLL,这个入口点是在进程初始化和关闭时以及线程创建/毁灭时被调用。对于DLL来说,这个值的意义不大,因为DLL甚至可以没有DllMain()函数,没有DllMain()只是无法捕获装载和卸载DLL时的4个消息。如果在DLL装载或卸载时没有需要进行处理的事件,可以将DllMain()函数省略掉。在大多数可执行文件中,这个地址并不直接指向Main、WinMain或DllMain,而是指向运行时库代码并由它来调用上述的函数。在DLL中这个域能被设置为0,前面提到的通知消息都不能收到。链接器/NOENTRY开关可以设置这个域为0。
(8)BaseOfCode:代码段的起始RVA。在内存中,代码段通常在PE文件头之后、数据块之前。在Microsoft链接器生成的执行文件中,RVA通常是1000h。Borland的Tlink32是将ImageBase加上第一个Code Section的RVA,并将结果存入该字段。
(9)BaseOfData:数据段的起始RVA。数据段通常是在内存的末尾,即PE文件头和Code Section之后。可是,这个域的值对于不同版本的微软链接器是不一致的,在64位可执行文件中是不出现的。
(10)ImageBase:

  • 《加密与解密》
    文件在内存中的首选装入地址。如果有可能(也就是说,目前如果没有其他占据这块地址,它是正确对齐的并且是一个合法的地址,等等),加载器试图在这个地址装入PE文件。如果可执行文件是在这个地址装入的,那么加载器将跳过应用基址重定位的步骤。
  • 小甲鱼
    指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中,只有指定的地址已经被其他模块使用时,文件才被装入到其他地址中。链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快,如果文件被装载到其他地址的话,将不得不进行重定位操作,这样就要慢一点。
    对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE 文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以 DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。
    在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。

(11)SectionAlignment:当被装入内存时的区块对齐大小。每个区块被装入的地址必定是本字段指定数值的整数倍。默认的对齐尺寸是目标CPU的页尺寸。对于运行在Windows 9x/Me下的用户模式可执行文件,最小的对齐尺寸是一页1000h(4KB)。这个字段可以通过链接器的/ALIGN开关来设置。在IA-64上,是按8KB来排列的。
(12)FileAlignment:磁盘上PE文件内的区块对齐大小,组成块的原始数据必须保证从本字段的倍数地址开始。对于x86可执行文件,这个值通常是200h或1000h,这是为了保证块总是从磁盘的扇区开始。在文件对齐值为1000h 时,由于与内存对齐值相同,可以加快装载速度。而文件对齐值为200h时,可以占用相对较少的磁盘空间。200h是512字节,通常磁盘的一个扇区即为512字节。

程序无论是在内存中还是磁盘上,都无法恰好满足SectionAlignment和FileAlignment值的倍数,在不足的情况下需要补0值,这样就导致节与节之间存在了无用的空隙。这些空隙对于病毒之类程序而言就有了可利用的价值。

(13)MajorOperatingSystemVersion:要求操作系统的最低版本号的主版本号。随着这么多版本的Windows的到来,这个字段明显地变得不切题了。
(14)MinorOperatingSystemVersion:要求操作系统的最低版本号的次版本号。
(15)MajorImageVersion:该可执行文件的主版本号,由程序员定义。它不被系统使用并可以设置为0,可以通过链接器的/VERSION开关设置它。
(16)MinorImageVersion:该可执行文件的次版本号,由程序员定义。
(17)MajorSubsystemVersion:要求最低子系统版本的主版本号。这个值与下一个字段一起,通常被设置为4,可以通过链接器开关/SUBSYSTEM来设置。
(18)MinorSubsystemVersion:要求最低子系统版本的次版本号。
(19)Win32VersionValue:另一个从来不用的字段,通常被设置为0。
(20)SizeOfImage:映像装入内存后的总尺寸。它指装入文件从Image Base到最后一个块的大小。最后一个块根据其大小往上取整。
(21)SizeOfHeaders:是MS-DOS头部、PE文件头、区块表的总尺寸。所有这些项目出现在PE文件中所有代码或数据区块之前。域值四舍五入至文件对齐值的倍数。
(22)CheckSum:映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算这个值。一般的EXE文件可以是0,但一些内核模式的驱动程序和系统DLL必须有一个检验和。当链接器的/RELEASE开关被使用时,校验和被置于文件中。
(23)Subsystem:一个标明可执行文件所期望的子系统(用户界面类型)的枚举值。这个值只对EXE是重要的

取值 Windows.inc中的预定义值 含义
0
IMAGE_SUBSYSTEM_UNKNOWN
未知的子系统
1 IMAGE_SUBSYSTEM_NATIVE 不需要子系统(如驱动程序)
2 IMAGE_SUBSYSTEM_WINDOWS_GUI Windows图形界面
3 IMAGE_SUBSYSTEM_WINDOWS_CUI Windows控制台界面
5 IMAGE_SUBSYSTEM_OS2_CUI OS2控制台界面
7 IMAGE_SUBSYSTEM_POSIX_CUI POSIX控制台界面
8 IMAGE_SUBSYSTEM_NATIVE_WINDOWS 不需要子系统
9 IMAGE_SUBSYSTEM_WINDOWS_CE_GUI Windows CE图形界面

(24)DllCharacteristics:DllMain()函数何时被调用,默认为0。
(25)SizeOfStackReserve:在EXE文件里,为线程保留的堆栈大小。它一开始只提交其中一部分,只有在必要时,才提交剩下的部分。
(26)SizeOfStackCommit:在EXE文件里,一开始即被委派给栈的内存。默认值是4KB。
(27)SizeOfHeapReserve:在EXE文件里,为进程的默认堆保留的内存。默认值是1MB。
(28)SizeOfHeapCommit:在EXE文件里,委派给堆的内存大小。默认值是4KB。
(29)LoaderFlags:与调试有关,默认值为0。
(30)NumberOfRvaAndSizes:数据目录的项数。这个字段从最早的Windows NT发布以来一直是16。
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
(31)DataDirectory[16]:数据目录表,这是一个结构体数组,由16个相同的IMAGE_DATA_DIRECTORY结构组成,大小为字节,指向输出表、输入表、资源块等数据。
IMAGE_DATA_DIRECTORY的结构定义如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;      //数据块的起始RVA
    DWORD   Size;                //数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

在DataDirectory这个数组中,每个元素都是一个结构体IMAGE_DATA_DIRECTORY,而根据索引值对应的结构如下所示:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 输出表(导入表) (重要)
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // 输入表 (重要)
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 资源目录 (重要)
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // 异常目录
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // 安全目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // 基址重定位表 (重要)
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // 调试目录
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // 描述信息(版权信息之类)(X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // 架构特定数据
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // 机器值
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // 线程级局部存储目录(重要)
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // 载入配置目录
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // 绑定输入目录
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // 输入地址表
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // 延迟加载导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM运行时描述符
                                             15   // 保留,必须为0


如何在十六进制文件中找到数据目录表对应的字段?

  1. 找NT头的起始地址
    首先找NT头,也就是PE\0\0,对应十六进制为0x00004550

  2. 确定文件位数
    然后找NT头中的可选头,在距NT头偏移地址为25字节的位置处,00000080h+18h=00000098h,这两个字节是NT可选头的Magic,代表着这个文件的类型,如果文件是32位,则为010B,若文件是64位,则为020B,之所以要判断文件的类型,是因为32位和64位,是因为这两种类型对应的可选头格式不同,从而数据目录表在NT头中偏移地址不同,如下图所示,010B,对应为32位文件

  3. 确定数据目录表起始和终止地址
    根据上一步,判断出文件为32位,那么数据目录表的起始地址相对于NT头的偏移地址为78h,从而可确定数据目录表起始于00000080h+78h=000000F8h
    而数据目录表为十六个元素的结构体数组,每个结构体由分别表示RVA和大小的两个双字构成。从而,数据目录表的大小为16x8=128字节。终止地址为000000f8h+80h-1=00000177h
    当然,终止地址还有一种确定方法。前面在可选头的定位技巧中说过,可选头的后面通常是第一项节表的名称,比如此处是.text。而可选头的终止地址就是数据目录表的终止地址。

  4. 使用工具
    根据前面三个步骤,已经可以将每个字段对应的值确定下来了,不过,使用工具更加直观。
    使用LordPE,将.exe文件拖入LordPE中,会弹出如下图所示的窗口:

    然后,点击“目录”,即可打开数据目录表,如下图所示:

参考教程

posted @ 2021-06-28 15:24  chuyaoxin  阅读(4078)  评论(0编辑  收藏  举报