分析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。
图1
下面我们就从头开始了解PE文件格式。图2显示了用UltraEdit 9.0打开其自身所得到的内容的开头部分。首先解释一下名词image,后面的数据结构有很多都带有这个词,那么到底什么是image呢?为了准确,我引用微软计算机辞典中关于image的定义,“对硬盘或软盘中的全部或部分内容的拷贝或表现形式,一段内存或硬盘的区域,一个文件,一个程序或是数据。”在我们这里,简单的说,image就是PE文件的实体。后面我们会一直使用image这个词。
图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。这是汇编中的基础知识,在此提及一下。
图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头的数据结构。
图4
图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。默认为平台架构的页面
虽然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。
图1
下面我们就从头开始了解PE文件格式。图2显示了用UltraEdit 9.0打开其自身所得到的内容的开头部分。首先解释一下名词image,后面的数据结构有很多都带有这个词,那么到底什么是image呢?为了准确,我引用微软计算机辞典中关于image的定义,“对硬盘或软盘中的全部或部分内容的拷贝或表现形式,一段内存或硬盘的区域,一个文件,一个程序或是数据。”在我们这里,简单的说,image就是PE文件的实体。后面我们会一直使用image这个词。
图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。这是汇编中的基础知识,在此提及一下。
图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头的数据结构。
图4
图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。默认为平台架构的页面
上一篇: 华为P20 Lite真机谍照曝光:刘海屏+竖排双摄
下一篇: 快递小哥不要这么盟好吗