· PE文件格式概述
· PE文件结构
· 如何获取PE文件中的OEP
· 如何获取PE文件中的资源
· 如何修改PE文件使其显示MessageBox的实例
2.1 引言
通常Windows下的EXE文件都采用PE格式。PE是英文Portable Executable的缩写,它是一种针对于微软Windows NT、Windows 95和Win32s系统,由微软公司设计的可执行的二进制文件(DLLs和执行程序)格式,目标文件和库文件通常也是这种格式。这种格式由TIS(Tool Interface Standard)委员会(Microsoft、Intel、Borland、Watcom、IBM等)在1993进行了标准化。显然,它参考了一些UNIXes和VMS的COFF(Common Object File Format)格式。
认识可执行文件的结构非常重要,在DOS下是这样,在Windows系统下更是如此。了解了这种结构后就可以对可执行程序进行加密、加壳和修改等,一些黑客也利用了这些技术。为了使读者对PE文件格式有进一步的认识,本章从一个程序员的角度出发再次介绍PE文件格式。如果已经熟悉这方面的知识,可以跳过这一章。
2.2 PE文件格式概述
认识PE文件,既要懂得它的结构布局,又要知道它是如何装载到计算机内存中的。下面分别对它们进行说明。
2.2.1 PE文件结构布局
找到文件中某一结构信息有两种定位方法。第一种是通过链表方法,对于这种方法,数据在文件的存放位置比较自由。第二种方法是采用紧凑或固定位置存放,这种方法要求数据结构大小固定,它在文件中的存放位置也相对固定。在PE文件结构中同时采用以上两种方法。
因为在PE文件头中的每个数据结构大小是固定的,因此能够编写计算程序来确定某一个PE文件中的某个参数值。在编写程序时,所用到的数据结构定义,包括数据结构中变量类型、变量位置和变量数组大小都必须采用Windows提供的原型。图2.1所示的PE文件结构的总体层次分布如下:
![](https://i-blog.csdnimg.cn/blog_migrate/2cb1c04707684e9b61e402c6d5336373.png)
PE文件结构总体层次分布
· DOS MZ Header
所有 PE文件(甚至32位的DLLs)必须以简单的DOS MZ header开始,它是一个IMAGE_DOS_HEADER结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ Header之后的DOS Stub。
· DOS Stub
DOS Stub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This program requires Windows”或者程序员可根据自己的意图实现完整的DOS代码。大多数情况下DOS Stub由汇编器/编译器自动生成。
· PE Header
紧接着DOS Stub的是PE Header。它是一个IMAGE_NT_HEADERS结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS MZ header中找到PE header的起始偏移量。因而跳过DOS Stub直接定位到真正的文件头 PE header。
· Section Table
PE Header之后是数组结构Section Table(节表)。如果PE文件里有5个节,那么此Section Table结构数组内就有5个(IMAGE_SECTION_HEADER)成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。排在节表中的最前面的第一个默认成员是text,即代码节头。通过遍历查找方法可以找到其他节表成员(节表头)。
· Sections
PE文件的真正内容划分成块,称为Sections(节)。每个标准节的名字均以圆点开头,但也可以不以圆点开头,节名的最大长度为8个字节。Sections是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,可以找到这些节。程序的代码,资源等就放在这些节中。
节的划分是基于各组数据的共同属性,而不是逻辑概念。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。节名称仅仅是个区别不同节的符号而已,类似“data”,“code”的命名只为了便于识别,唯有节的属性设置决定了节的特性和功能。
2.2.2 PE文件内存映射
在Windows系统下,当一个PE应用程序运行时,这个PE文件在磁盘中的数据结构布局和内存中的数据结构布局是一致的。系统在载入一个可执行程序时,首先是Windows装载器(又称PE装载器)把磁盘中的文件映射到进程的地址空间,它遍历PE文件并决定文件的哪一部分被映射。其方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被装入内存中,其某项的偏移地址可能与原始的偏移地址有所不同,但所表现的是一种从磁盘文件偏移到内存偏移的转换,如图2.2所示。
![](https://i-blog.csdnimg.cn/blog_migrate/796b8dcfd6cccb2df8ab679fb8ec1c79.png)
PE文件内存映射
当PE文件被加载到内存后,内存中的版本称为模块(Module),映射文件的起始地址称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为文件映像基址(ImageBase)。载入一个PE程序的主要步骤如下:
(1)当PE文件被执行时,PE装载器首先为进程分配一个4GB的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中0x400000的位置。装载一个应用程序的时间比一般人所设想的要少,因为装载一个PE文件并不是把这个文件一次性地从磁盘读到内存中,而是简单地做一个内存映射,映射一个大文件和映射一个小文件所花费的时间相差无几。当然,真正执行文件中的代码时,操作系统还是要把存在于磁盘上的虚拟内存中的代码交换到物理内存(RAM)中。但是,这种交换也不是把整个文件所占用的虚拟地址空间一次性地全部从磁盘交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页,也可能被交换到磁盘中。
(2)PE装载器在内核中创建进程对象和主线程对象以及其他内容。
(3)PE装载器搜索PE文件中的Import Table(引入表),装载应用程序所使用的动态链接库。对动态链接库的装载与对应用程序的装载方法完全类似。
(4)PE装载器执行PE文件首部所指定地址处的代码,开始执行应用程序主线程。
2.2.3 Big-endian和Little-endian
PE Header中IMAGE_FILE_HEADER的成员Machine 中的值,根据winnt.h中的定义,对于Intel CPU应该为0x014c。但是用十六进制编辑器打开PE文件时,看到这个WORD显示的却是4c 01。其实4c 01就是0x014c,只不过由于Intel CPU是Little-endian,所以显示出来是这样的。对于Big-endian和Little-endian,请看下面的例子。一个整型int变量,长度为4个字节。当这个整形变量的值为0x12345678时,对于Big-endian来说,显示的是{12,34,45,78},而对于Little-endian来说,显示的却是{78,45,34,12}。注意Intel使用的是Little-endian。
2.2.4 3种不同的地址
PE文件的各种结构中,涉及到很多地址、偏移。有些是指在文件中的偏移,有些 是指在内存中的偏移。以下的第一种是指在文件中的地址,第二、三种是指在内存中的地址。
第一种,文件中的地址。比如用十六进制编辑器打开PE文件,看到的地址(偏移)就是文件中的地址,使用某个结构的文件地址,就可以在文件中找到该结构。
第二种,当文件被整个映射到内存时,例如某些PE分析软件,把整个PE文件映射到内存中,这时是内存中的虚拟地址(VA)。如果知道在这个文件中某一个结构的内存地址的话,那么它等于这个PE文件被映射到内存的地址加上该结构在文件中的地址。
第三种,当执行PE时,PE文件会被载入器载入内存,这时经常需要的是RVA。例如知道一个结构的RVA,那么程序载入点加上RVA就可以得到该结构的内存地址。比如,如果PE文件装入虚拟地址(VA)空间的0x400000处,某一结构的RVA 为0x1000,那么其虚拟地址为0x401000。
PE文件格式要用到RVA,主要是为了减少PE装载器的负担。因为每个模块都有可能被重载到任何虚拟地址空间,如果让PE装载器修正每个重定位项,这肯定是个梦魇。相反,如果所有重定位项都使用RVA,那么PE装载器就不必操心那些东西了,即它只要将整个模块重定位到新的起始VA。这就像相对路径和绝对路径的概念:RVA类似相对路径,VA就像绝对路径。
注意,RVA和VA是指内存中,不是指文件中。是指相对于载入点的偏移而不是一个内存地址,只有RVA加上载入点的地址,才是一个实际的内存地址。