PE文件解析-文件头与整体介绍

一、PE的基本概念

    PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任何扩展名。
    认识PE文件不是作为单一内存映射文件被装入内存是很重要的。Windows加载器(又称PE加载器)遍历PE文件并决定文件的哪一部分被映射,这种映射方式是将文件较高的偏移位置映射到较高的内存地址中。PE文件的结构在磁盘和内存中是基本一样的,但在装入内存中时又不是完全复制。Windows加载器会决定加载哪些部分,哪些部分不需要加载。而且由于磁盘对齐与内存对齐的不一致,加载到内存的PE文件与磁盘上的PE文件各个部分的分布都会有差异。

二、PE结构分析

图1:PE文件的框架结构
PE文件至少包含两个段,即数据段和代码段。Windows NT 的应用程序有9个预定义的段,分别为 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,这些段并不是都是必须的,当然,也可以根据需要定义更多的段(比如一些加壳程序)。
在应用程序中最常出现的段有以下6种:
.执行代码段,通常  .text (Microsoft)或 CODE(Borland)命名;
.数据段,通常以 .data 、.rdata 或 .bss(Microsoft)、DATA(Borland)命名;
.资源段,通常以 .rsrc命名;
.导出表,通常以 .edata命名;
.导入表,通常以 .idata命名;
.调试信息段,通常以 .debug命名;

1、DOS头结构

所有的PE文件都是以一个64字节的DOS头开始。这个DOS头只是为了兼容早期的DOS操作系统。DOS头的结构如下:

typedef struct IMAGE_DOS_HEADER{
      WORD e_magic;			//DOS头的标识,为4Dh和5Ah。分别为字母MZ
      WORD e_cblp;
      WORD e_cp;
      WORD e_crlc;
      WORD e_cparhdr;
      WORD e_minalloc;
      WORD e_maxalloc;
      WORD e_ss;
      WORD e_sp;
      WORD e_csum;
      WORD e_ip;
      WORD e_cs;
      WORD e_lfarlc;
      WORD e_ovno;
      WORD e_res[4];
      WORD e_oemid;
      WORD e_oeminfo;
      WORD e_res2[10];
      DWORD e_lfanew;             //指向IMAGE_NT_HEADERS的所在
}IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS头后跟一个DOS Stub数据,是链接器链接执行文件的时候加入的部分数据,一般是“This program must be run under Microsoft Windows”。这个可以通过修改链接器的设置来修改成自己定义的数据。

2、PE头文件

   紧跟着DOS stub的时PE头文件(PE Header)。PE Header是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,其中包含许多PE装载器用到的重要字段。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基址得到PE文件头的指针。
PNTHeader = ImageBase + dosHeader->e_lfanew
PE头的数据结构被定义为IMAGE_NT_HEADERS。包含三部分,其结构如下:

typedef struct IMAGE_NT_HEADERS{
      DWORD Signature;
      IMAGE_FILE_HEADER FileHeader;
      IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}IMAGE_NT_HEADERS,*PIMAGE_NT_HEADERS; 

Signature字段:PE头的标识。双字结构。为50h, 45h, 00h, 00h. 即“PE\0\0”。
FileHeader字段:IMAGE_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;

OptionalHeader字段:IMAGE_OPTIONAL_HEADER(可选映像头)是一个可选的机构,实际上IMAGE_FILE_HEADER结构不足以定义PE文件属性,因此可选映像头中定义了更多的数据。总共224个字节,最后128个字节为数据目录(Data Directory),其结构如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;							//标志字
    BYTE    MajorLinkerVersion;				//链接器主版本号
    BYTE    MinorLinkerVersion;				//链接器次版本号
    DWORD   SizeOfCode;						//所有含有代码表的总大小
    DWORD   SizeOfInitializedData;			//所有初始化数据表总大小
    DWORD   SizeOfUninitializedData;		//所有未初始化数据表总大小
    DWORD   AddressOfEntryPoint;			//程序执行入口RVA
    DWORD   BaseOfCode;						//代码表其实RVA
    DWORD   BaseOfData;						//数据表其实RVA
    DWORD   ImageBase;						//程序默认装入基地址
    DWORD   SectionAlignment;				//内存中表的对齐值
    DWORD   FileAlignment;					//文件中表的对齐值
    WORD    MajorOperatingSystemVersion;	//操作系统主版本号
    WORD    MinorOperatingSystemVersion;	//操作系统次版本号
    WORD    MajorImageVersion;				//用户自定义主版本号
    WORD    MinorImageVersion;				//用户自定义次版本号
    WORD    MajorSubsystemVersion;			//所需要子系统主版本号
    WORD    MinorSubsystemVersion;			//所需要子系统次版本号
    DWORD   Win32VersionValue;				//保留,通常设置为0
    DWORD   SizeOfImage;					//映像装入内存后的总大小
    DWORD   SizeOfHeaders;					//DOS头、PE头、区块表总大小
    DWORD   CheckSum;						//映像校验和
    WORD    Subsystem;						//文件子系统
    WORD    DllCharacteristics;				//显示DLL特性的旗标
    DWORD   SizeOfStackReserve;				//初始化堆栈大小
    DWORD   SizeOfStackCommit;				//初始化实际提交堆栈大小
    DWORD   SizeOfHeapReserve;				//初始化保留堆栈大小
    DWORD   SizeOfHeapCommit;				//初始化实际保留堆栈大小
    DWORD   LoaderFlags;					//与调试相关,默认值为0
    DWORD   NumberOfRvaAndSizes;			//数据目录表的项数
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

DataDirectory是OptionalHeader的最后128个字节,也是IMAGE_NT_HEADERS的最后一部分数据。它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块等数据。IMAGE_DATA_DIRECTORY的结构如下:

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

数据表成员结构如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

用LordPE查看EXE文件的数据目录表:

3、区块表

3.1 区块结构

在PE文件头与原始数据之间存在一个区块表(Section Table),它是一个IMAGE_SECTION_HEADER结构数组,区块表包含每个块在映像中的信息(如位置、长度、属性),分别指向不同的区块实体。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS->FileHeader.NumberOfSections 字段来指定的。

IMAGE_SECTION_HEADER结构定义如下:

typedef struct _IMAGE_SECTION_HEADER {
    Name						//8个字节的块名
    union						
    {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;                     //区块尺寸</span>
    DWORD VirtualAddress;		//区块的RVA地址
    DWORD SizeOfRawData;		//在文件中对齐后的尺寸
    DWORD PointerToRawData;		//在文件中偏移
    DWORD PointerToRelocations;	//在OBJ文件中使用,重定位的偏移
    DWORD PointerToLinenumbers;	//行号表的偏移(供调试使用地)
    WORD NumberOfRelocations;	//在OBJ文件中使用,重定位项数目
    WORD NumberOfLinenumbers;	//行号表中行号的数目
    DWORD Characteristics;		//区块属性如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

(1)Name:这是一个8位的ASCII(不是Unicode内码),用来定义块名,多数块名以,开始(如.Text),这个实际上不是必需的,注意如果块名超过了8个字节,则没有最后面的终止标志NULL字节,带有$的区块的名字会从编译器里将带有$的相同名字的区块被按字母顺序合并。
(2) VirtualSize:指出实际的,被使用的区块大小,是区块在没有对齐处理前的实际大小.如果VirtualSize > SizeOfRawData,那么SizeOfRawData是可执行文件初始化数据的大小(SizeOfRawData – VirtualSize)的字节用0来填充。这个字段在OBJ文件中被设为0。
(3)VirtualAddress:该块时装载到内存中的RVA,注意这个地址是按内存页对齐的,她总是SectionAlignment的整数倍,在工具中第一个块默认RVA为1000,在OBJ中为0。
(4)SizeofRawData:该块在磁盘中所占的大小,在可执行文件中,该字段包括经过FileAlignment调整后块的长度。例如FileAlignment的大小为200h,如果VirtualSize中的块长度为19Ah个字节,这一块保存的长度为200h个字节。
(5) PointerToRawData:该块是在磁盘文件中的偏移,程序编译或汇编后生成原始数据,这个字段用于给出原始数据块在文件的偏移,如果程序自装载PE或COFF文件(而不是由OS装载),这种情况,必须完全使用线性映像方法装入文件,需要在该块处找到块的数据。
(6) PointerToRelocations 在PE中无意义
(7) PointerToLinenumbers 行号表在文件中的偏移值,文件调试的信息
(8) NumberOfRelocations 在PE中无意义
(9) NumberOfLinenumbers 该块在行号表中的行号数目
(10) Characteristics 块属性,(如代码/数据/可读/可写)的标志,这个值可通过链接器的/SECTION选项设置.下面是比较重要的标志:

通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。例如有一个区块叫.rdata,表明他是一个只读区块。注意:区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。另外,使用区块名字只是人们为了认识和编程的方便,而对操作系统来说这些是无关紧要的。微软给这些区块取了个有特色的名字,但这不是必须的。当编程从PE 文件中读取需要的内容时,如输入表、输出表,不能以区块名字作为参考,正确的方法是按照数据目录表中的字段来进行定位。
区块名称以及意义:

每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
在Visual C++中,用#pragma来声明,告诉编译器插入数据到一个区块内:
#pragma data_seg("MY_DATA")
链接器的一个有趣特征就是能够合并区块。如果两个区块有相似、一致性的属性,那么它们在链接的时候能被合并成一个单一的区块。这取决于是否开启编译器的 /merge 开关。下面的链接器选项将.rdata与.text区块合并为一个.text区块:
/MERGE : .rdata = .text
注意:当合并区块时,因为这没有什么硬性规定。例如,把.rdata合并到.text里不会有什么问题,但是不应该将.rsrc、.reloc或者.pdata合并到其它的区块里。

3.2 区块的对齐

   区块大小是要对齐的,有两种对齐值,一种用于磁盘文件内,另一种用于内存中。PE文件头指出了这两个值,他们可以不同。PE 文件头里边的FileAligment 定义了磁盘区块的对齐值。每一个区块从对齐值的倍数的偏移位置开始存放。而区块的实际代码或数据的大小不一定刚好是这么多,所以在多余的地方一般以00h 来填充,这就是区块间的间隙。例如,在PE文件中,一个典型的对齐值是200h ,这样,每个区块都将从200h
 的倍数的文件偏移位置开始,假设第一个区块在400h 处,长度为90h,那么从文件400h 到490h 为这一区块的内容,而由于文件的对齐值是200h,所以为了使这一区块的长度为FileAlignment 的整数倍,490h 到 600h 这一个区间都会被00h 填充,这段空间称为区块间隙,下一个区块的开始地址为600h 。
    PE 文件头里边的SectionAligment 定义了内存中区块的对齐值。PE 文件被映射到内存中时,区块总是至少从一个页边界开始。一般在X86 系列的CPU 中,页是按4KB(1000h)来排列的;在IA-64 上,是按8KB(2000h)来排列的。所以在X86 系统中,PE文件区块的内存对齐值一般等于 1000h,每个区块按1000h 的倍数在内存中存放。

3.3 文件偏移与RVA

由于一些PE文件为减少体积,磁盘对齐值不是一个内存页 1000h,而是 200h,当这类文件被映射到内存后,同一数据相对于文件头的偏移量在内存中和磁盘文件中是不同的,这样就存在着文件偏移地址与虚拟地址的转换问题。

由上图可以看出,文件被映射到内存,DOS文件头,PE文件头,区块表的偏移位置和大小都没有发生改变。而各区块映射到内存后,起偏移位置发生了改变。
转换需要前面提到的一个公式:设:ΔK为相对虚拟地址RVA与文件偏移地址File Offset的差值
VA = ImageBase + RVA
File Offset = RVA - ΔK
File Offset = VA - ImageBase - ΔK

 

原文:https://blog.csdn.net/shitdbg/article/details/49734495

猜你喜欢

转载自blog.csdn.net/zhyulo/article/details/85717711