PE文件详解------PE文件结构剖析

来源:互联网 发布:linux redis 批量删除 编辑:程序博客网 时间:2024/06/11 09:57

一.PE文件结构纵览


PE文件的结构如上图所示,由低地址到高地址分别为:Dos头,PE头,块表,块,调试信息。其中真正的PE文件头是位于Dos头的后面的部分。


上图为利用PE工具打开的一个可执行文件在磁盘中的映射,这就是PE文件的内部信息。

可以看到这个文件的钱两个字节为”4D 5A”,表示的就是Dos头的第一个字段的额信息,标示Dos的头部。在Dos头结构的最后一个字段是一个指针,这个指针指向PE头。在上图中可以看到00 00 00 60表示的就是这个指针的值,它指向这个PE文件的真正PE头。根据这个地址可以找到PE头开始的地方,45 50表示的就是PE头结构的而第一个字段,标示的就是PE头。可以看到,PE文件的结构十分分明,没有一块多余的地方,各个部分排列紧凑,井井有条。我们获取PE文件的信息就是通过读取这个2进制文件。


二.Dos 头(Dos Header)

Dos头中的信息存储在IMAGE_DOS_HEADER中。这个结构的定义为:

 

IMAGE_DOS_HEADER STRUCT

{

+0hWORDe_magic //Magic DOS signature MZ(4Dh 5Ah) DOS 可执行文件标记

+2h WORD e_cblp //Bytes on last page of file

+4hWORD e_cp //Pages in file

+6hWORD e_crlc //Relocations

+8hWORD e_cparhdr //Size of header in paragraphs

+0ahWORD e_minalloc //Minimun extra paragraphs needs

+0chWORD e_maxalloc //Maximun extra paragraphs needs

+0ehWORD e_ss //intial(relative)SS value DOS 代码的初始化堆栈SS

+10hWORD e_sp //intial SP value DOS 代码的初始化堆栈指针SP

+12hWORD e_csum //Checksum

+14hWORD e_ip // intial IP value DOS 代码的初始化指令入口[指针IP]

+16hWORD e_cs //intial(relative)CS value DOS 代码的初始堆栈入口

+18hWORD e_lfarlc //File Address of relocation table

+1ahWORD e_ovno // Overlay number

+1chWORD e_res[4] //Reserved words

+24hWORD e_oemid // OEM identifier(for e_oeminfo)

+26hWORD e_oeminfo // OEM information;e_oemid specific

+29hWORD e_res2[10] // Reserved words

+3chDWORD e_lfanew //Offset to start of PE header 指向PE 文件头

} IMAGE_DOS_HEADER ENDS

 

 

在Image_Dos_Header 中有几个成员是很重要的,第一个magic,这个字段是来标示是否为Dos 程序的,如果是的话那么就会有MZ 的标记,16 进制为4Dh 5Ah,一个字母占一个字节,一共是两个字节。还有一个是最后一个字段,e_lfanew,这是一个指向PE 文件头的指针,指出了PE 文件头在PE 文件中的偏移。如果这个值为0,那么说明这个exe 文件是一个Dos 程序,windows 会启动Dos 子系统来运行它。


三.Dos Stub

Dos Stub部分其实就是一段windows下的一段代码,当windows程序在Dos系统下运行的时候,那么系统就会返回一段信息,表示这个程序是一个windows下的可执行文件,不能在Dos系统下运行。


四.PE头(PE Header )

这部分是PE文件真正的PE头,它存储在一个结构体中,它的类型为IMAGE_NT_HEADER


1.PE头的结构 IMAGE_NT_HEADER 

PE头实际上是一个IMAGE_NT_HEADER结构,它的定义为:

 

IMAGE_NT_HEADERS STRUCT{

+0hDWORDSignature //

+4h IMAGE_FILE_HEADER FileHeader //

+18hIMAGE_OPTIONAL_HEADER32OptionalHeader //

} IMAGE_NT_HEADERS ENDS

 

紧接着Image_Dos_Header 的结构是Image_NT_Header,这是PE 真正的头文件。这个结构包含了三个字段,第一个是Siganture,第二个是一个结构Image_FILE_Header,第三个是Image_Optional_Header。

在一个有效的PE 文件里,Signature 字段被设置为00004550h, ASCII 码字符是“PE00”。标志这PE 文件头的开始。“PE00” 字符串是PE 文件头的开始, DOS 头部的e_lfanew 字段正是指向这里。

Image_FILE_Header 结构为PE 映像头,而Image_Optional_Header 是PE 扩展头部。


2.PE文件映像头结构 IMAGE_FILE_HEADER

它的定义为:

 

typedef struct _IMAGE_FILE_HEADER

{

+04h WORD Machine; // 运行平台

+06h WORD NumberOfSections; // 文件的区块数目

+08h DWORDTimeDateStamp; // 文件创建日期和时间

+0Ch DWORD PointerToSymbolTable; // 指向符号表(主要用于调试)

+10hDWORD NumberOfSymbols; // 符号表中符号个数(同上)

+14h WORD SizeOfOptionalHeader; // IMAGE_OPTIONAL_HEADER32 结构大小

+16h WORD Characteristics; // 文件属性

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 

以下为对IMAGE_FILE_HEADER部分数据成员做出解释:

Machine:可执行文件的目标CPU 类型。

NumberOfSection: 区块的数目。(注:区块表是紧跟在IMAGE_NT_HEADERS 后边的)

TimeDataStamp: 表明文件是何时被创建的。

这个值是自1970 年1 月1 日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统( FILESYSTEM ) 的日期时间更加精确的指示器。如何将这个值翻译请看:

PointerToSymbolTable: COFF 符号表的文件偏移位置,现在基本没用了。

NumberOfSymbols: 如果有COFF 符号表,它代表其中的符号数目,COFF 符号是一

个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量。

SizeOfOptionalHeader: 紧跟着IMAGE_FILE_HEADER 后边的数据结构(IMAGE_OPTIONAL_HEADER)的大小。(对于32 位PE 文件,这个值通常是00E0h;对于64 位PE32+文件,这个值是00F0h )。

Characteristics: 文件属性,有选择的通过几个值可以运算得到。( 这些标志的有效值是定义于winnt.h 内的IMAGE_FILE_** 的值,具体含义见下表。普通的EXE 文件这个字段

的值一般是0100h,DLL 文件这个字段的值一般是210Eh。)


3.可选的PE头结构 IMAGE_OPTIONAL_HEADER

它的定义为:

 

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; // D

llMain()函数何时被调用,默认为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_ENT

RIES];

// 数据目录表

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

 

这其中的大部分结构都不重要,不过有几个是很重要的。

 

(1).AddressOfEntryPoint 字段

指出文件被执行时的入口地址,这是一个RVA 地址(RVA 的含义在下一节中详细介绍)。如

果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口

地址指向附加的代码就可以了。

 

(2). ImageBase 字段

指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,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。

 

(3). SectionAlignment 字段和FileAlignment 字段

SectionAlignment 字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment 字段指定了节存储在磁盘文件中时的对齐单位。

 

(4).Subsystem 字段

指定使用界面的子系统,在windows下可选的子系统为CUI和GUI。这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:**选项指定的就是这个字段的值,在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI 的话,窗口必须由程序自己建立。


4.数据目录(Data Directory)

在结构IMAGE_NT_HEADER 中有一个结构IMAGE_OPTINAL_HEADER 结构,在IMAGE_OPTINAL_HEADER 中又有一个字段,这个字段是一个结构数组IMAGE_DATA_DIREECTORY[]。这个结构数组中的数据的类型全部都是IMAGE_DATA_HEADER 结构,这个结构中有两个成员,一个是RVA(相对虚拟地址),一个是Size(大小)。它的定义如下:

 

Typedefe IMAGE_DATA_DIRECTORT

{

DWORD VirtualAddress ;

DWORD Size ;

} IMAGE_DATA_DIRECTORT ;

 

在windows 系统撞在PE 可执行文件时,往往需要很快摘到一些装载需要的数据结构,比如导入表,导出表,资源,重定位表等等。这些常用的数据结构的位置和长度都被保存在IMAGE_DATA_HEADER 结构里。在这个数组里面,每一个元素都对应一个一个包含一定信息的表。比如数组的第一个元素就是记录的是导出表的地址的长度。


5.导入表(Import table )

在代码分析或编程中经常遇到“输入函数(Import Functions,也称导入函数)”的概念。这里我们就来解释下,输入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于相关的DLL 文件中,在调用者程序中只保留相关的函数信息(如函数名、DLL 文件名等)就可以。

对于磁盘上的PE 文件来说,它无法得知这些输入函数在内存中的地址,只有当PE 文件被装入内存后,Windows 加载器才将相关DLL 装入,并将调用输入函数的指令和函数实际所处的地址联系起来。这就是“动态链接”的概念。动态链接是通过PE 文件中定义的“输入表”来完成的,输入表中保存的正是函数名和其驻留的DLL 名等。

在PE 文件头的IMAGE_OPTIONAL_HEADER 结构中的DataDirectory(数据目录表) 的第二个成员就是指向输入表的。而输入表是以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)

的数组开始。每个被PE 文件链接进来的DLL 文件都分别对应一个IID 数组结构。在这个

IID 数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以

一个全为NULL(0) 的IID 作为结束的标志。

 

IMAGE_IMPORT_DESCRIPTOR STRUCT

{

union

Characteristics DWORD ?

OriginalFirstThunk DWORD ?

ends

TimeDateStamp DWORD ?

ForwarderChain DWORD ?

Name DWORD ?

FirstThunk DWORD ?

}

IMAGE_IMPORT_DESCRIPTOR ENDS 



五.块表(Section Table)


紧接着PE文件的头部就是section table(段表)了,它记录着pe文件中每一个区(段)的信息。块在映像中是按起始地址排(RVA)列的。它的定义如下:

 

typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text” 
//IMAGE_SIZEOF_SHORT_NAME=8
union {
DWORD PhysicalAddress;//物理地址
DWORD VirtualSize;//真实长度,这两个值是一个联合结构,可以使用其中的任何一个,
//一般是节的数据大小
} Misc;
DWORD VirtualAddress;//RVA
DWORD SizeOfRawData;//物理长度
DWORD PointerToRawData;//节基于文件的偏移量
DWORD PointerToRelocations;//重定位的偏移
DWORD PointerToLinenumbers;//行号表的偏移
WORD NumberOfRelocations;//重定位项数目
WORD NumberOfLinenumbers;//行号表的数目
DWORD Characteristics;//节属性 如可读,可写,可执行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; 

 

1.VirtualSize:该块的真实长度。和SizeOfRawData不一样,是块对齐前的长度。

2. VirtualAddress:该块装载到内存中的RVA。这个地址是按照内存页对齐的,它的数值总是SectionAlignment的整数倍。

3. SizeOfRawData:该块在磁盘中所占的大小。在可执行文件中,该字段包含经过FileAlignment调整后的块的长度。

4.PointerToRawData:该块在磁盘中的偏移。程序经过编译或汇编后生成原始数据,这个字段用于给原始数据在文件中的偏移

5.Characteristics:块属性。该字段是一组指出块属性(如代码/数据/可读/可写等)的标志。



六.块(Section)结构

1.text块

.text是在编译或者汇编结束时产生的一种块。它的内容全是指令代码。PE文件运行在32方式下,不受16位的约束,所以没有理由把代码放到不同的sections中。链接器把所有目标文件的.text块链接成一个大块的.text中。

2.data块

如同.text是默认的代码块一样,.data是初始化的数据块。这些数据包括编译时被初始化的global和static变量,也包括字符串。链接器把obj及lib文件的.data结合成一个大的.data。Local数据可以放在一个线性的堆栈中,不占.data和.bbs的空间。

 3.idata

.idata包含其他外来的DLL的函数及数据信息,即输入表。该功能与NE文件的模块引用表类似,关键的差异在于PE文件中的每一个输入函数都明确地列于该块中。要在NE格式中找到相同的信息,必须从各段的重定位数据中查找。

4 .rsrc

.rsrc包含模块的全部资源数据,如图表,菜单,位图等。

5.reloc

.reloc保存基地址重定位表。当装载程序不能按链接器所指定的地址装载文件时,需要对指令或已初始化的变量进行调整,基址重定位表包含调整所需的数据,如果装载程序能正常装载文件,它就忽略.reloc中的重定位数据。

6.edata

.edata是该PE文件的输出表,以供其他模块引用。PE格式文件没有必要输出一个函数,所以通常只是在DLL文件中才能看到.edata块。

7.rdata

.rdata块通常是在.data或.bbs中间,但程序很少用到该块中的数据。

8.tls

TLS的意思是”线程局部存储器。

其他常见的块:.debug$s , .bbs等。



关于PE文件的详解,大家看以参考鱼C论坛小甲鱼的加解密系统篇的视频,讲的很赞。