1500字范文,内容丰富有趣,写作好帮手!
1500字范文 > PE文件格式解析

PE文件格式解析

时间:2021-08-24 09:31:35

相关推荐

PE文件格式解析

最近工作需要用到PE特征,就了解了这方面的东西,搜了很多东西,发现这篇帖子很全面,再对照上PE文件格式的图片,这个帖子 我看了一个小时 ,算是对PE有了一定的了解。发现,PE文件好多东西都存在着里面。接下来是正贴 在这里很感谢 这篇帖子:/Bachelor/archive//07/24/3210748.html

PE文件格式分析

PE的意思是PortableExecutable(可移植的执行体)。它是Win32环境自身所带的执行文件格式。它的一些特性继承自Unix的Coff(commonobjectfileformat)文件格式。“PortableExecutable”(可移植的执行体)意味着此文件格式是跨Win32平台的;即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。

PE文件在文件系统中,与存贮在磁盘上的其它文件一样,都是二进制数据,对于操作系统来讲,可以认为是特定信息的一个载体,如果要让计算机系统执行某程序,则程序文件的载体必须符合某种特定的格式。要分析特定信息载体的格式,要求分析人员有数据分析、编码分析的能力。在Win32系统中,PE文件可以认为.exe、.dll、.sys、.scr类型的文件,这些文件在磁盘上存贮的格式都是有一定规律的。

PE文件格式总揽

Microsoft引入了PE文件格式,也就是大家都熟悉的PE格式,是Win32规范的一部分。然而,PE文件来源于更早的基于VAX/VMS的公共对象文件格式(COFF)。由于最初的WindowsNT小组成员很多都来自数字设备公司(DEC),于是很自然的这些开发者使用已存在的代码以加速新的WindowsNT平台的开发。

使用术语“可移植可执行”的目的是为了在所有Windows平台和所有支持的CPU上都有一个统一的文件格式。WindowsNT及其以后版本,Windows95及其以后版本和WindowsCE都使用了这个相同的格式,所以说在很大程度上,这个目的达到了。

Microsoft编译器生成的OBJ文件使用COFF格式。通过观察COFF格式的一些域你能知道它有多么老了,那些域使用八进制编码!COFFOBJ文件中有许多和PE文件一样的数据结构和枚举,随后我将提到它们中的一些。

对于64位的Windows,PE格式只是进行了很少的修改。这种新的格式被叫做PE32+。没有加入新的域,只有一个域被去除。剩下的改变只是一些域从32位扩展到了64位。在这种情况下,你能写出和32位与64位PE文件都能一起工作的代码。对于C++代码,Windows头文件的能力使这些改变很不明显。

EXE和DLL文件之间的不同完全是语义上的。它们都使用完全相同的PE格式。仅有的区别是用了一个单个的位来指出这个文件应该被作为EXE还是一个DLL。甚至DLL文件的扩展名也是不固定的,一些具有完全不同的扩展名的文件也是DLL,比如.OCX控件和控制面板程序(.CPL文件)。

PE文件一个方便的特点是磁盘上的数据结构和加载到内存中的数据结构是相同的。加载一个可执行文件到内存中(例如,通过调用LoadLibrary)主要就是映射一个PE文件中的几个确定的区域到地址空间中。因此,一个数据结构比如IMAGE_NT_HEADERS(稍后我将会解释)在磁盘上和在内存中是一样的。关键的一点是如果你知道怎么在一个PE文件中找到一些东西,当这个PE文件被加载到内存中后你几乎能找到相同的信息。

要注意到PE文件并不仅仅是被映射到内存中作为一个内存映射文件。代替的,Windows加载器分析这个PE文件并决定映射这个文件的哪些部分。当映射到内存中时文件中偏移位置较高的数据映射到较高的内存地址处。一个项目在磁盘文件中的偏移也许不同于它被加载到内存中时的偏移。然而,所有被表现出来的信息都允许你进行从磁盘文件偏移到内存偏移的转换(参见图1)。

图1偏移

通过Windows加载器加载PE文件到内存后,内存中的版本被称作一个模块。文件被映射到的起始地址称为HMODULE。有一点值得记住:得到一个HMODULE,你就知道那个地址处有些什么数据结构,并且你能找到内存中其它所有的数据结构。这是个很有用的功能,能被用做一些其它目的例如拦截API(WindowsCE下HMODULE和加载地址并不相同,这些以后再讲)。

内存中的模块描绘一个进程所需要的可执行文件的所有代码,数据,和资源。PE文件另一些部分只被读取,但不会被映射(例如重定位信息)。一些部分根本就不被映射,例如,文件末尾的调试信息。PE头中的一个域可以告诉系统映射一个可执行文件到内存中需要多少内存。不被映射的数据放在文件末尾,这些数据之前的部分将会被映射。

描述PE格式(以及COFF文件)的主要地方是在WINNT.H文件中。在这个头文件中,你可以找到要和PE文件一起工作所必须的每个结构定义,枚举,和#define定义。当然,其它地方也有相关文档。例如,MSDN中有“MicrosoftPortableExecutableandCommonObjectFileFormatSpecification”这篇文章。但WINNT.H文件最终决定了PE文件的格式。

有很多检查PE文件的工具。在它们之中有包含于VisualStudio中的Dumpbin,和包含于PlatformSDK的Depends。我比较喜欢Depends因为它有一个检查一个文件的导入表和导出表的简洁的方式。Smidgeonsoft()的PEBrowse专业版是一个很优秀的免费的PE观察器。这篇文章中包括的PEDUMP程序功能也很全面,实现了几乎Dumpbin的所有功能。

从API的角度来说,Microsoft的IMAGEHLP.DLL提供了读取和编辑PE文件的机制。

在我开始讨论PE文件的详细内容之前,让我们首先回顾几个基本概念,这些概念贯穿于整个PE文件格式。下面,我将讨论PE文件的节,相对虚拟地址(RVAs),数据目录,和导入函数的方法。

PE文件的节

PE文件节包含了代码或某种数据。代码就是程序中的可执行代码,而数据却有很多种。除了可读写的程序数据(例如全局变量)之外,节中的其它类型的数据包括导入和导出表,资源,和重定位表。每个节在内存中都有它自己的属性,包括这个节是否含有代码,它是只读的还是可写的,这个节中的数据是否可在多个进程之间共享。

一般而言,一个节中所有的代码和数据都通过一些方法逻辑地联系起来。一个PE文件中通常至少有两个节:一个代码节,一个数据节。一般地,在一个PE文件中至少有一个其它类型的数据节。在这篇文章的第二部分我将讨论这几种节。

每个节都有一个不同的名字。这个名字被用来意指节的作用。例如,一个叫做.rdata的节表示一个只读数据节。使用节名只是为了人们方便,对操作系统来说没有任何意义。一个命名为FOOBAR的节和一个命名为.text.的节一样有效。Microsoft通常以一个句点作为节名的前缀,但这不是必需的。多年来,Borland链接器就一直使用像CODE和DATA.这样的节名。

编译器有一组它们生成的标准的节,对于它们没有什么不可思议的东西。你可以创建并命名你自己的节,链接器很乐意在可执行文件中包括它们。在VisualC++中,你可以让编译器把代码或数据放到通过#pragma语句命名的节中。例如,下面这条语句

#pragmadata_seg("MY_DATA")

它会使VisualC++把它生成的所有数据放到一个命名为MY_DATA的节中,而不是缺省的.data节。大多数程序都使用编译器产生的默认节,但偶尔你也许会有把代码或数据放到一个单独的节中的需求。

节并不是全部由链接器生成的,它们其实存在于OBJ文件中,通常由编译器把它们放到那儿。链接器的工作是合并OBJ文件中所有必须的节并且最终放到PE文件相应节中。例如,你的工程中的每个OBJ文件都至少有一个包含代码的.text节。链接器合并这些OBJ文件中的.text节到一个PE文件中的单个的.text节中。同样地,这些OBJ文件中的叫做.data的节被合并到PE文件中一个单个的.data节中。.LIB文件中的代码和数据通常也被包含在可执行文件中,但那个主题已经超出本文的范围了。

链接器遵循一整套规则来决定哪些节该被合并以及如何合并。OBJ文件中的某个节也许是提供给链接器使用的,并不会放到最终的可执行文件中去。像这样的节是由编译器用来以传递信息给链接器。

节有两种对齐值,一个是在磁盘文件中的偏移另一个是在内存中的偏移。PE文件头指定了这两个对齐值,它们可以是不同的。每个节起始于那个对齐值的倍数的位置。例如,在PE文件中,典型的对齐值是0x200。因此,每个节开始于一个0x200的倍数的文件偏移处。

一旦加载到内存中,节总是起始于至少一个页边界。就是说,当一个PE节被映射到内存中后,每个节的第一个字节都符合一个内存页。对于x86CPUs,页是4KB,而IA-64,页是8KB。下面显示了PEDUMP输出的WindowsXPKERNEL32.DLL的.text节和.data节的一小部分。

节表

01.textVirtSize:00074658VirtAddr:00001000rawdataoffs:00000400rawdatasize:00074800...

02.dataVirtSize:000028CAVirtAddr:00076000rawdataoffs:00074C00rawdatasize:00002400

.text节在PE文件中的偏移为0x400,而在内存中位于KERNEL32加载地址之上第0x1000个字节处。同样的,.data节在PE文件中的偏移为0x74C00,而在内存中位于KERNEL32加载地址之上第0x76000个字节处。

创建一个节在文件中的偏移和在内存中的偏移相同的PE文件是可能的。这会使可执行文件变得很大,但在Windows9x或WindowsMe.下可以提高加载速度。缺省的/OPT:WIN98链接器选项(VisualStudio6.0引入)可以以这种方式创建PE文件。在VisualStudio®.NET中,也许会或者也许不会使用/OPT:NOWIN98,这依赖于文件是否足够小。

链接器的一个有趣的特点是可以合并节。如果两个节有类似的,兼容的特性,它们通常可以在链接时被合并到一个节中。这可通过/merge选项做到。例如,下面的链接器选项合并.rdata和.text节到一个单个的命名为.text的节中。

/MERGE:.rdata=.text

合并节的好处是可以节省磁盘文件和内存空间。每个节至少要占用一个内存页。如果你能把可执行文件中节的数量从4个减少到3个,你就可以少占用一个内存页。当然,这取决于这两个被合并的节的未使用空间是否达到一页。

对于合并节没有什么硬性的规定。例如,可以合并.rdata到.text中,但你不应该把.rsrc,.reloc,或者.pdata合并到其它节中。在之前,你可以合并.idata到其它节中。,,就不允放过样做了,但当链接一个发布版的时候,链接器经常合并.idata中的一部分到其它节中,例如.rdata。

既在一部分导入数据是当它们被加载到内存中时由加载器写入的,你也许很奇怪它们怎么能被写入一个只读内存节。这是因为在加载时系统临时把包含导入数据的页面的属性设为可读写。一旦导入表被初始化后,这些页被设置回它们最初的保护属性。

相对虚拟地址

在一个可执行文件中,有许多在内存中的地址必须被指定的位置。例如,当引用一个全局变量时就必须指定它的地址。PE文件可以被加载到进程地址空间的任何位置。虽然它们有一个首选加载地址,但你不能依赖于可执行文件真的会被加载到那个位置。因为这个原因,指定一个地址而不依赖于可执行文件的加载位置就很重要。

为了消除PE文件中对内存地址的硬编码,于是产生了RVA。一个RVA是在内存中相对于PE文件被加载的地址的一个偏移。例如,如果一个EXE文件被加载到地址0x400000,它的代码节位于地址0x401000处。那么代码节的RVA就是:

(目标地址)0x401000-(加载地址)0x400000=(RVA)0x1000.

要把一个RVA转换为实际地址,进行相反的步骤就行了:把RVA和实际加载地址相加就可得到实际内存地址。顺便说一下,实际内存地址在PE中被称为虚拟地址(VA)。另外也可以认为一个VA是加上首选加载地址的RVA。不要忘了我以前说过的,加载地址和HMODULE是一样的。

你是否想研究一下一些DLL在内存中的数据结构呢?这里有一个方法。以这个DLL的名字作为参数调用GetModuleHandle函数。返回的HMODULE是一个加载地址;你可以应用你的PE文件结构的知识找到这个模块中的任何你想要的东西。

数据目录

在可执行文件中有许多数据结构需要被快速定位。一些明显的例子是导入表,导出表,资源,和基址重定位表。所有这些众所周知的数据结构都可通过一致的方式被找到,就是数据目录。

数据目录是一个由16个结构组成的数组。每个数组元素都预定义了它所代表的含意。IMAGE_DIRECTORY_ENTRY_xxx定义了数据目录的数组索引(从0到15)。图2描述了每个IMAGE_DATA_DIRECTORY_xxx值分别表示了什么。这篇文章的第2部分包含了对其所指向的数据结构的更详细的描述。

图2IMAGE_DATA_DIRECTORY值

导入函数

当你使用其它DLL中的代码或数据时,就要导入它。加载一个PE文件时,Windows加载器的一个工作就是查找所有被导入的函数和数据并让那此函数和数据的地址可被加载的文件使用。完成这个工作所用到的数据结构的细节放到这篇文章的第二部分进行讨论,在这里学习一下这些概念。

当你直接调用到一个DLL的代码或数据时,你就是正在隐式地链接到这个DLL。要使被导入的API的地址可被你的代码使用你不需要做任何事情。加载器会完成所有需要做的工作。另外还有显式链接。意思就是说显式地加载目标DLL并查找API的地址。这几乎总是通过LoadLibrary和GetProcAddress来实现的。

当你隐式地链接一个API时,类似LoadLibrary和GetProcAddress的代码仍然被执行了,只不过是由加载器代替你自动执行的。加载器也会确保被加载的PE文件所需要的任何附加的DLL也被加载。例如,由VisualC++®链接器创建的每个正常的程序都要链接KERNEL32.DLL。而KERNEL32.DLL又从NTDLL.DLL导入函数。同样,如果你从GDI32.DLL导入函数,也将会依赖于USER32,ADVAPI32,NTDLL和KERNEL32DLL。加载器会保证这些DLL都被加载并且解决所有导入问题。(VisualBasic6.0和可执行文件直接链接到另外一个DLL而不是KERNEL32,但原理是相同的。)

隐式链接时,对主EXE文件和所有依赖的DLL的处理发生在程序第一次启动时。如果出现了任何问题(例如,一个被引用的DLL没有找到),进程将被终止。

VisualC++6.0引入了延迟加载的功能,它是隐式链接和显式链接的混合体。在延迟加载一个DLL时,链接器生成一些和正常导入一个DLL时非常相似的数据。然而,操作系统忽略这些数据。代替的,第一次调用一个延迟加载的API时,DLL才会被加载(如果还没有加载到内存中),然后调用GetProcAddress方法得到被调用API的地址。以后如果再调用这个API将会和这个API被正常导入时有着一样的效率。

在PE文件中,对于每个被导入的DLL都有一个数据结构的数组。这些结构给出被导入DLL的名称并指向一个函数指针数组。这个函数指针数组就是导入地址表(IAT)。每个被导入的API在IAT中都有它自己的位置,导入函数的地址由Windows加载器写入到那个位置中。最后一点非常重要:一旦一个模块被加载,IAT中包含所要调用导入函数的地址。

IAT的优点是在一个PE文件中只有一个地方保存了被导入API的地址。不管源文件中多少次调用一个API,都会通过IAT中同一个函数指针来完成。

让我们看一下怎样调用一个被导入的API。需要考虑两种情况:高效的和低效的。最好的情况,调用一个导入API看起来应该像下面这样:

CALLDWORDPTR[0x00405030]

这是通过函数指针进行调用。无论怎样,0x405030地址处的DWORD值就是这个CALL指令将把控制转移到的地址。在前面例子中,地址0x405030就位于IAT中。

低效的调用看起来像下面这样:

CALL0x0040100C

...

0x0040100C:

JMPDWORDPTR[0x00405030]

这种情况下,CALL把控制转到一个小的程序段处。这段程序通过JMP指令跳转到0x405030地址处。记住0x405030位于IAT中。低效调用导入函数用到了五个字节的额外代码,并且由于使用JMP指令花费了更长的执行时间。

你也许会奇怪为什么要使用低效的方法呢。有一个很好的解释。编译器无法区分导入函数调用和普通函数调用。因此,编译器生成同样形式的CALL指令

CALLXXXXXXXX

XXXXXXXX是一个稍后由链接器填充的实际地址。要注意这个CALL指令后面的地址并不是一个函数指针,而是一段实际代码的地址。链接器必须提供一块代码来替换这个XXXXXXXX。这样做的最简单的方法就是调用到一个JMPstub,就像你在上面看到的那样。

这个JMPstub从哪儿来呢?很令人惊奇,它来自于导入函数的导入库。如果你检查一个导入库,并且用导入API的名称来检查代码,你将会发现和上面JMPstub很相似的代码。这就是说缺省情况下将使用低效形式调用导入API。

那么,下一个要问的问题就是怎样才能得到优化的形式。答案是给编译器一个提示。__declspec(dllimport)函数修饰符告诉编译器这个函数位于其它DLL中,于是编译器将生成指令

CALLDWORDPTR[XXXXXXXX]

而不是:

CALLXXXXXXXX

另外,编译器也生成一些信息以告诉链接器把这个指令的函数指针部分解析为一个符号名__imp_functionname。例如,如果你正在调用MyFunction,符号名就是__imp_MyFunction。查看一个导入库,你会发现除了正常的符号名外,也有一个加了__imp__前缀的符号。__imp__symbol可以直接定位到IAT入口,而不是通过那个JMPstub。

那么这对你以后每天的生活有什么影响呢?如果你正在编写导出函数并为它们提供一个头文件,记住要使用这个__declspec(dllimport)修饰符:

__declspec(dllimport)voidFoo(void);

如果你查看Windows系统头文件,你会发现WindowsAPI都使用了__declspec(dllimport)。它并不容易被发现。你可在WINNT.H头文件中找到DECLSPEC_IMPORT宏定义,而这个宏被用在一些文件中例如WinBase.H。到这里你就会明白__declspec(dllimport)是如何被用在系统API声明上的。

我们知道,很多PE分析工具都可以查看一个EXE文件的引用DLL文件函数表,其实,这个本身就是存储在EXE头部的一个重要信息。

我们借用一张PE结构图来分析:

一个EXE完整的PE结构分五大部分。见上图.

MS-DOS头

最开头的是部分是DOS部首,DOS部首由两部分组成:DOS的MZ文件标志和DOSstub(DOS存根程序)。之所以设置DOS部首是微软为了兼容原有的DOS系统下的程序而设立的。

每个PE文件都以一个小的MS-DOS可执行体开头。在Windows早期很多消费者并没有安装Windows,所以就需要存在这个MS-DOS可执行体。当在没有安装Windows的机器上执行时,这段程序至少能打印一条信息来说明必须在Windows上才能执行这个可执行文件。

PE文件以一个传统的MS-DOS头开头,被称为IMAGE_DOS_HEADER。其中只有两个重要的值,它们是e_magic和e_lfanew。e_lfanew域包含PE头的文件偏移。e_magic域(一个WORD)必须被设为0x5A4D。对于这个值有个常量定义,叫做IMAGE_DOS_SIGNATURE。用ASCII字符表示,0x5A4D就是“MZ”,这是MS-DOS最初设计者之一MarkZbikowski名子的首字母大写。

DOSMZheader部分是DOS时代遗留的产物,是PE文件的一个遗传基因,一个Win32程序如果在DOS下也是可以执行,只是提示:“ThisprogramcannotberuninDOSmode.”然后就结束执行,提示执行者,这个程序要在Win32系统下执行。

DOSstub部分是DOS插桩代码,是DOS下的16位程序代码,只是为了显示上面的提示数据。这段代码是编译器在程序编译过程中自动添加的。

a.DOS头的数据结构

typedefstruct_IMAGE_DOS_HEADER{//DOS.EXEheader

WORDe_magic;//Magicnumber

WORDe_cblp;//Bytesonlastpageoffile

WORDe_cp;//Pagesinfile

WORDe_crlc;//Relocations

WORDe_cparhdr;//Sizeofheaderinparagraphs WORDe_minalloc;//Minimumextraparagraphsneeded WORDe_maxalloc;//Maximumextraparagraphsneeded WORDe_ss;//Initial(relative)SSvalue

WORDe_sp;//InitialSPvalue

WORDe_csum;//Checksum

WORDe_ip;//InitialIPvalue

WORDe_cs;//Initial(relative)CSvalue WORDe_lfarlc;//Fileaddressofrelocationtable

WORDe_ovno;//Overlaynumber

WORDe_res[4];//Reservedwords

WORDe_oemid;//OEMidentifier(fore_oeminfo) WORDe_oeminfo;//OEMinformation;e_oemidspecific

WORDe_res2[10];//Reservedwords

LONGe_lfanew;//Fileaddressofnewexeheader

}IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;

这个是winnt.h中定义的DOS头数据结构,对于运行在win32上的程序,我们需要关心的是e_lfanew成员, 因为它指向了PE结构相对于文件头的位置偏移,还有一个可能会用于简单判断文件类型的就是e_magic;它是IMAGE_DOS_SIGNATURE固定值'ZM'(0x5A4D)

b.PE文件结构

现在来让我们研究PE文件的实际格式。我将从文件的开头开始,并描述在每个PE文件中都会出现的数据结构。然后,我将描述在一个PE节中的更特殊的数据结构(例如导入表和资源)。下面我将讨论的所有数据结构都定义在WINNT.H中,除非另有说明。

通常,这些结构都有32位和64位之分---例如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64。这些结构除了一些域被扩展为64位外几乎是一样的。如果你正在试着编写可移植的代码,WINNT.H文件中有一些#defines定义可以用来选择使用32位还是64位的结构并且给它们起了一个与大小无关的别名(对于前面的例子这个别名就是IMAGE_NT_HEADERS)。具体选择哪一个结构依赖于你正在以哪种模式编译(是否定义了_WIN64)。只有在PE文件的目标执行平台的大小属性与正在编译的平台的大小属性不同时才需要使用特定的32位或64位版本的结构。

PE头信息

IMAGE_NT_HEADERS结构是存储PE文件细节信息的主要位置。它的偏移由这个文件开头的IMAGE_DOS_HEADER的e_lfanew域给出。实际上有两个版本的IMAGE_NT_HEADER结构,一个用于32位可执行文件,另一个用于64位版本。它们之间的区别很小,在讨论中我将认为它们是相同的。区别这两种格式的唯一正确的、由Microsoft认可的方法是通过IMAGE_OPTIONAL_HEADER结构(马上就会讲到)的Magic域的值。 typedefstruct_IMAGE_NT_HEADERS{

DWORDSignature;

IMAGE_FILE_HEADERFileHeader;

IMAGE_OPTIONAL_HEADER32OptionalHeader;

}IMAGE_NT_HEADERS32,*PIMAGE_NT_HEADERS32;

在一个有效的PE文件中,Signature字段的值是0x00004550,用ASCII表示就是“PE00”。#defineIMAGE_NT_SIGNATURE定义了这个值。第二个域是一个IMAGE_FILE_HEADER类型的结构,它包含了关于这个文件的一些基本的信息,最重要的是其中一个域指出了其后的可选数据的大小。在PE文件中,这个可选数据是必须的,但仍然被称为IMAGE_OPTIONAL_HEADER。

图3显示了IMAGE_FILE_HEADER结构的域以及对这些域的注释。这个结构在COFF格式的OBJ文件开头也可以找到。

图4列出了IMAGE_FILE_xxx通常的取值。

图5显示了IMAGE_OPTIONAL_HEADER结构的成员。

IMAGE_OPTIONAL_HEADER结构末尾的数据目录数组用来定位可执行文件中的重要数据的地址。每个数据目录条目看起来就像下面这样:

typedefstruct_IMAGE_DATA_DIRECTORY{

DWORDVirtualAddress;//RVAofthedata

DWORDSize;//Sizeofthedata

};

文件头信息

值得注意的是PE文件头中的IMAGE_OPTIONAL_HEADER32是一个非常重要的结构,PE文件中的导入表、导出表、资源、重定位表等数据的位置和长度都保存在这个结构里。

IMAGE_FILE_HEADER这个结构的定义如下:

图3IMAGE_FILE_HEADER

01.typedefstruct_IMAGE_FILE_HEADER{

02.00h WORDMachine;//运行平台

03.02h WORDNumberOfSections;//区块数目pe文件中区块的数量.

04.06h DWORDTimeDateStamp;//文件日期时间戳,指这个pe文件生成的时间,它的值是从1969年12月31日16:00:00以来的秒数. 05.0Ah DWORDPointerToSymbolTable;//指向符号表Coff调试符号表的偏移地址.

06.0Eh DWORDNumberOfSymbols;//符号表中的符号数量Coff符号表中符号的个数.这个域和前个域在release版本的程序里是0.

07.12h WORDSizeOfOptionalHeader;//映像可选头结构的大小IMAGE_OPTIONAL_HEADER32结构的大小(即多少字节).我们接着就要提到这个结构了.事实上,pe文件的大部分重要的域都在IMAGE_OPTIONAL_HEADER结构里.

08.14hWORDCharacteristics;//文件特征值

}IMAGE_FILE_HEADER,*PIMAGE_FILE_HEADER;

这个结构体表明一个PE文件的基本特征属性,也是一个PE文件的入口

Machine域说明这个pe文件在什么CPU上运行,具体如下:

#defineIMAGE_FILE_MACHINE_UNKNOWN0

#defineIMAGE_FILE_MACHINE_I3860x014c//Intel386.

#defineIMAGE_FILE_MACHINE_R30000x0162//MIPSlittle-endian,0x160big-endian #defineIMAGE_FILE_MACHINE_R40000x0166//MIPSlittle-endian

#defineIMAGE_FILE_MACHINE_R100000x0168//MIPSlittle-endian

#defineIMAGE_FILE_MACHINE_WCEMIPSV20x0169//MIPSlittle-endianWCEv2

#defineIMAGE_FILE_MACHINE_ALPHA0x0184//Alpha_AXP

#defineIMAGE_FILE_MACHINE_POWERPC0x01F0//IBMPowerPCLittle-Endian

#defineIMAGE_FILE_MACHINE_SH30x01a2//SH3little-endian

#defineIMAGE_FILE_MACHINE_SH3E0x01a4//SH3Elittle-endian

#defineIMAGE_FILE_MACHINE_SH40x01a6//SH4little-endian

#defineIMAGE_FILE_MACHINE_ARM0x01c0//ARMLittle-Endian

#defineIMAGE_FILE_MACHINE_THUMB0x01c2

#defineIMAGE_FILE_MACHINE_IA640x0200//Intel64

#defineIMAGE_FILE_MACHINE_MIPS160x0266//MIPS

#defineIMAGE_FILE_MACHINE_MIPSFPU0x0366//MIPS

#defineIMAGE_FILE_MACHINE_MIPSFPU160x0466//MIPS

#defineIMAGE_FILE_MACHINE_ALPHA640x0284//ALPHA64

#defineIMAGE_FILE_MACHINE_AXP64IMAGE_FILE_MACHINE_ALPHA64

Characteristics这个域描述pe文件的一些属性信息,比如是否可执行,是否是一个动态连接库等.具体定义如下:

图4IMAGE_FILE_XXX

#defineIMAGE_FILE_RELOCS_STRIPPED0x0001//重定位信息被移除 #defineIMAGE_FILE_EXECUTABLE_IMAGE0x0002//文件可执行 #defineIMAGE_FILE_LINE_NUMS_STRIPPED0x0004//行号被移除 #defineIMAGE_FILE_LOCAL_SYMS_STRIPPED0x0008//符号被移除 #defineIMAGE_FILE_AGGRESIVE_WS_TRIM0x0010//Agressivelytrimworkingset #defineIMAGE_FILE_LARGE_ADDRESS_AWARE0x0020//程序能处理大于2G的地址 #defineIMAGE_FILE_BYTES_REVERSED_LO0x0080//Bytesofmachinewordarereversed. #defineIMAGE_FILE_32BIT_MACHINE0x0100//32位机器 #defineIMAGE_FILE_DEBUG_STRIPPED0x0200//.dbg文件的调试信息被移除 #defineIMAGE_FILE_REMOVABLE_RUN_FROM_SWAP0x0400//如果在移动介质中,拷到交换文件中运行 #defineIMAGE_FILE_NET_RUN_FROM_SWAP0x0800//如果在网络中,拷到交换文件中运行 #defineIMAGE_FILE_SYSTEM0x1000//系统文件 #defineIMAGE_FILE_DLL0x2000//文件是一个dll #defineIMAGE_FILE_UP_SYSTEM_ONLY0x4000//文件只能运行在单处理器上 #defineIMAGE_FILE_BYTES_REVERSED_HI0x8000//Bytesofmachinewordarereversed.

所以,根据这个结构体的信息,我们就可以判断一个文件究竟是不是一个真正的PE文件,该PE文件的类型是可执行的还是可调用的(DLL)

我们可以写个简单的小程序来读取这个信息:

#include"stdafx.h"

#include"windows.h"

#include"stdio.h"

#include"conio.h"

intmain(intargc,char*argv[])

{

FILE*p;

LONGe_lfanew;//指向IMAGE_NT_HEADERS32结构在文件中的偏移

IMAGE_FILE_HEADERmyfileheader;

p=fopen("test1.exe","r+b");//自定义读取的exe文件

if(p==NULL)return-1;//如果打开失败就返回

fseek(p,0x3c,SEEK_SET);//注意这里是指针偏移,也就是绕过开头的DOS区块

fread(&e_lfanew,4,1,p);

fseek(p,e_lfanew+4,SEEK_SET);//指向IMAGE_FILE_HEADER结构的偏移

fread(&myfileheader,sizeof(myfileheader),1,p);

printf("IMAGE_FILE_HEADER结构:\n");

printf("Machine:%04X\n",myfileheader.Machine);

printf("NumberOfSections:%04X\n",myfileheader.NumberOfSections);

printf("TimeDateStamp:%08X\n",myfileheader.TimeDateStamp);

printf("PointerToSymbolTable:%08X\n",myfileheader.PointerToSymbolTable);

printf("NumberOfSymbols:%08X\n",myfileheader.NumberOfSymbols);

printf("SizeOfOptionalHeader:%04X\n",myfileheader.SizeOfOptionalHeader);

printf("Characteristics:%04X\n",myfileheader.Characteristics);

getch();

return0;

}

注释比较详细了,大家根据这个就可以读取一个PE文件的基本特征信息了.以上代码VC6编译通过

紧接着上一节,我们来研究下IMAGE_OPTIONAL_HEADER32,这个属于PE中附加结构信息,同样是很重要的。

我们先来看看它的结构:

图5IMAGE_OPTIONAL_HEADER

01.typedefstruct_IMAGE_OPTIONAL_HEADER{

//Standardfields.标准域

00hWORDMagic;//幻数,32位pe文件总为010bh32位pe文件总为010bh 这个常数的定义如下:

#defineIMAGE_NT_OPTIONAL_HDR32_MAGIC0x10b

#defineIMAGE_NT_OPTIONAL_HDR64_MAGIC0x20b

#defineIMAGE_ROM_OPTIONAL_HDR_MAGIC0x107

02h BYTEMajorLinkerVersion;//连接程序的主版本号如vc6.0的为06h 08.03hBYTEMinorLinkerVersion;//连接器副版本号如vc6.0的为00h

04h DWORDSizeOfCode;//代码段总大小pe文件代码段的大小.是FileAlignment的整数倍.

08h DWORDSizeOfInitializedData;//所有含已初始化数据的块的大小,一般在.data段中.

0ch DWORDSizeOfUninitializedData;//所有含未初始化数据的块的大小,一般在.bss段中

10h DWORDAddressOfEntryPoint;//程序执行入口地址(RVA)程序开始执行的地址,这是一个RVA(相对虚拟地址).对于exe文件,这里是启动代码;

//对于dll文件,这里是libMain()的地址. 在脱壳时第一件事就是找入口点,指的就是这个值.

14h DWORDBaseOfCode;//代码段起始地址(RVA)代码段基地址,微软的连接程序生成的程序一般把这个值置为1000h

18h DWORDBaseOfData;//数据段起始地址(RVA)数据段基地址

//NTadditionalfields.

1ch DWORDImageBase;//pe文件默认的装入起始地址.windows9x中exe文件为400000h,dll文件为10000000h

20h DWORDSectionAlignment;//内存中区块的对齐单位.区块总是对齐到这个值的整数倍.x86的32位系统上默认值位1000h

24h DWORDFileAlignment;//文件中区块的对齐单位;pe文件中默认值为200h.

28h WORDMajorOperatingSystemVersion;//所需操作系统主版本号

2ah WORDMinorOperatingSystemVersion;//所需操作系统副版本号上面两个域是指运行这个pe文件所需的操作系统的最低版本号.windows95/98和windowsnt4.0的内部版本号都是4.0,而windows2000的内部版本号是5.0

2ch WORDMajorImageVersion;//自定义主版本号

2eh WORDMinorImageVersion;//自定义副版本号上面两个域是指用户自定义的pe文件的版本号.可以通过连接程序来设置,如:LINK/VERSION:2.0MyApp.obj一般在升级时使用.

30h WORDMajorSubsystemVersion;//所需子系统主版本号

32h WORDMinorSubsystemVersion;//所需子系统副版本号上面两个域是指运行这个pe文件所要求的子系统的版本号.

34h DWORDWin32VersionValue;//总是0

38h DWORDSizeOfImage;//pe文件装入内存后映像的总大小.如果SectionAlignment域和FileAlignment域相等,那么这个值也是pe文件在硬盘上的大小.

3ch DWORDSizeOfHeaders;//从pe文件开始到节表(包含节表)的总大小.其后是各个区段的数据.

40h DWORDCheckSum;//pe文件CRC校验和

44h WORDSubsystem;//用户界面使用的子系统类型,见后面

46h WORDDllCharacteristics;//为0

48h DWORDSizeOfStackReserve;//为线程的栈初始保留的虚拟内存的默认大小,默认为00100000h.如果在调用CreateThread函数时指定

//堆栈的大小为0,被创建的线程的堆栈的初始大小就与这个值相同.

4ch DWORDSizeOfStackCommit;//为线程的栈初始提交的虚拟内存的大小.微软的连接程序把这个值置为1000h.

50h DWORDSizeOfHeapReserve;//为进程的堆保留的虚拟内存的大小.默认值为00100000h.

54h DWORDSizeOfHeapCommit;//为进程的堆初始提交的虚拟内存的大小微软的连接程序把这个值置为1000h.

58h DWORDLoaderFlags;//通常为0

5ch DWORDNumberOfRvaAndSizes;//数据目录结构数组的项数,总为00000010h这个值定义如下:#defineIMAGE_NUMBEROF_DIRECTORY_ENTRIES16

60hIMAGE_DATA_DIRECTORYDataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录结构数组

}IMAGE_OPTIONAL_HEADER32,*PIMAGE_OPTIONAL_HEADER32;

这个结构体非常的庞大,大家通过注释可以看出,这个结构体保存了相当全面的PE附件信息。

Subsystempe文件的用户界面使用的子系统类型.定义如下:

#defineIMAGE_SUBSYSTEM_UNKNOWN0//Unknownsubsystem.

#defineIMAGE_SUBSYSTEM_NATIVE1//Imagedoesn'trequireasubsystem. #defineIMAGE_SUBSYSTEM_WINDOWS_GUI2//ImagerunsintheWindowsGUIsubsystem. #defineIMAGE_SUBSYSTEM_WINDOWS_CUI3//ImagerunsintheWindowscharactersubsystem. #defineIMAGE_SUBSYSTEM_OS2_CUI5//imagerunsintheOS/2charactersubsystem. #defineIMAGE_SUBSYSTEM_POSIX_CUI7//imagerunsinthePosixcharactersubsystem. #defineIMAGE_SUBSYSTEM_NATIVE_WINDOWS8//imageisanativeWin9xdriver. #defineIMAGE_SUBSYSTEM_WINDOWS_CE_GUI9//ImagerunsintheWindowsCEsubsystem.

IMAGE_DATA_DIRECTORYDataDirectory[0x10]

数据目录结构数组

IMAGE_DATA_DIRECTORY结构定义如下:

1.typedefstruct_IMAGE_DATA_DIRECTORY{

2.DWORDVirtualAddress;//相对虚拟地址

3.DWORDSize;//大小

4.}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;

这个结构包含了pe文件中重要部分的RVA地址和大小.这个数组使操作系统的加载程序能够快速定位特定的区段.具体定义如下:

#defineIMAGE_DIRECTORY_ENTRY_EXPORT0//ExportDirectory

#defineIMAGE_DIRECTORY_ENTRY_IMPORT1//ImportDirectory

#defineIMAGE_DIRECTORY_ENTRY_RESOURCE2//ResourceDirectory

#defineIMAGE_DIRECTORY_ENTRY_EXCEPTION3//ExceptionDirectory

#defineIMAGE_DIRECTORY_ENTRY_SECURITY4//SecurityDirectory

#defineIMAGE_DIRECTORY_ENTRY_BASERELOC5//BaseRelocationTable

#defineIMAGE_DIRECTORY_ENTRY_DEBUG6//DebugDirectory

//IMAGE_DIRECTORY_ENTRY_COPYRIGHT7//(X86usage) #defineIMAGE_DIRECTORY_ENTRY_ARCHITECTURE7//ArchitectureSpecificData #defineIMAGE_DIRECTORY_ENTRY_GLOBALPTR8//RVAofGP

#defineIMAGE_DIRECTORY_ENTRY_TLS9//TLSDirectory

#defineIMAGE_DIRECTORY_ENTRY_LOAD_CONFIG10//LoadConfigurationDirectory #defineIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT11//BoundImportDirectoryinheaders #defineIMAGE_DIRECTORY_ENTRY_IAT12//ImportAddressTable #defineIMAGE_DIRECTORY_ENTRY_DELAY_IMPORT13//DelayLoadImportDescriptors #defineIMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR14//COMRuntimedescriptor

节表

IMAGE_NT_HEADERS之后紧跟着节表。节表是一个IMAGE_SECTION_HEADER结构数组。IMAGE_SECTION_HEADER提供了和它关联的节的信息,包括位置,长度和属性。图6描述了IMAGE_SECTION_HEADER结构的各域。在IMAGE_FILE_HEADER结构中的NumberOfSections域中提供了IMAGE_SECTION_HEADER结构的数目。

可执行文件中的节的文件对齐对最终的文件大小有很大的影响。在VisualStudio6.0中,链接器缺省的对齐大小为4KB,除非使用了/OPT:NOWIN98或/ALIGN选项。链接器也缺省使用了/OPT:WIN98选项,但它检测可执行文件的大小是否小于某个值,如果是则使用0x200字节进行对齐。

另一个值得注意的对齐方式来自.NET文件规范。它规定.NET可执行文件的内存对齐值是8KB,而不是x86平台的4KB。这是为了保证在x86平台上创建的可执行文件在IA-64平台上仍然可以运行。如果节的内存对齐值是4KB,IA-64加载器就不能加载这个文件,因为64位Windows的页大小是8KB。

图6IMAGE_SECTION_HEADER

c.Sections的目录

#defineIMAGE_SIZEOF_SHORT_NAME8

typedefstruct_IMAGE_SECTION_HEADER{

BYTEName[IMAGE_SIZEOF_SHORT_NAME];//标识字,表示是可执行镜像还是ROM镜像 union{

DWORDPhysicalAddress;

DWORDVirtualSize;

}Misc;//目标文件是重定位的地址,执行文件是镜像的大小

DWORDVirtualAddress;加载到内存后的相对地址

DWORDSizeOfRawData;Section原始数据的大小,FileAlignment对齐

DWORDPointerToRawData;原始数据的文件偏移

DWORDPointerToRelocations;重定位信息的数据位置

DWORDPointerToLinenumbers;行数据的位置

WORDNumberOfRelocations;重定向信息的数目

WORDNumberOfLinenumbers;行数据的项数

DWORDCharacteristics;Section的属性配置,详细见下面

}IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;

图7Flags

Characteristics它描述了这个Section的元属性;

IMAGE_SCN_CNT_CODE表示节的内容保护可执行代码

IMAGE_SCN_CNT_INITIALIZED_DATA含有已经初始化的数据

IMAGE_SCN_CNT_UNINITIALIZED_DATA含有未初始化数据,需要在加载时初始化为全0

IMAGE_SCN_LNK_INFO连接器信息,是目标文件的一部分

IMAGE_SCN_LNK_REMOVE连接后是否可以丢弃,对目标文件有效

IMAGE_SCN_LNK_COMDAT数据是连接通用数据

IMAGE_SCN_MEM_FARDATA(内存远程数据节)

IMAGE_SCN_MEM_PURGEABLE(内存可清除节),使用后可以把内存清除?还是加载后就可以清除?

IMAGE_SCN_MEM_LOCKED内存不能被移出?

IMAGE_SCN_MEM_PRELOAD内存需要预先加载?数据对齐方式,只用于目标文件

IMAGE_SCN_LNK_NRELOC_OVFL表示重定向的数目大于0xffff,真正的数据会保存在第一个relocationSection的VirtualAddress中? IMAGE_SCN_MEM_DISCARDABLE,如果有需要,该Section占有的内存可以被丢弃?意思是在加载成功后就可以丢弃吗?

IMAGE_SCN_MEM_NOT_CACHED,这节的内存不能被cache?是不是指每次都要重新从磁盘里读?这个东西会被修改?

IMAGE_SCN_MEM_NOT_PAGEDSection不能被页交换出内存

IMAGE_SCN_MEM_SHARED表示所有的实例都共享同一个内存镜像,对于DLL有效,这个只对数据Section有意义的,因为代码Section都是写拷贝(这里拷贝不应该被理解为有代码功能拷贝操作,只是里面的地址会被修正)的,因为在执行重定向的时候,会映射到不同的地址。 IMAGE_SCN_MEM_EXECUTESection的内容可以被执行

IMAGE_SCN_MEM_READ,可读

IMAGE_SCN_MEM_WRITE,可写

SectionHeaders是一个数组,但是绑定sectionheader后会马上跟着Section的内容。这个也是NumberOfSections存在的目的,因为这样才可以精确访问各个Section,而不能简单通过枚举,并当遇到全0的SectionHeader时停。

d.代码Section

i.IMAGE_OPTIONAL_HEADER32的BaseOfCode将指向这个Section的开始处。AddressOfEntryPoint则指向这个Section中中的某个位置这个Section的标志至少需要设置IMAGE_SCN_CNT_CODE,IMAGE_SCN_MEM_EXECUTE,IMAGE_SCN_MEM_READ这3个标志典型的Section名称:".text",".code","AUTO"

e.数据Section

已初始化的数据段,包括已初始化的全局数据和已经初始化的静态变量,这个Section的标志至少为; IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_WRITE,IMAGE_SCN_MEM_READ 已经初始化的数据Section可能有多个,但是它们都会在IMAGE_OPTIONAL_HEADER32所表示的 BaseOfData+SizeOfInitializedData范围内 典型的Section名称有:".data",".idata","DATA"

f.BSSSection

未初始化数据Section,包括没有初始化的全局变量和静态变量,这种Section的PointToRawData 是0,Section的标志变量包括IMAGE_SCN_CNT_UNINITIALIZED_DATA,IMAGE_SCN_MEM_WRITE,IMAGE_SCN_MEM_READ而整个数据长度由IMAGE_OPTIONAL_HEADER32的SizeOfUninitializedData表示,而且它的初始化需要由PELoader来完成。 典型的Section名称有:".bss","BSS" g.栈Section和堆Section并不保存在PE中,而是由PELoader根据IMAGE_OPTIONAL_HEADER32设置的堆栈大小创建

h.版权Section

目录结构的IMAGE_DIRECTORY_ENTRY_COPYRIGHT下标的内容是一个以ASCII的描述的字符串,通常它是通过参数的形式传给连接器的,这个串并不以0结尾的。这个Section是不能写的。

i.输入地址表Section

对于编译器而言发现对外部符号的调用时,它只会直接生成对那个符号的调用指令。但是连接器就需要为每个 输入的函数符号设置调用stub,这个stub就是跳转到目的地址,程序员可以通过"__declspec(dllimport)"来 避免生成stub,因为编译器会自己去计算,连接器就不要生成stub了。 stub的集合就是"转移区",通常它位于代码Section中,连接器并不知道这些地址的真正的值,它需要PEloader 在加载的时候修正。 转移区的结构:

_symbol:jmp[__imp__symbol]

_other_symbol:jmp[__imp__other__symbol]

而"__imp__symbol","__imp__other__symbol"这些符号的真正地址值是需要修正的,而且可以通过IMAGE_DIRECTORY_ENTRY_IAT从IMAGE_DATA_DIRECTORY获得。

输入地址转换表通常是由函数输出者如:DLL等提供给连接器使用的,事实上地址转换表并不是必须的,因为加载器可以通过加载者的输入符号表和依赖文件(DLL)输出符号表来修正。 地址转换表从概念上是输入导入输入目录的范畴,但它实际上是一个独立的Section。

j.输入符号表Section

输入符号Section的内部数据(当然,不会包含一个SectionHeader)是一个IMAGE_IMPORT_DESCRIPTOR数组

这个Section属性至少包括IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ

通过IMAGE_DIRECTORY_ENTRY_IMPORT可以得到第一个IMAGE_IMPORT_DESCRIPTOR的位置

typedefstruct_IMAGE_IMPORT_DESCRIPTOR{

union{

DWORDCharacteristics;//0forterminatingnullimportdescriptor

//原来的导入函数名数组数组首地址RVAORG

DWORDOriginalFirstThunk;//RVAtooriginalunboundIAT(PIMAGE_THUNK_DATA)数组的成员是IMAGE_THUNK_DATA结构

};

DWORDTimeDateStamp;//0ifnotbound,时间戳(作用比较复杂)1ifbound,andrealdate\timestamp //inIMAGE_DIRECTORY_ENTRY_BOUND_IMPORT(newBIND) //O.W.date/timestampofDLLboundto(OldBIND)

DWORDForwarderChain;//-1ifnoforwarders中转链这个数据一般为0,可以不关心

DWORDName;RVA,指向DLL名字的指针,ASCII字符串

DWORDFirstThunk;//RVAtoIAT(ifboundthisIAThasactualaddresses)函数转换后的地址,

//指向一个IMAGE_THUNK_DATA结构数组的RVA,这个数据与IAT所指向的地址一致

}IMAGE_IMPORT_DESCRIPTOR;

IMAGE_THUNK_DATA这是一个DWORD类型的集合。通常我们将其解释为指向一个IMAGE_IMPORT_BY_NAME结构的指针,

其定义如下:

IMAGE_THUNK_DATA{

union

{ PBYTEForwarderString;

PDWORDFunction;

DWORDOrdinal;//判定当前结构数据是不是以序号为输出的,如果是的话该值为0x800000000,此时PIMAGE_IMPORT_BY_NAME不可做为名称使用 PIMAGE_IMPORT_BY_NAMEAddressOfData;

}u1;

}IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;

typedefstruct_IMAGE_IMPORT_BY_NAME{

WORDHint;//函数输出序号导入的DLL的输出名字表的索引

BYTEName1[1];//输出函数名称BYTE0结尾的ASCII字符串(函数名)

}IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME

这里有两个指向导入函数信息的RVA数组ORG和TAR,使用方式是这样的,PELoader查造执行文件的导入符号表,优先通过导入DLL的函数索引导出表中查找函数,如果失败就使用名字查找,得到最终的转换地址后就将这个线性地址保存在一个"地方",然后把这个"地方"的地址填到TAR对应的IMAGE_THUNK_DATA中,这个地方就是地址转换表,但是并不是所有的连接器都会生成可以通过IMAGE_DIRECTORY_ENTRY_IAT访问。(如果没有IAT,那么只要将线性地址直接放到IMAGE_THUNK_DATA中)

这里需要有两个地方需要注意:

1.IMAGE_THUNK_DATA的值最高位为1时表示不包含导入函数的名字。也就是它不是指向IMAGE_IMPORT_BY_NAME的RVA,我们可以通过它的低地址的WORD得到序数

2."绑定"事实上就是限定导入的DLL的加载地址,然后就能在连接的时候设置TAR的值,PELoader就能节省时间;当DLL的版本不对,或者重定向必须发生时,ORG仍然提供足够的信息让PELoader来修正地址映射。

重定向的发生Loader是知道的,而DLL版本是通过时间戳来判断的,如果时间戳为0,表示没有绑定,如果非0,就需要和DLL里Header的时间戳对比,只有一致时,才不需要进行导入地址的修正。

3.对于中转的情况,也就是引用的DLL中导出了一个不在本身定义的符号,这时修正是必须发生的。

4.中转链的值是TAR的下标,它表示这个符号是中转的,而这TAR的内容就是下一个中转的下标,一直到(-1)表示没有中转了

k.新式绑定

它不是一个独立的Section,而是放到SectionHeaders后面,第一节之前,通过IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT可以转向到数据开头IMAGE_BOUND_IMPORT_DESCRIPTOR 所有导入符号的地址都已经被事先修正,而不管它是不是中转的。 typedefstruct_IMAGE_BOUND_IMPORT_DESCRIPTOR

{

DWORDTimeDateStamp;//时间戳,为-1是有效的,显示版本

WORDOffsetModuleName;//DLL 名称相对目录开头的偏移模块名称偏移

WORDNumberOfModuleForwarderRefs;//DLL中转的其他DLL的数目未使用ArrayofzeroormoreIMAGE_BOUND_FORWARDER_REF

//follows多个IMAGE_BOUND_FORWARDER_REF

}IMAGE_BOUND_IMPORT_DESCRIPTOR,*PIMAGE_BOUND_IMPORT_DESCRIPTOR;

从这种新的绑定方式来看,我不明白它能带来什么样的用处,如果只是强制所有的符号都进行重定位的话 那只需要强制PEloader完成地址修正就可以了。

它的作用感觉只是强调了绑定的信息,通过单独列出DLL 的"版本"来决定是否需要重新计算

l.输出符号表Section 输出符号表通常存在于DLL中,通过IMAGE_DIRECTORY_ENTRY_EXPORT可以直接得到数据的入口,它的属性至少包括IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ,不可丢弃,典型的段名称:".edata"

typedefstruct_IMAGE_EXPORT_DIRECTORY{

DWORDCharacteristics;DLL的特性,保留

DWORDTimeDateStamp;时间戳,不一定有效

WORDMajorVersion;主版本

WORDMinorVersion;次版本号

DWORDName;名字的RVA

DWORDBase;基址(就是导出函数的起始下标)

DWORDNumberOfFunctions;输出的函数数目

DWORDNumberOfNames;输出的名字的数目

DWORDAddressOfFunctions;//RVAfrombaseofimage输出的函数地址数组地址的RVA DWORDAddressOfNames;//RVAfrombaseofimage输出函数名字数组地址的RVA DWORDAddressOfNameOrdinals;//RVAfrombaseofimage函数名字对应的输出函数所在AddressOfFunction的下标 }IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

这里需要说明的是AddressOfFunction、AddressOfNames、AddressOfNameOrdinals的使用方法:

1.如果通过函数的编号来查找函数,那么首先通过(编号-Base)得到AddressOfFunction的下标这样就可以直接得到查找函数的RVA

2.如果通过函数名字查找函数,那么首先从AddressOfNamesz中查找对应的名字,如果找到,比如 在下标为10的为位置,那么就用10去索引AddrssOfNameOrdinals数组,从而得到查找函数在 AddressOfFunction中的位置,通过这个位置信息就能得到查找函数的RVA

m.资源Section

该Section至少包含IMAGE_SCN_CNT_INITIALIZED_DATA、IMAGE_SCN_MEM_READ标志。 可以通过IMAGE_RESOURCE_DIRECTORY_ENTRY索引IMAGE_DATA_DIRECTORY得到相应的RVA,资源结构 是通过IMAGE_RESOURCE_DIRECTORY来描述的 typedefstruct_IMAGE_RESOURCE_DIRECTORY{

DWORDCharacteristics;资源属性,保留

DWORDTimeDateStamp;时间戳,资源生成时间

WORDMajorVersion;

WORDMinorVersion;

WORDNumberOfNamedEntries;资源名称的数目

WORDNumberOfIdEntries;资源ID的数目 //IMAGE_RESOURCE_DIRECTORY_ENTRYDirectoryEntries[]; }IMAGE_RESOURCE_DIRECTORY,*PIMAGE_RESOURCE_DIRECTORY;

紧跟着的是IMAGE_RESOURCE_DIRECTORY_ENTRY

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {

union {

struct {

DWORD NameOffset:31;

DWORD NameIsString:1;

};

DWORD Name;

WORD Id;

};

union {

DWORD OffsetToData;

struct {

DWORD OffsetToDirectory:31;

DWORD DataIsDirectory:1;

};

};

} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

它由两个DWORD组成,它的含义分别如下:

1.第一个DWORD,如果他的最高位为1,那么表示剩下的31位表示一个相对于资源开始位置的偏移,偏移内容是IMAGE_RESOURCE_DIR_STRING,它标识这个IMAGE_RESOURCE_DIRECTORY_ENTRY;如果最 高位为0时,就表示这个DWORD的低16位(WORD)是表示IMAGE_RESOURCE_DIRECTORY_ENTRY的ID。

2.第二个DWORD,如果它的最高位是1,表示它还有下一层结构(也不是它本身不表表示资源内容),剩下的31位是相对于资源开始位置的偏移,偏移的内容是下一个IMAGE_RESOURCE_DIRECTORY_ENTRY如果最高位为0,表示没有下一层结构了,剩下的31位也是偏移,偏移的内容是 IMAGE_RESOURCE_DATA_ENTRY,这个结构会说明资源的具体信息。

(资源的开始位置实际上就是IMAGE_DATA_DIRECTORY[IMAGE_RESOURCE_DIRECTORY_ENTRY]) 通常我们会使用ID来表示资源,但也通过IMAGE_RESOURCE_DIR_STRING来表示

typedef struct _IMAGE_RESOURCE_DIR_STRING_U {

WORD Length; 资源名称的长度

WCHAR NameString[ 1 ]; 资源名称

} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {

DWORD OffsetToData; 实际数据的RVA

DWORD Size; 资源的大小,以字节为单位

DWORD CodePage; 通常是Unicode code page

DWORD Reserved; Reserved

} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

在到达真正的资源描述结构IMAGE_RESOURCE_DATA_ENTRY之前,通常需要经过3层结构: 资源类型(bmp/menu)-->资源名-->资源的不同语言版本-->IMAGE_RESOURCE_DATA_ENTRY

n.重定位Section

基址重定位目录通过IMAGE_DIRECTORY_ENTRY_BASERELOC索引IMAGE_DATA_DIRECTORY得到, 它的属性标志至少包括IMAGE_SCN_CNT_INITIALIZED_DATA、IMAGE_SCN_MEM_DISCARDABLE和IMAGE_SCN_MEM_READ Section的典型名称是".reloc",如果镜像不能加载到预定的位置,那么重定位信息就是必须的,链接器提供的地址就不再有效,PELoader需要对静态变量,字符串变量使用绝对的地址进行访问。 typedefstruct_IMAGE_BASE_RELOCATION{DWORDVirtualAddress;重定位目标块基本的RVADWORDSizeOfBlock;重定位块数据的大小 //WORDTypeOffset[1];}IMAGE_BASE_RELOCATION;重定位信息是一些连续的"块",每一"块"包含4K的重定位信息。 每一"块"数据由IMAGE_BASE_RELOCATION+实际的重定位数据,每一条数据都是16位的。

IMAGE_BASE_RELOCATION后的16bit的数据由高4bit的标志位和低12bit的位置信息含义:(实际上我们只要关心两种重定位类型,不需要任何操作和全替换操作)

1.当标志位为IMAGE_REL_BASED_ABSOLUTE(0),表示只用于字节对齐,不需要操作

2.当标志位为IMAGE_REL_BASED_HIGHLOW(3),表示由(12bit的值+块基址)的RVA指向的DWORD内容需要被计算后的修正地址替换。

1、该结构后面紧跟的是word型的重定位项。

2、重定位地址的 RVA = VirtualAddress + word型重定位项的低12位

3、word行重定位项的高4位表示重定位类型,一般常见的值为0和3

4、重定位项数 = (SizeOfBlock - 4 - 4) / 2

5、重定位块结束,以IMAGE_BASE_RELOCATION结构的VirtualAddress值为0结束。因此若映像加载00400000h,则代码加载地址为00401000h

本文内容个人通过网络,整理,收集。

附件:PE文件格式表

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。