PE学习

发布时间 2023-07-12 16:21:23作者: 清雨中欣喜

1、主要结构体

主要结构体

DOS MZ文件头的内存大小为64个字节

DOS Stub的大小不确定,因为这段是给连接器用的,即使这段数据被删改也不影响运行

通过DOS MZ文件头尾到PE文件头的内存确定大小

DOS部分属于是历史遗留问题,用于DOS 操作系统与exe程序运行无关,只是保留在PE中

 

PE文件头由三个部分组成,PE标识(PE指纹)类型为DWORD 占四个字节、标准PE头和扩展PE头

标准PE头的大小为20个字节,扩展PE头是224个字节(32位)(主要原因是其中包括了一个结构体数组)

扩展PE头可以产生变化,其主要取决于结构体中的 WORD SizeOfOptionalHeader 变量

 

之后就是节表,其所占字节为40个字节。其中包含了几个节数据,在节数据之后的全都是编译器插入的其他数据(节数据数量不定,可以进行修改)

 

扩展PE头中有一个结构体变量是DWORD SizeOfHeaders 这个变量的数值代表着DOS 部分、PE文件头和节表在文件对齐后的大小。 文件对齐对应的变量是DWORD FileAlignment 在头之后,就是各个节数据

image-20220924232304583

扩展PE头中还有一个DWORD SectionAlignment 这个代表的内存对齐,一般来说与文件对齐数据不同。 两种状态不同,主要是指的是 头跟节,节跟节之间的距离不同,一般来说在内存中都是拉伸之后的。

 

为什么在文件中的对齐参数和在内存中不同?

内存对齐是为了提高内存利用效率,充分利用系统总线。 文件对齐是为了提高解析的效率

 

为什么PE文件在设计的时候要分块?

分块的一个原因是节省硬盘,而分块的另一个目的是节省内存空间。

 

一个PE文件加载进内存中可能大于在硬盘上的大小,并且无论是在内存中还是硬盘上,都是是分块管理(分节),一块和一块存储空间之间是空隙。在硬盘上空隙有可能小于内存中空隙;在内存中空隙较大(相较于硬盘)。而存在间隙的原因则是分块管理。 分块的一个原因是节省硬盘:比如notepad.exe,由于是早期的程序,当时硬盘容量比较小,编译器在生成可执行文件时,不仅要考虑效率问题使得内存对齐/文件对齐,还需要设计成节省硬盘空间的结构。所以这种结构遵循的对齐原则:内存对齐(1000H)和硬盘对齐(200H),对齐的补充数据(0X0000)便是间隙。硬盘的对齐值较小,补充间隙自然小,因此同一个可执行程序在内存中可能比在硬盘上大。但是现如今的硬盘空间更大,所以编译器生成的可执行程序在硬盘上与内存中对齐方式都是1000H。统一对齐为1000H的目的依旧是提高效率。 而分块的另一个目的是节省内存空间,比如同时在电脑上运行登录多个QQ账号,就需要运行多次QQ可执行程序。而代码段为只读数据需要一份即可,数据段则需要为每个账号均开辟一份,,多个QQ程序共享代码块,单独使用数据块,这样就节省了多份代码块的内存。(这些块是使用结构体来维护的,分块即创建结构体)。

2、DOS头属性说明

DOS部分由DOS块和DOS MZ文件头组成,DOS块不是结构体而是由单个字节组成的数据可以填写任何内容 ,而DOS MZ文件头是一个结构体 ,该结构体如下图组成

image-20220927190100513

但该结构体是运行在16位操作系统上的,如今不再使用

但有其中两个成员是例外 WORD e_magic和WORD e_lfanew

即为第一和最后一个成员,如果将他们中间的成员都删除也不影响程序的使用,主要是因为第一个成员是一个标识,其代表的是PE指纹其内容应该是MZ,操作系统以此来判断一个程序是否是PE,最后一个成员指明的了PE的位置 ,操作系统以这两个特征来标识是PE文件,而如果进行了修改,再去点击该程序就会出现下面的情况

image-20220927191349470

image-20220927190853685

DOS块中的数据是由连接器进行填写的,将其中的进行修改也不会影响运行

3、标准PE头属性说明

PE文件头(_IMAGE_NT HEADERS)部分结构体分为了三个部分分别是:

typedef struct_IMAGE_NT_HEADERS{
DWORD Signature;    //PE标识
IMAGE_FILE_HEADER FileHeader;    //标准PE头
IMAGE_OPTIONAL_HEADER32 OptionalHeader;   //扩展PE头
}IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

 

PE标识:PE标识不能破坏,操作系统在启动一个程序的时候会检测这个标识。

typedef struct_IMAGE_FILE_HEADER (
WORD Machine;
//可以运行在什么样的CPU上 任意: O Intel 386以及后续: 14C x64: 8664
WORD NumberOfSections;//表示节的数量
DWORD TimeDateStamp;//编译器填写的时间截与文件属性里面(创建时间、修改时间)无关
DWORD PointerToSymbolTable; //调试相关
DWORD NumberOfSymbols;//调试相关
WORD SizeOfOptionalHeader; //可选PE头的大小(32位PE文件: OxEO 64位PE文件: OxFO)
WORD Characteristics;//文件属性
}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

标准PE头的第一个成员是代表了可以运行在什么样的CPU上,即在PE标识后面的一个四个字节的数据,如果是0000,即可以运行在任意CPU上,如果是Intel386以及后续的是014C(包含X86),X64的是8664

第二个成员指的是节的数量(即节数据的个数)

第三个成员为四个字节,其中填写的是时间戳,即从1970.1.1 0时0分开始 ,每过一秒编译器就往里面添1。(其与文件创建时间和修改时间无关。我们也可以通过自己的需求进行修改

第四个和第五个是与调试相关的

第六个成员指的是扩展PE头的大小。如果是32位的PE文件,则该成员的数据是0xE0 64位的数据是0xF0

最后一个成员指的是文件属性,是两个字节,16位的数据

image-20220927220154867

十六位的数据其中每个位有其代表的含义,如上图所示。

其中第五个数据位的代表,应用程序可处理大于2GB的地址。我们知道的是在32位操作系统中,应用能够处理的一共是4GB的地址,其中高2G的地址由内核部分使用,低2G的地址是由应用程序使用。故由此可知,此数据位代表的是应用程序是64位的。

4、扩展PE头属性说明

typedef struct _IMAGE_NT_HEADERS{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
}IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

PE头在32位和64位是不同的,上述代码上面的是32位,下面的是64位,且不同的地方主要在于扩展PE头,主要差异是该结构体里面少了几个成员,同时部分成员由2个字节变为4个字节。

我们这里主要研究32位的扩展PE头,64位的大同小异

//大小: 32bit(0xE0) 64bit(0xF0)

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES   16

typedef struct _IMAGE_OPTIONAL_HEADER {

   WORD    Magic;                          //文件类型: 10bh为32位PE文件 / 20bh为64位PE文件   !!!!!****************!!!!!
   BYTE    MajorLinkerVersion;             //链接器(主)版本号 对执行没有任何影响
   BYTE    MinorLinkerVersion;             //链接器(次)版本号 对执行没有任何影响
   DWORD   SizeOfCode;                     //包含代码的节的总大小.文件对齐后的大小.编译器填的没用
   DWORD   SizeOfInitializedData;          //包含已初始化数据的节的总大小.文件对齐后的大小.编译器填的没用.
   DWORD   SizeOfUninitializedData;        //包含未初始化数据的节的总大小.文件对齐后的大小.编译器填的没用.(未初始化数据,在文件中不占用空间;但在被加载到内存后,PE加载程序会为这些数据分配适当大小的虚拟地址空间).
   DWORD   AddressOfEntryPoint;            //程序入口(RVA)           !!!!!****************!!!!!
   DWORD   BaseOfCode;                     //代码的节的基址(RVA).编译器填的没用(代码节起始的RVA,表示映像被加载进内存时代码节的开头相对于ImageBase的偏移地址,节的名称通常为".text")
   DWORD   BaseOfData;                     //数据的节的基址(RVA).编译器填的没用(数据节起始的RVA,表示映像被加载进内存时数据节的开头相对于ImageBase的偏移地址,节的名称通常为".data")
   DWORD   ImageBase;                      //内存镜像基址         !!!!!****************!!!!!
   DWORD   SectionAlignment;               //内存对齐大小 !!!!!****************!!!!!
   DWORD   FileAlignment;                  //文件对齐大小     !!!!!****************!!!!!
   WORD    MajorOperatingSystemVersion;    //标识操作系统主版本号
   WORD    MinorOperatingSystemVersion;    //标识操作系统次版本号
   WORD    MajorImageVersion;              //PE文件自身的主版本号
   WORD    MinorImageVersion;              //PE文件自身的次版本号
   WORD    MajorSubsystemVersion;          //运行所需子系统主版本号
   WORD    MinorSubsystemVersion;          //运行所需子系统次版本号
   DWORD   Win32VersionValue;              //子系统版本的值.必须为0,否则程序运行失败.
   DWORD   SizeOfImage;               //内存中整个PE文件的映射尺寸.可比实际的值大.必须是SectionAlignment的整数倍 !!!!!****************!!!!!
   DWORD   SizeOfHeaders;                  //所有头+节表按照文件对齐后的大小.
!!!!!****************!!!!!
   DWORD   CheckSum;                       //校验和.大多数PE文件该值为0.在内核模式的驱动程序和系统DLL中,该值则是必须存在且是正确的.在IMAGEHLP.DLL中函数CheckSumMappedFile就是用来计算文件头校验和的,对于整个PE文件也有一个校验函数MapFileAndCheckSum.
!!!!!****************!!!!!
   WORD    Subsystem;                      //文件子系统 驱动程序(1) 图形界面(2) 控制台/DLL(3)
   WORD    DllCharacteristics;             //文件特性.不是针对DLL文件的
   DWORD   SizeOfStackReserve;             //初始化时保留的栈大小.该字段默认值为0x100000(1MB),如果调用API函数CreateThread时,堆栈参数大小传入NULL,则创建出来的栈大小将是1MB.
   DWORD   SizeOfStackCommit;              //初始化时实际提交的栈大小.保证初始线程的栈实际占用内存空间的大小,它是被系统提交的.这些提交的栈不存在与交换文件里,而是在内存中.
   DWORD   SizeOfHeapReserve;              //初始化时保留的堆大小.用来保留给初始进程堆使用的虚拟内存,这个堆的句柄可以通过调用函数GetProcessHeap获得.每一个进程至少会有一个默认的进程堆,该堆在进程启动时被创建,而且在进程的生命期中不会被删除.默认值为1MB.
   DWORD   SizeOfHeapCommit;               //初始化时实践提交的堆大小.在进程初始化时设定的堆所占用的内存空间,默认值为PAGE_SIZE.
   DWORD   LoaderFlags;                    //调试相关
   DWORD   NumberOfRvaAndSizes;            //目录项数目,默认为10h.
   IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//结构数组 数组元素个数由IMAGE_NUMBEROF_DIRECTORY_ENTRIES定义
} IMAGE_OPTIONAL_HEADER32, * PIMAGE_OPTIONAL_HEADER32;

上面结构体即是32位的扩展PE头,其中的成员不必全部记忆,只用记忆注释中带有符号的 “ !!!!!****!!!!!”

第一个成员 WORD Magic; 这个成员是 识别该文件是32位还是64位的文件最为准确的方式,如果其中的数值是10B即为32位的文件,而如果其中的数值是20B就是64位的文件。

往后五个都是有关于链接器和编译器的,前两个分别是版本号,后三个是由编译器填写的一些统计数据,如果进行修改,其实对于运行也并不影响。

我们先来看这个DWORD ImageBase(内存镜像基址),因为每一个进程都有自己对应的4个G的虚拟内存,因为程序在硬盘中和内存中的状态不同,在内存中会根据文件对齐而展开,而这个ImageBase则决定了从哪里开始展开。

而在内存中包含了数据和程序,DWORD AddressOfEntryPoint则代表了该对应的程序是从哪里开始的,其代表了程序的入口。

然后,DWORD FileAlignmentDWORD SectionAlignment则已经在前面介绍过了,其代表了PE文件在内存和硬盘中的状态。

DWORD SizeOfImage这个成员代表了,PE文件在内存中映射的大小,一般来说都是比硬盘中的实际大小要大,因为内存对齐的原因,所以其大小需要是SectionAlignment的整数倍。DWORD SizeOfHeaders而这个就代表了是PE文件在硬盘中按照内存对齐后的大小。

DWORD CheckSum即为所有的文件内容加上文件长度,最后得到的一个四个字节的数据,以此来判断系统文件是否被修改。但是这样并不能保证文件是否真正未被修改,因为当我们修改文件的时候我们也会同时将该数据进行修改。

WORD DllCharacteristics则是文件特性,但不是针对DLL文件的

image-20220929172317824

因为是WORD的大小,所以是一个字节的大小,一个字节16位,每一位都有它对应的含义。

DWORD NumberOfRvaAndSizes代表了有多少个表,而这些表则代表了之后的结构体数组的长度。

其他的成员,就不过多赘述,因为重要性相较于上面的几个不是那么大。做简单的了解就可以了。