危险漫步博客
有时候,正是那些意想不到之人,成就了无人能成之事。
文章1443 浏览13839329

PE系列之详解PE文件结构

在PSDK的头文件Winnt.h中,包含了PE文件结构的定义格式,PE头文件分为32位和64位,64为的PE结构是对32位的PE结构为了扩展,这里主要讨论32位的PE文件结构,对于64为的PE文件结构大家可以自行查阅资料进行学习。

DOS头部详解IMAGE_DOS_HEADER

对于一个PE文件来说,最开始的位置就是一个DOS程序,DOS程序包含一个DOS头和一个DOS程序体,DOS头是用来装载DOS程序的,DOS程序也就是图4-1中的那个DOS存根,也就是说DOS头是用来装载DOS存根的,保留这部分内容是为了与DOS系统做兼容,一般在PE结构中的DOS存根,只会输出一个“This progran cannot be run in DOS mode”字符串。    虽然DOS头是为了装载DOS程序的,但是DOS头部中的一个字段保存着指向PE头部的位置,DOS头在Winnt.h头文件中被定义为IMAGE_DOS_HEADER,该结构体的定义如下:


在该结构体中,有两个字段需要注意,分别是第一个字段e_magic,和最后一个字段e_lfanew字段。

e_lfanew字段是一个DOS可执行文件的标识符,该字段占用两个字节,该位置保存着的字符是MZ,该标识符在Winnt.h头文件中有一个宏定义,定义如下所示:

e_lfanew字段中保存着PE文件的起始位置。

在VC下创建一个简单的win32application程序,用来学习PE结构。

程序代码如下:

由于创建的工程是win32APPlication的程序,而不是控制台的程序,因此主函数是Winmain(),而不是main()了,winmain()函数有4个参数,具体参数的含义请参考MSdn这里就不进行介绍了。

该程序的功能是弹出一个Messagebox对话框,使用Win32 release方式进行编译链接,并把编译好的程序用C32Asm打开,C32Asm是一个反汇编与十六进制编辑于一体的程序,其界面如图4-2所示。

在这里选择十六进制模式单选项,单击确定按钮,打开十六进制编辑模式,如图4-3所示

在途中可以看到,在文件偏移为0x00000000的位置处保存着两个字节的内容DX5A4D,用ASCII标识则是MZ,途中明明写着是4D5A,为什么说是0X5A4D,大家到上面看winnt.h头文件中定义的那个宏,也写着是0X5A4D,这是为什么呢?这就是在前面章节中讲解的字节顺序,高位保存在高地址,地址保存在低地址,这个概念是很重要的,希望大家能够掌握。

在途中0X00000000的位置处,就是E_lfanew字段,该字段保存着PE结构的起始位置,该字段的值为多少呢?保存的是0XC800000000吗?如果是这样就错了,CPU架构使用的是小尾方式,系统对数据的存放始终是高位存放高字节,低位存放低字节,因此该处保存的是0X000000C8,大家查看一下0X000000C8这个位置保存的内容是50 45 00 00.与之对应的是ASCII字符为PE\0\0,这里就是PE结构开始的位置。

PE\0\0和IMAGE_DOS_HEADER之间的内容就是DOS存根,就是一个没什么太大用处的DOS程序,由于这个程序本身没有什么利用的价值,因此这里就不做介绍了。选中DOS存根程序,也就是从0X40000000处一直到0x000000c7处的内容,然后单击右键选择填充命令,在弹出的填充数据的对话框中,选中使用十六进制填充单选钮,在其后的编辑框中输入00,单击OK按钮,该过程如图4-4、4-5所示。

把DOS存根部分填充完以后,单击工具栏上的保存按钮对修改内容进行保存,保存时会提示是否备份,选择是,这样文件就保存了,找到我们的文件,然后运行,对话框依旧弹出了,说明这里的内容是无关紧要的。

PE头部详解IMAGE_NT_HEADERS

DOS头是为了兼容DOS系统而遗留的,在DOS头中的最后一个字段给出了PE头的位置,PE头部是真正用来装载Win32程序的头部,PE头的定义为IMAGE_NT_HEADERS,该结构体包含了PE标识符,文件头与可选头这三部分,IMAGE_NT_HEADERS是一个宏,其定义如下:

该头分为32位与64位,看是否定义了_WIN64,我们只讨论32位的PE文件格式,因此来看一下IMAGE_NT_HEAER32的定义,该定义如下:

该结构体中的Singnature就是PE标识符,该标识符标识该文件是否是PE文件,该部分占4个字节,即50 45 00 00,该部分可以参看图4-3,这里在Winnth中有一个宏定义。定义如下。

该值非常重要,简单地判断一个文件是否是PE文件,首先要判断DOS头部的开始字节是否是MZ,然后通过DOS头部找到PE头,接着判断PE头部的前四个字节是否为PE\0\0,如果是的刷,则可以说明该文件是一个有效的PE文件。

在PE头中,除了IMAGE NT SIGNATURE以外,还有两个重要的结构体,分别是IMAEG FILE HEADER(文件头)和IMAGE OPTJONAL HEADER32(可选头)。这两个头在PE头中占据重要的位置,因此需要详细介绍这两个结构体。

IAMGE_FIEE_HEADER

该结构体是IAMGE_FIEE_HEADER中的第一个结构体,该结构紧接在PE标识符的后面,该结构体大小为20字节,起始位置为0x000000cc,结束位置为0x000000df,如图4-6所示。

该结构包含了PE文件的一些基础信息,其结构体的定义如下:

下面介绍一下该结构各字段。

(1)Machine:该字段是WORD类型,占用两个字节,该字段表示可执行文件的目标CPU类型,该字段的取值,如图4-7所示。

在图4-6中,Machine字段的值为“4C Ol”,即Ox0l4C,也就是支持Intel类型的CPU。

(2) blumberOfSection:该字段是WORD类型,占用两个字节。该字段表示PE文件的节区的个数。在图4-6中,该字段值为“03 00”,即为Ox0003,也就是说明该PE文件的节区有3个。

(3) TimeDataStamp:该字段表明文件是何时被创建的,这个值是自1970年1月1 日以来用格林威治时间计算的秒数。

(4)PointerToSymboITable:该字段很少被使用,这里不做介绍。

(5) NumberOfSymbols.该字段很少被使用.这里不做介绍。

(6)  SizeOf10ptionaIHeader:该字段为WORD类型,占用两个字节。该字段指定IMAGE._ OPTIO:NAL- HEADER结构的大小。在图4-6中,该字段的值为“E0 00”,即“OKOOEO”,也就是说  IMAGE__ OPTIONAL_ HEADER  的大小为OxEO。  注意,  在计算IMAGE_ OPTIONAL—HEADER的大小时应该从IMAGE_FILE_HEADER结构中的SizeOfjptionaIHeader获取,而不应该直接使用sizeof(IMAGE_OPTlONAL_HEADER)获取。

由该字段可以看出IMAGE OPTIOhIAL-- HEADER结构体的大小可能是会改变的。

( 7) Characteristics:该字段为WORD类型,占用两个字节。该字段指定文件的类型。该字段取值如图4-8所示。

从图4-6可知,该字段的值为“OF 0l”,即“Ox010F”。该值表示该文件运行的目标平台为32位平台,是一个可执行文件,且不存在重定位信息,行号信息和符号信息已从文件中移除。

IMAGE_OPTIONA_HEADER

IMAGE_OPTIONA_HEADER在几乎所有的参考书中都被称作可选头,虽然被称作可选头,但是该头不是一个可选头,而是一个必须存在的头,不可以没有,该头被称作可选头,认为在该头的数据目录数组中,有的数据目录项是可有可无的,这部分内容是可U选的,因此成为可选项,而我觉得如果称之为选项头是否会更好一点呢?不管如何称呼,只要大家能够理解该头是必须存在的,就可以了。

可选头紧挨着文件头,文件头的结束位置为0X000000DF,那么可选头的起始位置为0X000000E0,可选头的大小在文件头中给出,其大小为0X00E0字节,其结束位置为0X000000E0+0X00E0-1=0X000001BF,如图4-9所示。

可选头是文件头的一个补充,其中的字段除了对文件的一些定义外,还为操作系统提供了装载PE文件的相关定义,该头同样有32位与64位版本之分。IMAGE_OPTIONAL_HEADER是一个宏,该宏的定义如下:32位版本与64位版本的选择是根据是否定义了Win64而决定的,这里只讨论其32位的版本,IMAGE_optional_header32的定义如下:

该结构体的成员变量非常之多,为了能够很好的掌握该结构体,这里对该结构体的成员变量进行一一介绍。

(1)Magic:该成员变量指定了文件的状态类型,状态类型如图4-10所示。

(2) MajorLinkerVersion:主链接版本号。

(3)  MinorLinkerVersion:次链接版本号。

(4) SizeOfCode:代码节的大小,如果有多个代码节的话,就是它们的总和。该处是指所有包含口J执行属性的节的大小。

(5)SizeOflnitializedData:已初始化数据块的大小。

(6) SizeO砌T1initializedData:未初始化数据块的大小。

(7) AddressOfF,ntryPoint:程序执行的入口地址,该地址是一个相对虚拟地址,该地址简称EP。如粜加壳后,找到了该地址,就被称作了OEP。该地址指向的不是main(),也不是WinMainO的地址,该地址指向了运行库代码的地址。对于DLL这个值的意义小大,因为DLL甚至可以没有DIIMain0函数,没有DlIMain()是无法捕获DLL的4个消息的。

(8)  BaseOfl:ode:代码段的起始相对虚拟地址。

(9) BaseOfData:数据段的起始相对虚拟地址。

(10) ImageBase:文件被装入内存后的首选建议装载地址。对于EXE文件来说,通常情况下该地址就是装载地址;对于DLL来说,可能就不是其装入内存后的地址了。

(11) SectionAJignment;节被装入内存后的对齐值。节被映射到内存中需要对齐的单位。

通常情况下Oxl000,也就是4KB大小。Windows操作系统的内存分页一般为4KB。

( 12)  FileAlignment:节在文什中的对齐值。节在磁盘上是对齐单位。

( 13)  MajorOperatingSystemVersion:要求最低操作系统的主版本号。

(14)  MinorOperatingSystemVersion;要求最低操作系统的次版本号。

( 15) MajorlmageVersion:可执行文件的主版本号。

(16) MinorlmageVersion:可执行文件的次版本号。

(17)  MajorSussystemVersion:要求最低于系统的主版本号。

(18) MinorSubsystemVersiOn:要求最低子系统的次版本号。    (19) Win32VersionValue:该成员变量是被保留的。

(20)  SizeOflmage:可执行文件装入内存后的总大小。该大小按内存对齐方式对齐。

(21) SizeOfHeaders: PE头的大小,这个PF.头泛指DOS头、PE头、节表的总和大小。

(22) CheckSum:校验和。对于EXE文件通常为0.对于sYs文件则必须有一个校验和。

(23) Subsystem:可执行文件的子系统类型。该值如图4-11所示。

(24)DLLCHARACTERISTIES:指定DLL文件的属性,该值大部分时候为0

(25)SizeOfStackreserve:为线程保留的栈大小

(26)sizeofstackcommit:为线程已经提交的栈大小

(27)sizeofheapreserve:为线程保留的堆大小

(28)sizeofheapcommit:为线程已经提交的堆大小

(29)loaderflags:被废弃的成员值,MS电脑上的原话为fhismernber is obsolete,但是该值在某些情况下还是会被用到的,比如针对旧版OD时,修改该值会起到反调试的作用。

(30)Number0frvaandsizes:数据目录项的个数,该个数在PSdk中有一个宏定义,定义如下:

(30)DataDirectory:数据目录表,由Nunberofrvaandsize个IMAGE_DATA_DIRECTORY结构体组成RVAIMAGE_DATA_DIRECTORY的定义如下:

该结构体的第一个变量为该目录的相对虚拟地址的起始值,第二个是该目录的长度,数据目录中的部分成员在数组中的索引如图4-12所示

在数据目录中,并不是所有的目录项都会有值,有很多目录项的值都为0

这个可选头的结构体就介绍完了,希望大家按照对结构体中各成员变量的掌握自行学习可选头中的十六进制的字段,这样有助于我们对PE文件格式的分析,加快对PE文件文件格式的掌握。

节区详细IMAGE_SECTION_HEADER

节表的位置IMAGE_OPTIONAL_HEADER的后面,节表中的每个IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信息,节的个数由IMAGE_FILE_HIEADER中的Numberofsections给出,希望大家没有忘记,该内容如图4-13所示。

IMAGE_SECTION_HEADER的结构体起始位置在0X000001C0处,结束位置在0X0000237处,IMAGE_SECTION_HEADER结构体的定义如下:

这个结构体相对于IMAGE_OPTIONAL_HEADER来说成员变量少很多,下面逐一进行介绍。

(1)Name:该成员变量保存节的名称,节的名称用ASCLL编码来保存,节名的长度为IMAGE_SIZEOF_SHORT_NAME,这是一个宏,该宏的定义如下:

节名的长度为8个字节,多余的字节会被自动截断,通常情况下节名以“.”为开始,当然这是编译器的习惯,我们看一下图4-13的前8个字节的内容为2E 74 65 78 74 00 00 00其对应ASCII字符为TEX

(2)Vinrtualsize:该值为数据实际的节区大小,该值不一定为对齐后的值。

(3)VitualAddress:该节区载入到内存后的相对虚拟地址,这个地址是按内存进行对齐的。

(4) SizeOiRawData:该节区在磁盘上的大小,该值通常是对齐后的值,但是也有例外。

(5) PointerOfRawData:该节区在磁盘文件上的偏移值。

(6) Characteristics:节区属性。

关于IMAGE SECTION_ HEADER的介绍就到这里了。

与PE结构相关的3种地址

下面介绍一下与PE结构相关的3种地址,分别是VA(虚拟地址)、RVA(相对虚拟地址)和FileOffset(文件偏移地址)。VA(虚拟地址):PE文件映射到内存后的地址。RVA(相对虚拟地址):相对虚拟地址是内存地址相对于映射基地址的偏移地址。

FileOffset(文件偏移地址):相对PE文件在磁盘上的文件开头的偏移量。

这3个地址都是和PE文件结构密切相关的,前面简单地引用过这几个地址,但是前面只是个概念。从了解节区开始,这3个地址的概念就非常重要了,否则后面的很多内容部将没法理解。

这3个概念之所以重要,是因为我们要不断地使用它们,而且三者之间的关系也很重要。每个地址之间的转换也很重要,尤其是VA和FileOffset的转换,还有RVA和EileOffset之间的转换。这两个转换不能说是复杂,但是需要一定的公式。而VA和RVA的转换就非常得简单与容易了。

PE文件在磁盘上和在内存中的结构是一样的。所不同的是,在磁盘上文件是按照IMAGE OPTIONAL__ HEADER的FileAhgnment僮进行对齐的,而在内存中,映像文件是按IMAGE_OPTINAL_HEADERSectionAlignment进行对齐的,这两个值前面已经介绍过了,这里在进行一个简单的回顾,FileAlingnment是按照磁盘上的扇区为单位的,也就是说FileAlingnment最小为512字节,十六进制的0X200字节,而Sectionalignment是按照内存分页为单位来对齐的,其值为4KB,也就是十六进制的0X1000字节,一般情况下,FileAlingnment的值会与SECtionalignment的值相同,这样磁盘文件和内存映像的结构是完全一样的,当FileAlignment的值和SECTIONALINMENT的值不相同的时候就存在一些细微的差异了,其差异的主要区别在于,根据对齐的实际情况而多填充了很多0值,PE文件映像。


3种地址的转换

当Filealingnment和sectionalignmenf的值不相同时候,磁盘文件与内存映像的同一数据在磁盘和内存中的偏移也不相同,这样两个偏移就发生了一个需要转换的问题,当你知道某数据的RVA的时候,想要在文件中读取同样的数据,就必须将RVA转换为Fileoffset,反之也是同样的情况。

下面用一个例子来学习如何进行转换,还记得前面为了分析PE文件结构而写的那个用MEssageBOX()输出hello world的例子程序吧?我们用PEDI打开它,查看他的节表情况,如图4-16所示。

从入4-16的标题栏可以看到,这里不叫节表,而叫区段,还要别的资料上称之为区块,这个只是叫法不同,内容都是一样的。

从图4-16中可以看到,节表的第一个节区的节名称为TEXT,通常情况下,第一个节区都是代码区,入口点也通常指向这个节区,在早期壳不流行时候,通过判断入口点是否在第一个节区来判断该程序是否为病毒,如今这种做法都不可靠了,我们关键要看的是R.偏移,这个表明了该节区在文件中的起始位置,PE头部,包括DOS头,PE头和节表,通常不会超过512个字节,也就是说,不会超过0X200的大小,如果这个R偏移为0x00001000,那么通常情况下可以确定该文件的磁盘对齐大小为0X1000,测试验证一下这个程序,看到V偏移与R偏移相同,则说明磁盘对齐与内存对齐是一样的,这样就没办法完成演示转换的工作了,不过,可以人为的修改磁盘对齐大小,也可以通过工具来修改磁盘对齐的大小,这里帮LORDPE来修改其大小,修改方法很简单,先将要修改的测试文件复制一份,以与修改后的文件做对比,打开LordPE,单击重建PE按钮,然后再选择刚才复制的那个测试文件,如图4-17、4-18所示。

PE重建功能中会压缩文件大小的功能,这里的压缩也就是修改磁盘文件的对齐值,避免过多的因对齐而进行补0,使其少占用磁盘空间,用PEID查看一下这个进行重建的PE文件的节表,如图4-19所示,现在可以看到V.偏移与R.偏移的值不相同了,他们的对齐值也不相同了,大家可以自己验证一下FileAlignment和SECTIONALINGNMENT的值是否相同。

现在我们有两个功能完全一样,而且PE结构也一样的两个文件了,唯一的不同就是其磁盘对齐大小不同,现在在这两个程序中分别寻找相同的数据,来学习不同地址之间的转换。

先用OD打开未进行重建PE的测试程序,找到MESSAGEBOX()处要弹出的两个对话框的地址,如图4-20、4-21所示。

从入4-20和图4-21中可以看到,字符串HELLO world的虚拟地址为0X000405030相对虚拟地址为VA减去IMAGEBASE,则rva=va—imagebase=0x00405030—0x00400000=0x0005030,由于SectionAlignment与fileAlignment的值相同,因此其SileOffset的值也为0X00005030,用于C32ASM打开该文件查看0X00005030处,如图4-22所示。

从这个例子中可以看出,当SECTIONALIGNMENT和FileAlignment相同时,统一数据的RVA和FILEOFFset相同,RVA的值是使用VA—imagebase得到的。

在次用OD打开重建PE后的测试程序,同样找到MEssagebox()函数使用的那个字符串,FielloWorld,看其虚拟地址是多少,可告诉大家,他的虚拟的地址仍然是0X00405030,虚拟地址是0X00405030,那么同样的用虚拟地址减去装载地址,相对地址的值仍然为0X00005030,用C32ASM打开该文件,看一下0X00005030地址处的内容,如图4-23所示。

从图中可以看到,用C32ASM打开该文件后,文件的末尾偏移为000379C,根本没有0X00005030这个偏移地址,这就是文件对其与内存对其的差异而引起的,这个时候就要通过一些简单的计算把RVA转换FILEOFFSET。把RVA转换为FILEOFFSET的方法很简单,首先要看一下当前的RVA或者是FILEOFFSET属于哪个节,0x00005030这个RAV属于.DATA节。0X00005030这个RAV相对于该节的起始RVA地址0X00005000来说偏移0X30个字节,在看.DATA节在文件中的起始位置为0X0003400.以.DATA的文件起始偏移0X00003400加上0X30个字节的值为0X00003430,用CS2ASM看一下0X00003430这个地址处的内容。如图4-24所示,

从图4-24可以看出,该文件偏移处保存着HELLO WORLD字符串,也就是说将RVA转换为FILEOFFSET是正确的,通过LORDPE工具来验证一下,如图4-25所示。

我们再来回顾一下这个过程。

某数据的文件偏移=该数据所在节的其实文件偏移+(某数据的RVA—该数据所在节的起始RVA)。

除了上面的计算方法以外,还有一种计算方法,把节的起始RVA值减去节的起始文件偏移值,得到一个差值,然后再用RVA减去这个得到的差值就可以得到其所对应的FILEOFFSET了,大家可以对这种方法自行验证。

这三种地址相互的转换方法就介绍完了,如果没有理解没那么就反复的按照公式进行学习和计算吧,只要在头脑中建立了关于磁盘文件和内存映像的结构,那么理解起来就不会太吃力了,在后面的例子当中,将会写一个类似LordPE中那样转换3中地址从程序,来加强理解。