PE结构

来源:互联网 发布:淘宝秒杀软件taovb 编辑:程序博客网 时间:2024/05/21 06:38
什么是PE文件百度百科1.常见的PE文件:exe、dll、ocx 、sys……

2.OS和可执行文件的关系:

OS依靠PE结构知道代码和数据是如何分布的。可执行文件的格式是操作系统本身执行机制的反映。


3.Exe和Dll都是使用完全相同的PE格式,唯一的区别就是用一个字段标识出这个文件是Exe还是Dll


4.32位和64位PE格式不区别:64位Windows只是针对PE格式做了一些简单的修饰,新格式叫做PE32+。并没有任何新的结构加进去,改变的只是简单的将32位字段扩展为64位


5.PE格式的定义主要在Winnt.h中,PE文件中的数据结构一般有32位和64位之分,一般名称上会表现出来IMAGE_NTHEADERS32和IMAGE_NTHEADERS64


6、PE文件使用的是一个平面地址空间,所有代码和数据都被合并在一起,组成很大的结构。

文件的内容被分割为不同的区块,块中包含代码或数据,各个区块按页边界来对齐,区块没有大小的限制,是一个连续的结果。

此外,每个块都有自己在内存中的一套属性,比如这个区块是否包含代码,是否只读或可读/写等。


下面是PE文件结构图:


DOS头:

IMAGE_DOS_HEADER STRUCT(注:最左边是文件头的偏移量。){+0hWORDe_magic   //Magic DOS signature MZ(4Dh 5Ah)     DOS可执行文件标记  MZ:PE格式设计者的名称,如果开头不为MZ,则不能成功运行【DOS头的标识】 //每一个PE文件是以一个Dos程序开始了, DOS头作用相当于一个windows应用程序被运行在DOS操作系统中,避免程序在DOS中运行可能出现崩溃的情况,Dos才能识别这是有效的执行体,如果运行在windows平台,会根据头部0x3cH偏移处的“指针”来找到NT头【注意大小端】+ 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 偏移3c的位置 指向PE文件头 } IMAGE_DOS_HEADER ENDS

用UE的方式对PE文件进行打开,可以看到如下图:


PE文件都是以4D 5A MZ标志开头的,然后接着的就是一些DOS信息,这一部分对于Windows系统下的软件执行没有影响,一些简单的病毒,或将代码写在DOS头和PE头之间的位置上,以达到利用软件传播病毒的效果。

PE文件头(PE Header):

紧挨着DOS stub 。PE Header是PE相关结构NT映像头IMAGE_NT_HEADER的简称,里面包含着许多PE装载器用到的重要字段

IMAGE_NT_HEADERS STRUCT{+0h       DWORDSignature  //4 PE标识   “PE00” 字符串是 PE 文件头的开始,DOS 头部的 e_lfanew 字段正是指向这里。 在一个有效的 PE 文件里,Signature 字段被设置为00004550h, ASCII 码字符是“PE00”。标志这 PE 文件头的开始。// PE头-NT头*1  :NT头的标识必须是PE,同样不允许随意修改。【与DOS头之间的区域可以任意修改(一些病毒代码写在这里)】+ 4h       IMAGE_FILE_HEADER FileHeader+ 18h      IMAGE_OPTIONAL_HEADER32 OptionalHeader} IMAGE_NT_HEADERS ENDS

其中IMAGE_FILE_Header的结构如下

typedef struct _IMAGE_FILE_HEADER{+04hWORD  Machine;              //2 CPU类型 运行平台+06h  WORD  NumberOfSections;     //2 区段数目【重要】 文件的区块数目+08hDWORD TimeDateStamp;        // 2 创建日期(何时被编译) +0Ch  DWORD PointerToSymbolTable; // 8 符号表相关,用于调试(release版本全为0) 指向符号表(主要用于调试)+10h DWORD NumberOfSymbols;      // 符号表中符号个数(同上)+14h  WORD  SizeOfOptionalHeader; //   2 可选头大小 IMAGE_OPTIONAL_HEADER32 结构大小+16h  WORD  Characteristics;      //2文件信息标志  文件属性} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

IMAGE_OPTIONAL_HEADER32 结构:

typedef struct _IMAGE_OPTIONAL_HEADER{ +18h    WORD    Magic;//2标志字,ROM 映像(0107h),普通可执行文件(010Bh),不可随便修改+1Ah    BYTE    MajorLinkerVersion;      //1链接程序的主版本号+1Bh    BYTE    MinorLinkerVersion;      //1链接程序的次版本号+1Ch    DWORD   SizeOfCode;     //4 所有含代码的节的总大小Rsize .test段+20h    DWORD   SizeOfInitializedData;   //4所有含已初始化数据的节的总大小+24h    DWORD   SizeOfUninitializedData; //4未初始化数据段大小 所有含未初始化数据的节的大小+28h    DWORD   AddressOfEntryPoint;     // 4  程序入口地址RVA【重点】+2Ch    DWORD   BaseOfCode;              // 4  代码块起始RVA+30h    DWORD   BaseOfData;              //4数据块起始RVA// NT additional fields.    以下是属于NT结构增加的领域。+34h    DWORD   ImageBase;               // 4 基址 程序的首选装载地址+38h    DWORD   SectionAlignment;  //4内存中的区块的对齐大小【一般不改】+3Ch    DWORD   FileAlignment;     // 4文件中的区块的对齐大小【一般不改】+40h    WORD    MajorOperatingSystemVersion;  //   2 操作系统首版本号 要求操作系统最低版本号的主版本号+42h    WORD    MinorOperatingSystemVersion;  //2操作系统次版本号 要求操作系统最低版本号的副版本号+44h    WORD    MajorImageVersion; // 4 用户程序首次版本号 可运行于操作系统的主版本号+46h    WORD    MinorImageVersion;       // 可运行于操作系统的次版本号+48h    WORD    MajorSubsystemVersion;   //4 子系统首次版本号 要求最低子系统版本的主版本号+4Ah    WORD    MinorSubsystemVersion;   // 要求最低子系统版本的次版本号+4Ch    DWORD   Win32VersionValue;       //4 保留 莫须有字段,不被病毒利用的话一般为0+50h    DWORD   SizeOfImage;             //4映像大小【也就是经过windows属性标识后文件的大小,不能随便改】 映像装入内存后的总尺寸+54h    DWORD   SizeOfHeaders;           //4块前头部大小 所有头 + 区块表的尺寸大小+58h    DWORD   CheckSum;                //4 校验和【很多应用程序都为空,若应用程序校验和和实际程序的相匹配的话,杀毒会放宽】 映像的校检和+5Ch    WORD    Subsystem;               //2程序运行所需子系统【exe时为02,改为03,则多出一个控制台程序】 可执行文件期望的子系统+5Eh    WORD    DllCharacteristics;      //2当文件为dll时使用 DllMain()函数何时被调用,默认为 0+60h    DWORD   SizeOfStackReserve;      //4保留栈大小 初始化时的栈大小+64h    DWORD   SizeOfStackCommit;       //4使用栈大小 初始化时实际提交的栈大小+68h    DWORD   SizeOfHeapReserve;       //4保留堆大小 初始化时保留的堆大小+6Ch    DWORD   SizeOfHeapCommit;        //4使用堆大小  初始化时实际提交的堆大小+70h    DWORD   LoaderFlags;             //  4设置自动调用断点或调试器与调试有关,默认为 0 +74h    DWORD   NumberOfRvaAndSizes;     //4未知  下边数据目录的项数,这个字段自Windows NT 发布以来一直是16+78h    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 80  Image_Data_Directors  数据目录表} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

链接一些概念:

文件偏移地址: 数据在文件内相对于文件头的偏移

VA虚拟地址 : 数据(代码以及真正的数据data)加载到内存后,内存里面的地址

RVA 相对虚拟地址栏 : 数据(代码以及真正的data)加载到内存后,相对的地址,其中的一条数据或指令相对于整个存储在内存中最开始的偏移

文件对齐:

硬盘分算区,最少读取512个字节【为了提高效率】

如:一个代码段,长度513字节,根据对齐,在硬盘中就要占用1024个字节。

内存对齐值:1000H==4096个字节,最小分配单位就是4096个字节【内存页】


SectionTable (区块表,也成节表):

PE文件到内存的映射

 在执行一个PE文件的时候,windows并不在一开始就将整个文件读入内存的,而是采用与内存映射文件类似的机制。也就是说,windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。

当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。(需要才加载入内存)

但是要注意的是,系统装载可执行文件的方法又不完全等同于内存映射文件。

当使用内存映射文件的时候,如果将磁盘文件和内存映像比较的话,可以发现不管是数据本身还是数据之间的相对位置都是完全相同的。

总结:当磁盘文件一但被装入内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。


而我们知道,在装载可执行文件的时候,有些数据在装入前会被预处理,如重定位等,正因此,装入以后,数据之间的相对位置可能发生微妙的变化。

Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。

 

一般情况下,它会处理以下几个方面的内容:

内存页的属性;

节的偏移地址;

节的尺寸;

不进行映射的节。

 

内存页的属性:

对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的。但是在装载可执行文件时,与节对应的内存页属性要按照节的属性来设置。所以,在同属于一个模块的内存页中,从不同节映射过来的的内存页的属性是不同的。

 

节的偏移地址:

节的起始地址在磁盘文件中是按照 IMAGE_OPTIONAL_HEADER32 结构的 FileAlignment 字段的值进行对齐的。而当被加载到内存中时是按照同一结构中的 SectionAlignment 字段的值对其的,两者的值可能不同。所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。

 

注意,节事实上就是相同属性数据的组合!当节被装入到内存中的时候,相同一个节所对应的内存页都将被赋予相同的页属性。

事实上,Windows 系统对内存属性的设置是以页为单位进行的,所以节在内存中的对齐单位必须至少是一个页的大小。

(温馨提示:对于32位操作系统来说,这个值一般是4KB==1000H; 对于64位操作系统这个值一般是8KB==2000H)

 

在磁盘中排放是以空间为主导,在磁盘只是存放,不是使用,所以不用设置那么详细的属性。


区块表结构:

typedef struct _IMAGE_SECTION_HEADER{BYTE Name[IMAGE_SIZEOF_SHORT_NAME];   // 节表名称,如“.text” //IMAGE_SIZEOF_SHORT_NAME=8union{DWORD PhysicalAddress; // 物理地址DWORD VirtualSize;     // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个} Misc;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;

注意:每个区块的名称都是唯一的,不能有同名的两个区块。

各种区块的描述:

通常,区块中的数据在逻辑上是关联的。PE 文件一般至少都会有两个区块:一个是代码块,另一个是数据块。

每一个区块都需要有一个截然不同的名字,这个名字主要是用来表达区块的用途。

区块在映像中是按起始地址(RVA)来排列的,而不是按字母表顺序。


我们知道PE 文件中的数据被载入内存后根据不同页面属性被划分成很多区块(节),并有区块表(节表)的数据来描述这些区块

注意:一个区块中的数据仅仅只是由于属性相同而放在一起,并不一定是同一种用途的内容

例如输入表、输出表等就有可能和只读常量一起被放在同一个区块中,因为他们的属性都是可读不可写的。

 

其次,由于不同用途的数据有可能被放入同一个区块中,因此仅仅依靠区块表是无法确定和定位的

 

PE 文件头中IMAGE_OPTIONAL_DEADER32 结构的数据目录表来指出他们的位置。

可以由数据目录表来定位的数据包括输入表、输出表、资源、重定位表和TLS等15 种数据。(数据目录表)

 

由于我们从数据目录表得到的仅仅是一些指定数据的RVA 和数据块的尺寸。不同的数据块中的数据组织方式(结构)是显然不同的,例如输入表和资源数据块中的数据就完全是不相关的。因此,我们想要深入了解PE 文件就必须了解这些数据的组织方式,以及了解系统是如何处理调用它们的。所以,需要先了解输入表。

 

输入函数:在代码分析或编程中经常遇到“输入函数(Import Functions,也称导入函数)”的概念。

输入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于相关的DLL 文件中,在调用者程序中只保留相关的函数信息(如函数名、DLL 文件名等)就可以。

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

动态链接是通过PE 文件中定义的“输入表”来完成的,输入表中保存的正是函数名和其驻留的DLL 名等。

 

输入函数

注:MessageBox 是来自于USER32.DLL动态链接库里的一个函数,我们通过对PE 文件的静态反编译分析来观察软件是如何定位和调用MessageBox 这个函数。

根据查看输入表结构,我们可知:

之所以有两个并行的指针数组同时指向 IMAGE_IMPORT_BY_NAME 结构,是因为

由 OriginalFirstThunk 所指向是单独的一项,而且不能被改写,我们前边称为 INT

而由 FirstThunk 所指向的,事实上是由 PE 装载器重写的。