欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  网络运营

分析UltraEdit学习PE文件格式

程序员文章站 2022-04-22 10:17:37
作者: Rock虽然PE文件格式不是一个新话题,但相信许多新近踏入密界的菜鸟们仍然需要这一方面的知识。对PE文件格式的深入了解不论是对于加壳、脱壳,还是对编程以及对整个系统的更深刻理解都有非常...
作者: Rock
虽然PE文件格式不是一个新话题,但相信许多新近踏入密界的菜鸟们仍然需要这一方面的知识。对PE文件格式的深入了解不论是对于加壳、脱壳,还是对编程以及对整个系统的更深刻理解都有非常重要的意义。之前已经有很多高手写译了不少PE文章,但大多面面俱到或者过于理论,对菜鸟来说理解起来可能有些困难。本文将以分析UltraEdit为例,以微软最新的PE文件规格作参考,来帮助广大菜鸟们通过实例了解并掌握PE文件格式。
PE文件格式的全称是Portable Executable File Format,是微软设计用于各种不同平台(x86、alpha等等)上的不同版本的Windows操作系统的可执行文件格式,这就是Portable(可移植)名字的由来。在VisualStudio目录里的“VCPlatformSDKIncludewinnt.h”文件中部有一个image format部分,包含了PE格式的数据结构,后面介绍PE文件格式的各个部分时会引用这些结构。图1是大师Matt Pietrek制作的PE文件结构的概览图,可以让我们大体上对PE的整体结构有个了解,这时不必追求细节,后面我们会介绍每一个部分。关于此图需要注意,位移0在最下面,表示此图是地址从高到低显示的;Sections部分取决于程序的需要,可能与图示不同也可能相同,比如我们将要介绍的UltraEdit就有.text、.rdata、.data、.rsrc四个Sections。
分析UltraEdit学习PE文件格式
图1
下面我们就从头开始了解PE文件格式。图2显示了用UltraEdit 9.0打开其自身所得到的内容的开头部分。首先解释一下名词image,后面的数据结构有很多都带有这个词,那么到底什么是image呢?为了准确,我引用微软计算机辞典中关于image的定义,“对硬盘或软盘中的全部或部分内容的拷贝或表现形式,一段内存或硬盘的区域,一个文件,一个程序或是数据。”在我们这里,简单的说,image就是PE文件的实体。后面我们会一直使用image这个词。
分析UltraEdit学习PE文件格式
图2
从图2可以看到文件是以ASCII字符‘MZ’开始的。‘MZ’是DOS头的标志,表示文件开头这些字节是一个DOS头。严格来说,DOS头并不是PE文件格式的一部分,但是几乎所有的PE文件都是以DOS头开始的。从文件起始到PE头之间的这个DOS头及其后面的一小段内容,我们叫做Dos stub。一个Dos stub其实就是一个DOS程序,其作用主要是为了在非Win32(比如纯DOS)环境下显示一个“This program cannot be run in DOS mode”的提示字符串(你可以在图2的右边看到这串字符)。
当执行程序时,加载到内存的image我们一般也称为Module,Module也是以DOS头起始。‘M’字符的地址就是基地址,可以用GetModuleHandle()函数来得到这个地址。我们可以将其类型转换为DOS头结构的指针,之后就可以寻访PE文件结构中的其它数据。图3显示了在winnt.h中的DOS头的结构IMAGE_DOS_HEADER。在图中,我将UltraEdit文件的内容一一对应放到结构中,这样可以让大家一目了然。请注意,这些内容我是原样拷贝而来。因为Intel体系对数据的存放是按字节的,低字节在低地址,高字节在高地址,所以那些成员的内容如果是数值或地址,那么应该将其颠倒过来才是真正的值。例如某个表示数值或者地址的成员的内容是以0x12345678的形式存在于文件中,其实它应该是数值0x78563412或者地址0x78563412。这是汇编中的基础知识,在此提及一下。
分析UltraEdit学习PE文件格式
图3
DOS头结构里的成员我们几乎不用去了解,唯一对我们有意义的是e_lfanew成员。它是真正的PE头(图1中的IMAGE_NT_HEADERS)的文件偏移地址。我们可以从图3中看到UltraEdit的IMAGE_DOS_HEADERS.e_lfanew的值为0x110(别忘了上面提过的低字节低地址高字节高地址原则),也就是说,从File Pointer0x110处开始就是IMAGE_NT_HEADERS,真正的PE头。图4显示了UltraEdit中的截图,图5显示了PE头的数据结构。
分析UltraEdit学习PE文件格式
图4
分析UltraEdit学习PE文件格式
图5
IMAGE_NT_HEADERS结构,也即PE头,包括三个部分,首先是一个DWORD的标志Signature,它的值总是50450000,也就是“PE ”;之后是IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER两个结构。从图5中我们可以看到,在32位环境下的NT头具体结构已经列出来,但如果是在64位环境下,即定义了_WIN64时,IMAGE_NT_HEADERS结构实际是IMAGE_NT_HEADERS64(PE32+)。IMAGE_NT_HEADERS64和IMAGE_NT_HEADERS32的区别不是很大,主要是BaseOfCode等几个成员从DWORD变成了ULONGLONG,以及去掉了BaseOfData成员,这里就不详细列出了,如果感兴趣可以自己到winnt.h结构中查看。
下面再介绍一个知识点,后面很多地方都需要,即什么是Base Address、RVA和VA。当我们执行程序时,在独立的4G虚拟地址空间中,我们的image不会被直接映射到地址0x00000000。在当前的Windows世界,我们的image一般会被映射到0x00400000,这个地址就是基地址(Base Address)。RVA是Relative Virtual Address(相对虚拟地址)的缩写,我们会看到很多结构里的地址都是RVA。RVA就是相对于基地址开始的偏移地址。VA即Virtual Address虚拟地址,就是4G地址空间中的地址。我们的UltraEdit实例被映射到的基地址为0x400000,Image开头的‘M’的RVA是0x00000000,其被映射到的地址为0x00400000,这个地址0x400000是‘M’的VA,也是image的基地址;‘Z’的RVA是0x00000001,它的VA是0x00400001。基地址、虚拟地址和RVA之间的关系为:相对虚拟地址RVA=虚拟地址VA-基地址Base Address。这几个地址的概念非常重要,后面的UltraEdit结构图展示了具体的地址值,希望能够帮大家进一步理解这些地址。
我们先来看IMAGE_FILE_HEADER结构,它保存着关于image的最基本信息。下面是每个成员的具体说明。
WORD Machine:该image是针对何种平台的标志,本文的值为14C,表示是Intel 386,可以在winnt.h中看到IMAGE_FILE_MACHINE_XXXX的列表,详细列出了每个可能值。
WORD NumberOfSections:image中Section的数量。该值也是紧随PE头之后的Section Tables数组的项数。Section可以看作是按功能或某些目的将文件分成的各个部分。这里的值为4,表示UltraEdit一共有4个Section,后面我们会详细介绍各个Section。
DWORD TimeDataStamp:连接程序产生该文件的时间。其值是从1970年1月1日零点开始所经过的秒数。
DWORD PointerToSymbolTable COFF:符号表的文件偏移地址,只对带有COFF 调试信息的文件有效,可以看到UltraEdit的值是0。
DWORD NumberOfSymbols COFF:符号表中的符号数,只对带有COFF调试信息的文件有效。
WORD SizeOfOptionalHeader:其后的IMAGE_OPTIONAL_HEADER结构的大小。这里的值为E0。
WORD Characteristics:一系列属性的标志位的组合,这里的值为0x010F,表示重定位信息、行号、局部符号已从文件中移出,image是32位编码,可执行文件。详细列表请看winnt.h文件中的# define IMAGE_FILE_XXXX信息。
第3部分是IMAGE_OPTIONAL_HEADER。虽然名字是optional(可选),但对PE文件来说其实是必不可少的。这个结构本身又可以分为3个部分,即图5中的Standard Fields、NT Additional Fields和最后的DataDirectory数组,下面分别列出了每个成员的说明。
Standard Fields的成员说明:
WORD Magic:一个标志,决定了该文件是PE32还是PE32+(64位)。这里的值为0x10B,表示是PE32;0x20B则表示PE32+。
BYTE MajorLinkerVersion:产生该文件的连接程序的主版本号,这里为6。
BYTE MinorLinkerVersion:产生该文件的连接程序的副版本号,这里为0。
DWORD SizeOfCode:文件中代码(.text)Section的大小,如果文件有多个代码Section,那么这个值是所有代码Section的总和。一般来说,文件中只有一个代码Section,就是.text Section,UltraEdit的情况就是如此。我们可以看到大小是0x12A000。后面介绍Section表的时候也会看到.text Section的大小和这里是一样的。
DWORD SizeOfInitializedData:已初始化数据Section的大小,或者当有多个这样的数据Section时,这些Section的大小之和。这里是0x0B2000。这个值实际上是加载进内存之后.rdata、.data和.rsrc3个section的大小之和。
DWORD SizeOfUninitializedData:未初始化数据Section(.bss)的大小,或者当有多个这样的数据Section时,这些Section的大小之和。这里是0。
DWORD AddressOfEntryPoint:程序进入点的RVA。加载程序载入程序之后将会从该RVA+基地址获得的地址处开始执行。这里的值为0x0E42D1。当基地址为0x400000时,程序进入点的虚拟地址为0x4E42D1。
DWORD BaseOfCode:代码section的RVA,即程序载入内存之后.text section相对于基地址的偏移。这里的值为0x1000,当基地址为0x400000时,0x401000处就是代码section。
DWORD BaseOfData:数据section的RVA。这里的值为0x12B000,当基地址为0x400000时,0x52B000处就是数据section。该成员在PE32+中被废弃。
NT Additional Fields的成员说明:
DWORD ImageBase:当程序加载进内存时被映射到的首选地址,即基地址。一般来说它的值是0x400000。
DWORD SectionAlignment:当加载进内存时,每个section必须从该值的整数倍值起始。这个值必须大于等于后面的FileAlignment。默认为平台架构的页面