PE文件格式分析
PE 的意思是 Portable Executable(可移植的执行体)。它是 Win32环境自身所带的执行文件格式。它的一些特性继承自Unix的Coff(common object file format)文件格式。“Portable Executable”(可移植的执行体)意味着此文件格式是跨Win32平台的;即使Windows运行在非Intel的CPU上,任何win32平台的PE装载器都能识别和使用该文件格式。
PE文件在文件系统中,与存贮在磁盘上的其它文件一样,都是二进制数据,对于操作系统来讲,可以认为是特定信息的一个载体,如果要让计算机系统执行某程序,则程序文件的载体必须符合某种特定的格式。要分析特定信息载体的格式,要求分析人员有数据分析、编码分析的能力。在Win32系统中,PE文件可以认为.exe、.dll、.sys 、.scr类型的文件,这些文件在磁盘上存贮的格式都是有一定规律的。
PE文件格式总揽
Microsoft 引入了PE文件格式,也就是大家都熟悉的PE格式,是Win32规范的一部分。然而,PE文件来源于更早的基于VAX/VMS的公共对象文件格式(COFF)。由于最初的Windows NT小组成员很多都来自数字设备公司(DEC),于是很自然的这些开发者使用已存在的代码以加速新的Windows NT平台的开发。
使用术语“可移植可执行”的目的是为了在所有Windows平台和所有支持的CPU上都有一个统一的文件格式。Windows NT及其以后版本,Windows 95及其以后版本和Windows CE都使用了这个相同的格式,所以说在很大程度上, 这个目的达到了。
Microsoft编译器生成的OBJ文件使用COFF格式。通过观察COFF格式的一些域你能知道它有多么老了,那些域使用八进制编码!COFF OBJ 文件中有许多和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(Windows CE下HMODULE和加载地址并不相同,这些以后再讲)。
内存中的模块描绘一个进程所需要的可执行文件的所有代码,数据,和资源。PE文件另一些部分只被读取,但不会被映射 (例如重定位信息)。一些部分根本就不被映射,例如,文件末尾的调试信息。PE头中的一个域可以告诉系统映射一个可执行文件到内存中需要多少内存。不被映射的数据放在文件末尾,这些数据之前的部分将会被映射。
描述PE格式(以及COFF文件)的主要地方是在WINNT.H文件中。在这个头文件中,你可以找到要和PE文件一起工作所必须的每个结构定义,枚举,和#define定义。当然,其它地方也有相关文档。例如,MSDN中有“Microsoft Portable Executable and Common Object File Format Specification” 这篇文章。但WINNT.H 文件最终决定了PE文件的格式。
有很多检查PE文件的工具。在它们之中有包含于Visual Studio中的Dumpbin,和包含于Platform SDK的Depends。我比较喜欢Depends因为它有一个检查一个文件的导入表和导出表的简洁的方式。Smidgeonsoft(http://www.smidgeonsoft.com)的PEBrowse专业版是一个很优秀的免费的PE观察器。这篇文章中包括的PEDUMP程序功能也很全面,实现了几乎Dumpbin的所有功能。
从API的角度来说,Microsoft的IMAGEHLP.DLL 提供了读取和编辑PE文件的机制。
在我开始讨论PE文件的详细内容之前,让我们首先回顾几个基本概念,这些概念贯穿于整个PE文件格式。下面,我将讨论PE文件的节,相对虚拟地址(RVAs),数据目录,和导入函数的方法。
PE文件的节
PE文件节包含了代码或某种数据。代码就是程序中的可执行代码,而数据却有很多种。除了可读写的程序数据(例如全局变量)之外,节中的其它类型的数据包括导入和导出表,资源,和重定位表。每个节在内存中都有它自己的属性,包括这个节是否含有代码,它是只读的还是可写的,这个节中的数据是否可在多个进程之间共享。
一般而言,一个节中所有的代码和数据都通过一些方法逻辑地联系起来。一个PE文件中通常至少有两个节:一个代码节,一个数据节。一般地,在一个PE文件中至少有一个其它类型的数据节。在这篇文章的第二部分我将讨论这几种节。
每个节都有一个不同的名字。这个名字被用来意指节的作用。例如,一个叫做.rdata的节表示一个只读数据节。使用节名只是为了人们方便,对操作系统来说没有任何意义。一个命名为FOOBAR的节和一个命名为.text.的节一样有效。Microsoft通常以一个句点作为节名的前缀,但这不是必需的。多年来,Borland链接器就一直使用像CODE和DATA.这样的节名。
编译器有一组它们生成的标准的节,对于它们没有什么不可思议的东西。你可以创建并命名你自己的节,链接器很乐意在可执行文件中包括它们。在Visual C++中,你可以让编译器把代码或数据放到通过#pragma 语句命名的节中。例如,下面这条语句
#pragma data_seg( "MY_DATA" )
它会使Visual C++把它生成的所有数据放到一个命名为MY_DATA的节中,而不是缺省的.data节。大多数程序都使用编译器产生的默认节,但偶尔你也许会有把代码或数据放到一个单独的节中的需求。
节并不是全部由链接器生成的,它们其实存在于OBJ文件中,通常由编译器把它们放到那儿。链接器的工作是合并OBJ文件中所有必须的节并且最终放到PE文件相应节中。例如,你的工程中的每个OBJ文件都至少有一个包含代码的.text节。链接器合并这些OBJ文件中的.text节到一个PE文件中的单个的.text节中。同样地,这些OBJ文件中的叫做.data的节被合并到PE文件中一个单个的.data节中。.LIB文件中的代码和数据通常也被包含在可执行文件中,但那个主题已经超出本文的范围了。
链接器遵循一整套规则来决定哪些节该被合并以及如何合并。OBJ文件中的某个节也许是提供给链接器使用的,并不会放到最终的可执行文件中去。像这样的节是由编译器用来以传递信息给链接器。
节有两种对齐值,一个是在磁盘文件中的偏移另一个是在内存中的偏移。PE文件头指定了这两个对齐值,它们可以是不同的。每个节起始于那个对齐值的倍数的位置。例如,在PE文件中,典型的对齐值是0x200。因此,每个节开始于一个0x200的倍数的文件偏移处。
一旦加载到内存中,节总是起始于至少一个页边界。就是说,当一个PE节被映射到内存中后,每个节的第一个字节都符合一个内存页。对于x86 CPUs,页是4KB,而IA-64,页是8KB。下面显示了PEDUMP输出的Windows XP KERNEL32.DLL 的.text节和.data节的一小部分。
节表
01 .text VirtSize: 00074658 VirtAddr: 00001000 raw data offs: 00000400 raw data size: 00074800...
02 .data VirtSize: 000028CA VirtAddr: 00076000 raw data offs: 00074C00 raw data size: 00002400
.text节在PE文件中的偏移为0x400,而在内存中位于KERNEL32加载地址之上第0x1000个字节处。同样的,.data节在PE文件中的偏移为0x74C00,而在内存中位于KERNEL32加载地址之上第0x76000个字节处。
创建一个节在文件中的偏移和在内存中的偏移相同的PE文件是可能的。这会使可执行文件变得很大,但在Windows 9x或Windows Me.下可以提高加载速度。缺省的/OPT:WIN98 链接器选项(Visual Studio 6.0引入)可以以这种方式创建PE文件。在Visual Studio® .NET中,也许会或者也许不会使用/OPT:NOWIN98,这依赖于文件是否足够小。
链接器的一个有趣的特点是可以合并节。如果两个节有类似的,兼容的特性,它们通常可以在链接时被合并到一个节中。这可通过/merge 选项做到。例如,下面的链接器选项合并.rdata和.text节到一个单个的命名为.text的节中。
/MERGE:.rdata=.text
合并节的好处是可以节省磁盘文件和内存空间。每个节至少要占用一个内存页。如果你能把可执行文件中节的数量从4个减少到3个,你就可以少占用一个内存页。当然,这取决于这两个被合并的节的未使用空间是否达到一页。
对于合并节没有什么硬性的规定。例如,可以合并.rdata到.text中,但你不应该把.rsrc,.reloc,或者.pdata合并到其它节中。在Visual Studio .NET之前,你可以合并.idata到其它节中。Visual Studio .NET,,就不允放过样做了,但当链接一个发布版的时候,链接器经常合并.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部分包含了对其所指向的数据结构的更详细的描述。
图 2 IMAGE_DATA_DIRECTORY 值
值 | 描述 |
IMAGE_DIRECTORY_ENTRY_EXPORT | 指向导出表(一个IMAGE_EXPORT_DIRECTORY结构)。 |
IMAGE_DIRECTORY_ENTRY_IMPORT | 指向导入表(一个IMAGE_IMPORT_DESCRIPTOR结构数组)。 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 指向资源(一个IMAGE_RESOURCE_DIRECTORY结构。 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION | 指向异常处理表(一个IMAGE_RUNTIME_FUNCTION_ENTRY结构数组)。CPU特定的并且基于表的异常处理。用于除x86之外的其它CPU上。 |
IMAGE_DIRECTORY_ENTRY_SECURITY | 指向一个WIN_CERTIFICATE结构的列表,它定义在WinTrust.H中。不会被映射到内存中。因此,VirtualAddress域是一个文件偏移,而不是一个RVA。 |
IMAGE_DIRECTORY_ENTRY_BASERELOC | 指向基址重定位信息。 |
IMAGE_DIRECTORY_ENTRY_DEBUG | 指向一个IMAGE_DEBUG_DIRECTORY结构数组,其中每个结构描述了映像的一些调试信息。早期的Borland链接器设置这个IMAGE_DATA_DIRECTORY结构的Size域为结构的数目,而不是字节大小。要得到IMAGE_DEBUG_DIRECTORY结构的数目,用IMAGE_DEBUG_DIRECTORY 的大小除以这个Size域。 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 指向特定架构数据,它是一个IMAGE_ARCHITECTURE_HEADER结构数组。不用于x86或IA-64,但看来已用于DEC/Compaq Alpha。 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 在某些架构体系上VirtualAddress域是一个RVA,被用来作为全局指针(gp)。不用于x86,而用于IA-64。Size域没有被使用。参见2000年11月的Under The Hood 专栏可得到关于IA-64 gp的更多信息。 |
IMAGE_DIRECTORY_ENTRY_TLS | 指向线程局部存储初始化节。 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 指向一个IMAGE_LOAD_CONFIG_DIRECTORY结构。IMAGE_LOAD_CONFIG_DIRECTORY中的信息是特定于Windows NT、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把这个结构放到你的可执行文件中,你必须用名字__load_config_used 定义一个全局结构,类型是IMAGE_LOAD_CONFIG_DIRECTORY。对于非x86的其它体系,符号名是_load_config_used (只有一个下划线)。如果你确实要包含一个IMAGE_LOAD_CONFIG_DIRECTORY,那么在 C++ 中要得到正确的名字比较棘手。链接器看到的符号名必须是__load_config_used (两个下划线)。C++ 编译器会在全局符号前加一个下划线。另外,它还用类型信息修饰全局符号名。因此,要使一切正常,在 C++ 中就必须像下面这样使用: extern "C" IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {...} |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 指向一个 IMAGE_BOUND_IMPORT_DESCRIPTOR结构数组,对应于这个映像绑定的每个DLL。数组元素中的时间戳允许加载器快速判断绑定是否是新的。如果不是,加载器忽略绑定信息并且按正常方式解决导入API。 |
IMAGE_DIRECTORY_ENTRY_IAT | 指向第一个导入地址表(IAT)的开始位置。对应于每个被导入DLL的IAT都连续地排列在内存中。Size域指出了所有IAT的总的大小。在写入导入函数的地址时加载器使用这个地址和Size域指定的大小临时地标记IAT为可读写。 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 指向延迟加载信息,它是一个CImgDelayDescr结构数组,定义在Visual C++的头文件DELAYIMP.H中。延迟加载的DLL直到对它们中的API进行第一次调用发生时才会被装入。Windows中并没有关于延迟加载DLL的知识,认识到这一点很重要。延迟加载的特征完全是由链接器和运行时库实现的。 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 在最近更新的系统头文件中这个值已被改名为IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可执行文件中.NET信息的最高级别信息,包括元数据。这个信息是一个IMAGE_COR20_HEADER结构。 |
导入函数
当你使用其它DLL中的代码或数据时,就要导入它。加载一个PE文件时,Windows 加载器的一个工作就是查找所有被导入的函数和数据并让那此函数和数据的地址可被加载的文件使用。完成这个工作所用到的数据结构的细节放到这篇文章的第二部分进行讨论,在这里学习一下这些概念。
当你直接调用到一个DLL的代码或数据时,你就是正在隐式地链接到这个DLL。要使被导入的API的地址可被你的代码使用你不需要做任何事情。加载器会完成所有需要做的工作。另外还有显式链接。意思就是说显式地加载目标DLL并查找API的地址。这几乎总是通过LoadLibrary和GetProcAddress来实现的。
当你隐式地链接一个API时,类似LoadLibrary和GetProcAddress的代码仍然被执行了,只不过是由加载器代替你自动执行的。加载器也会确保被加载的PE文件所需要的任何附加的DLL也被加载。例如,由Visual C++®链接器创建的每个正常的程序都要链接KERNEL32.DLL。而KERNEL32.DLL又从NTDLL.DLL导入函数。同样,如果你从GDI32.DLL导入函数,也将会依赖于USER32,ADVAPI32,NTDLL和KERNEL32 DLL。加载器会保证这些DLL都被加载并且解决所有导入问题。(Visual Basic 6.0和Microsoft .NET 可执行文件直接链接到另外一个DLL而不是KERNEL32,但原理是相同的。)
隐式链接时,对主EXE文件和所有依赖的DLL的处理发生在程序第一次启动时。如果出现了任何问题(例如,一个被引用的DLL没有找到),进程将被终止。
Visual C++ 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看起来应该像下面这样:
CALL DWORD PTR [0x00405030]
这是通过函数指针进行调用。无论怎样,0x405030地址处的DWORD值就是这个CALL指令将把控制转移到的地址。在前面例子中,地址0x405030就位于IAT中。
低效的调用看起来像下面这样:
CALL 0x0040100C
...
0x0040100C:
JMP DWORD PTR [0x00405030]
这种情况下,CALL把控制转到一个小的程序段处。这段程序通过JMP指令跳转到0x405030地址处。记住0x405030位于IAT中。低效调用导入函数用到了五个字节的额外代码,并且由于使用JMP指令花费了更长的执行时间。
你也许会奇怪为什么要使用低效的方法呢。有一个很好的解释。编译器无法区分导入函数调用和普通函数调用。因此,编译器生成同样形式的CALL指令
CALL XXXXXXXX
XXXXXXXX是一个稍后由链接器填充的实际地址。要注意这个CALL指令后面的地址并不是一个函数指针,而是一段实际代码的地址。链接器必须提供一块代码来替换这个XXXXXXXX。这样做的最简单的方法就是调用到一个JMP stub,就像你在上面看到的那样。
这个JMP stub从哪儿来呢?很令人惊奇,它来自于导入函数的导入库。如果你检查一个导入库,并且用导入API的名称来检查代码,你将会发现和上面JMP stub很相似的代码。这就是说缺省情况下将使用低效形式调用导入API。
那么,下一个要问的问题就是怎样才能得到优化的形式。答案是给编译器一个提示。__declspec(dllimport)函数修饰符告诉编译器这个函数位于其它DLL中,于是编译器将生成指令
CALL DWORD PTR [XXXXXXXX]
而不是:
CALL XXXXXXXX
另外,编译器也生成一些信息以告诉链接器把这个指令的函数指针部分解析为一个符号名__imp_functionname。例如,如果你正在调用MyFunction,符号名就是__imp_MyFunction。查看一个导入库,你会发现除了正常的符号名外,也有一个加了__imp__前缀的符号。__imp__ symbol可以直接定位到IAT入口,而不是通过那个JMP stub。
那么这对你以后每天的生活有什么影响呢?如果你正在编写导出函数并为它们提供一个头文件,记住要使用这个__declspec(dllimport)修饰符:
__declspec(dllimport) void Foo(void);
如果你查看Windows系统头文件,你会发现Windows API都使用了__declspec(dllimport)。它并不容易被发现。你可在WINNT.H头文件中找到DECLSPEC_IMPORT 宏定义,而这个宏被用在一些文件中例如WinBase.H。到这里你就会明白__declspec(dllimport)是如何被用在系统API声明上的。
我们知道,很多PE分析工具都可以查看一个EXE文件的引用DLL文件函数表,其实,这个本身就是存储在EXE头部的一个重要信息。
我们借用一张PE结构图来分析:
一个EXE完整的PE结构分五大部分。见上图.
MS-DOS头
最开头的是部分是DOS部首,DOS部首由两部分组成:DOS的MZ文件标志和DOS stub(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最初设计者之一Mark Zbikowski名子的首字母大写。
DOS MZ header部分是DOS时代遗留的产物,是PE文件的一个遗传基因,一个Win32程序如果在DOS下也是可以执行,只是提示:“This program cannot be run in DOS mode.”然后就结束执行,提示执行者,这个程序要在Win32系统下执行。
DOS stub 部分是DOS插桩代码,是DOS下的16位程序代码,只是为了显示上面的提示数据。这段代码是编译器在程序编译过程中自动添加的。
a.DOS头的数据结构
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} 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 域的值。 typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
在一个有效的PE文件中,Signature字段的值是0x00004550,用ASCII表示就是“PE00”。 #define IMAGE_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结构末尾的数据目录数组用来定位可执行文件中的重要数据的地址。每个数据目录条目看起来就像下面这样:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA of the data
DWORD Size; // Size of the data
};
文件头信息
值得注意的是PE文件头中的IMAGE_OPTIONAL_HEADER32是一个非常重要的结构,PE文件中的导入表、导出表、资源、重定位表等数据的位置和长度都保存在这个结构里。
IMAGE_FILE_HEADER这个结构的定义如下:
图 3 IMAGE_FILE_HEADER
大小 | 域 | 描述 |
WORD | Machine | 可执行文件的目标CPU。通常的值是: IMAGE_FILE_MACHINE_I386 0x014c // Intel 386 IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64 |
WORD | NumberOfSections | 指出节表中有多少个节。节表紧跟在IMAGE_NT_HEADERS之后。 |
DWORD | TimeDateStamp | 指出这个文件被创建的时间。这个值是用格林尼治时间(GMT)计算的自从1970年1月1日以来所经过的秒数。这个值比文件系统的日期/时间更准确地指出了文件被创建的时间。使用_ctime 函数(对时区敏感)可以很容易地把这个值转换为人们可读的字符串形式。另一个有用的函数是gmtime。 |
DWORD | PointerToSymbolTable | COFF符号表的文件偏移,描述于Microsoft规范的5.4节。COFF符号表在PE文件中很少见,因为出现了新的调试格式。Visual Studio .NET之前,可通过指定链接器选项/DEBUGTYPE:COFF来创建COFF符号表。COFF符号表几乎总是会出现在OBJ文件中。如果没有符号表则设此值为0。 |
DWORD | NumberOfSymbols | 如果存在COFF符号表,此域表示其中的符号的数目。COFF符号是一个固定大小的结构,要找到COFF符号表的末尾就必须用到此域。紧跟COFF符号之后是一个用来保存较长符号名的字符串表。 |
WORD | SizeOfOptionalHeader | IMAGE_FILE_HEADER 之后的可选数据的大小。在PE文件中,这个数据称为IMAGE_OPTIONAL_HEADER。这个大小在32位和64位的文件中是不同的。对于32位PE文件,这个域通常是224。对于64位PE32+文件,它通常是240。然而,这些值只是所要求的最小值,更大的值也可能会出现。 |
WORD | Characteristics | 一组指示文件属性的位标。这些标记的有效值是定义于WINNT.H文件中的IMAGE_FILE_xxx值。一些常用的值在图4中列出。 |
01.typedef struct _IMAGE_FILE_HEADER {
02.00h WORD Machine;//运行平台
03.02h WORD NumberOfSections;//区块数目 pe文件中区块的数量.
04.06h DWORD TimeDateStamp;//文件日期时间戳,指这个pe文件生成的时间,它的值是从1969年12月31日16:00:00以来的秒数. 05.0Ah DWORD PointerToSymbolTable;//指向符号表 Coff调试符号表的偏移地址.
06.0Eh DWORD NumberOfSymbols;//符号表中的符号数量 Coff符号表中符号的个数. 这个域和前个域在release版本的程序里是0.
07.12h WORD SizeOfOptionalHeader;//映像可选头结构的大小 IMAGE_OPTIONAL_HEADER32结构的大小(即多少字节).我们接着就要提到这个结构了.事实上,pe文件的大部分重要的域都在IMAGE_OPTIONAL_HEADER结构里.
08.14hWORD Characteristics;//文件特征值
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
这个结构体表明一个PE文件的基本特征属性,也是一个PE文件的入口
Machine域说明这个pe文件在什么CPU上运行,具体如下:
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian #define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
Characteristics 这个域描述pe文件的一些属性信息,比如是否可执行,是否是一个动态连接库等.具体定义如下:
图 4 IMAGE_FILE_XXX
值 | 描述 |
IMAGE_FILE_RELOCS_STRIPPED | 文件中不包括重定位信息。 |
IMAGE_FILE_EXECUTABLE_IMAGE | 文件是可执行的。 |
IMAGE_FILE_AGGRESIVE_WS_TRIM | 让操作系统强制整理工作区。 |
IMAGE_FILE_LARGE_ADDRESS_AWARE | 应用程序可处理超过2GB的地址。 |
IMAGE_FILE_32BIT_MACHINE | 需要一个32位的机器。 |
IMAGE_FILE_DEBUG_STRIPPED | 调试信息位于一个.DBG文件中。 |
IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP | 如果映像在可移动媒体中,那么复制到交换文件并从交换文件中运行。 |
IMAGE_FILE_NET_RUN_FROM_SWAP | 如果映像在网络上,那么复制到交换文件并从交换文件中运行。 |
IMAGE_FILE_DLL | 是一个DLL文件。 |
IMAGE_FILE_UP_SYSTEM_ONLY | 只能在单处理器机器中运行。 |
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 重定位信息被移除 #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可执行 #define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 行号被移除 #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符号被移除 #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 程序能处理大于2G的地址 #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed. #define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位机器 #define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // .dbg文件的调试信息被移除 #define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // 如果在移动介质中,拷到交换文件中运行 #define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // 如果在网络中,拷到交换文件中运行 #define IMAGE_FILE_SYSTEM 0x1000 // 系统文件 #define IMAGE_FILE_DLL 0x2000 // 文件是一个dll #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // 文件只能运行在单处理器上 #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
所以,根据这个结构体的信息,我们就可以判断一个文件究竟是不是一个真正的PE文件,该PE文件的类型是可执行的还是可调用的(DLL)
我们可以写个简单的小程序来读取这个信息:
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "conio.h"
int main(int argc,char* argv[])
{
FILE *p;
LONG e_lfanew;//指向IMAGE_NT_HEADERS32结构在文件中的偏移
IMAGE_FILE_HEADER myfileheader;
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();
return 0;
}
注释比较详细了,大家根据这个就可以读取一个PE文件的基本特征信息了.以上代码VC6编译通过
紧接着上一节,我们来研究下IMAGE_OPTIONAL_HEADER32,这个属于PE中附加结构信息,同样是很重要的。
我们先来看看它的结构:
图 5 IMAGE_OPTIONAL_HEADER
Size | Structure Member | Description |
WORD | Magic | 一个签名,确定这是什么类型的头。两个最常用的值是IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b和IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b. |
BYTE | MajorLinkerVersion | 创建可执行文件的链接器的主版本号。对于Microsoft的链接器生成的PE文件,这个版本号的Visual Studio的版本号相一致(例如,版本6表示Visual Studio 6.0)。 |
BYTE | MinorLinkerVersion | 创建可执行文件的链接器的次版本号。 |
DWORD | SizeOfCode | 所有具有IMAGE_SCN_CNT_CODE属性的节的总的大小。 |
DWORD | SizeOfInitializedData | 所有包含已初始数据的节的总的大小。 |
DWORD | SizeOfUninitializedData | 所有包含未初始化数据的节的总的大小。这个域总是0,因为链接器可以把未初始化数据附加到常规数据节的末尾。 |
DWORD | AddressOfEntryPoint | 文件中将被执行的第一个代码字节的RVA。对于DLL,这个进入点将在进程初始化和关闭时以及线程被创建和销毁时调用。在大多数可执行文件中,这个地址并不直接指向main,WinMain或DllMain函数,而是指向运行时库代码,由运行时库调用前述函数。在DLL中,这个域可以被设为0,这样的话上面所说的通知就不能被接收到。链接器选项/NOENTRY可以设置这个域为0。 |
DWORD | BaseOfCode | 加载到内存后代码的第一个字节的RVA。 |
DWORD | BaseOfData | 理论上,它表示加载到内存后数据的第一个字节的RVA。然而,这个域的值对于不同版本的Microsoft链接器是不一致的。在64位的可执行文件中这个域不出现。 |
DWORD | ImageBase | 文件在内存中的首选加载地址。加载器尽可能地把PE文件加载到这个地址(就是说,如果当前这块内存没有被占用,它是对齐的并且是一个合法的地址,等等)。如果可执行文件被加载到这个地址,加载器就可以跳过进行基址重定位(在这篇文章的第二部分描述)这一步。对于EXE,缺省的ImageBase是0x400000。对于DLL,缺省是0x10000000。在链接时可以通过/BASE 选项来指定ImageBase,或者以后用REBASE工具重新设置。 |
DWORD | SectionAlignment | 加载到内存后节的对齐大小。这个值必须大于等于FileAlignment(下一个域)。缺省的对齐值是目标CPU的页大上。对于运行在Windows 9x或Windows Me下的用户模式可执行文件,最小对齐大小是一页(4KB)。这个域可以通过链接器选项/ALIGN来设置。 |
DWORD | FileAlignment | 在PE文件中节的对齐大小。对于x86下的可执行文件,这个值通常是0x200或0x1000。不同版本的Microsoft链接器缺省值不同。这个值必须是2的幂,并且如果SectionAlignment小于CPU的页大小,这个域必须和SectionAlignment相匹配。链接器选项/OPT:WIN98可设置x86可执行文件的文件对齐为0x1000,/OPT:NOWIN98设置文件对齐为0x200。 |
WORD | MajorOperatingSystemVersion | 所要求的操作系统的主版本号。随着那么多版本Windows的出现,这个域的值就变得很不确切。 |
WORD | MinorOperatingSystemVersion | 所要求的操作系统的次版本号。 |
WORD | MajorImageVersion | 这个文件的主版本号。不被系统使用并可设为0。可以通过链接器选项/VERSION来设置。 |
WORD | MinorImageVersion | 这个文件的次版本号。 |
WORD | MajorSubsystemVersion | 可执行文件所要求的操作子系统的主版本号。它曾经被用来表示需要较新的Windows 95或Windows NT用户界面,而不是老版本的Windows NT界面。今天随着各种不同版本Windows的出现,这个域已不被系统使用,并且通常被设为4。可通过链接器选项/SUBSYSTEM设置这个域的值。 |
WORD | MinorSubsystemVersion | 可执行文件所要求的操作子系统的次版本号。 |
DWORD | Win32VersionValue | 另一个不被使用的域,通常设为0。 |
DWORD | SizeOfImage | 映像的大小。它表示了加载文件到内存中时系统必须保留的内存的数量。这个域的值必须是SectionAlignmnet的倍数。 |
DWORD | SizeOfHeaders | MS-DOS头,PE头和节表的总的大小。PE文件中所有这些项目出现在任何代码或数据节之前。这个域的值被调整为文件对齐大小的整数倍。 |
DWORD | CheckSum | 映像的校验和。IMAGEHLP.DLL中的CheckSumMappedFile函数可以计算出这个值。校验和用于内核模式的驱动和一些系统DLL。对于其它的,这个域可以为0。当使用链接器选项/RELEASE时校验和被放入文件中。 |
WORD | Subsystem | 指示可执行文件期望的子系统(用户界面类型)的枚举值。这个域只用于EXE。一些重要的值包括: IMAGE_SUBSYSTEM_NATIVE// 映像不需要子系统IMAGE_SUBSYSTEM_WINDOWS_GUI// 使用Windows GUIIMAGE_SUBSYSTEM_WINDOWS_CUI// 作为控制台程序运行。// 运行时,操作系统创建一个控制台// 窗口并提供stdin,stdout和stderr// 文件句柄。 |
WORD | DllCharacteristics | 标记DLL的特性。对应于IMAGE_DLLCHARACTERISTICS_xxx定义。当前的值是: IMAGE_DLLCHARACTERISTICS_NO_BIND// 不要绑定这个映像IMAGE_DLLCHARACTERISTICS_WDM_DRIVER// WDM模式的驱动程序IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE// 当终端服务加载一个不是// Terminal- Services-aware 的应用程// 序时,它也加载一个包含兼容代码// 的DLL。 |
DWORD | SizeOfStackReserve | 在EXE文件中,为线程保留的堆栈大小。缺省是1MB,但并不是所有的内存一开始都被提交。 |
DWORD | SizeOfStackCommit | 在EXE文件中,为堆栈初始提交的内存数量。缺省情况下,这个域是4KB。 |
DWORD | SizeOfHeapReserve | 在EXE文件中,为默认进程堆初始保留的内存大小。缺省是1MB。然而在当前版本的Windows中,堆不经过用户干涉就能超出这里指定的大小。 |
DWORD | SizeOfHeapCommit | 在EXE文件中,提交到堆的内存大小。缺省情况下,这里的值是4KB。 |
DWORD | LoaderFlags | 不使用。 |
DWORD | NumberOfRvaAndSizes | 在IMAGE_NT_HEADERS结构的末尾是一个IMAGE_DATA_DIRECTORY结构数组。此域包含了这个数组的元素个数。自从最早的Windows NT发布以来这个域的值一直是16。 |
IMAGE_ | DataDirectory[16] | 一个IMAGE_DATA_DIRECTORY结构数组。每个结构都包含了可执行文件中一些重要数据的RVA和大小(例如导入表,导出表和资源)。 |
01.typedef struct _IMAGE_OPTIONAL_HEADER {
// Standard fields. 标准域
00hWORD Magic;//幻数,32位pe文件总为010bh 32位pe文件总为010bh 这个常数的定义如下:
#define IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC 0x107
02h BYTE MajorLinkerVersion;//连接程序的主版本号 如vc6.0的为06h 08.03hBYTE MinorLinkerVersion;//连接器副版本号 如vc6.0的为00h
04h DWORD SizeOfCode;//代码段总大小 pe文件代码段的大小.是FileAlignment的整数倍.
08h DWORD SizeOfInitializedData;//所有含已初始化数据的块的大小,一般在.data段中.
0ch DWORD SizeOfUninitializedData;//所有含未初始化数据的块的大小,一般在.bss段中
10h DWORD AddressOfEntryPoint;//程序执行入口地址(RVA) 程序开始执行的地址,这是一个RVA(相对虚拟地址).对于exe文件,这里是启动代码;
//对于dll文件,这里是libMain()的地址. 在脱壳时第一件事就是找入口点,指的就是这个值.
14h DWORD BaseOfCode;//代码段起始地址(RVA) 代码段基地址,微软的连接程序生成的程序一般把这个值置为1000h
18h DWORD BaseOfData;//数据段起始地址(RVA) 数据段基地址
// NT additional fields.
1ch DWORD ImageBase;//pe文件默认的装入起始地址.windows9x中exe文件为400000h,dll文件为10000000h
20h DWORD SectionAlignment;//内存中区块的对齐单位.区块总是对齐到这个值的整数倍.x86的32位系统上默认值位1000h
24h DWORD FileAlignment;//文件中区块的对齐单位; pe文件中默认值为 200h.
28h WORD MajorOperatingSystemVersion;//所需操作系统主版本号
2ah WORD MinorOperatingSystemVersion;//所需操作系统副版本号 上面两个域是指运行这个pe文件所需的操作系统的最低版本号.windows95/98和windows nt 4.0 的内部版本号都是 4.0 ,而windows2000的内部版本号是5.0
2ch WORD MajorImageVersion;//自定义主版本号
2eh WORD MinorImageVersion;//自定义副版本号 上面两个域是指用户自定义的pe文件的版本号.可以通过连接程序来设置,如: LINK /VERSION:2.0 MyApp.obj一般在升级时使用.
30h WORD MajorSubsystemVersion;//所需子系统主版本号
32h WORD MinorSubsystemVersion;//所需子系统副版本号 上面两个域是指运行这个pe文件所要求的子系统的版本号.
34h DWORD Win32VersionValue;//总是0
38h DWORD SizeOfImage;//pe文件装入内存后映像的总大小.如果SectionAlignment域和FileAlignment域相等,那么这个值也是pe文件在硬盘上的大小.
3ch DWORD SizeOfHeaders;//从pe文件开始到节表(包含节表)的总大小 .其后是各个区段的数据.
40h DWORD CheckSum;//pe文件CRC校验和
44h WORD Subsystem;//用户界面使用的子系统类型,见后面
46h WORD DllCharacteristics;//为0
48h DWORD SizeOfStackReserve;//为线程的栈初始保留的虚拟内存的默认大小,默认为00100000h.如果在调用CreateThread函数时指定
//堆栈的大小为0,被创建的线程的堆栈的初始大小就与这个值相同.
4ch DWORD SizeOfStackCommit;// 为线程的栈初始提交的虚拟内存的大小.微软的连接程序把这个值置为 1000h.
50h DWORD SizeOfHeapReserve;// 为进程的堆保留的虚拟内存的大小.默认值为 00100000h.
54h DWORD SizeOfHeapCommit;//为进程的堆初始提交的虚拟内存的大小 微软的连接程序把这个值置为1000h.
58h DWORD LoaderFlags;//通常为0
5ch DWORD NumberOfRvaAndSizes;//数据目录结构数组的项数,总为 00000010h 这个值定义如下: #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
60h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];//数据目录结构数组
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
这个结构体非常的庞大,大家通过注释可以看出,这个结构体保存了相当全面的PE附件信息。
Subsystem pe文件的用户界面使用的子系统类型.定义如下:
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem. #define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem. #define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem. #define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem. #define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem. #define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver. #define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
IMAGE_DATA_DIRECTORY DataDirectory[0x10]
数据目录结构数组
IMAGE_DATA_DIRECTORY结构定义如下:
1.typedef struct _IMAGE_DATA_DIRECTORY {
2.DWORD VirtualAddress;// 相对虚拟地址
3.DWORD Size;//大小
4.} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
这个结构包含了pe文件中重要部分的RVA地址和大小.这个数组使操作系统的加载程序能够快速定位特定的区段.具体定义如下:
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
节表
IMAGE_NT_HEADERS之后紧跟着节表。节表是一个IMAGE_SECTION_HEADER结构数组。IMAGE_SECTION_HEADER提供了和它关联的节的信息,包括位置,长度和属性。图6描述了IMAGE_SECTION_HEADER结构的各域。在IMAGE_FILE_HEADER结构中的NumberOfSections 域中提供了IMAGE_SECTION_HEADER 结构的数目。
可执行文件中的节的文件对齐对最终的文件大小有很大的影响。在Visual Studio 6.0中, 链接器缺省的对齐大小为4KB,除非使用了/OPT:NOWIN98或/ALIGN选项。Visual Studio .NET链接器也缺省使用了/OPT:WIN98选项,但它检测可执行文件的大小是否小于某个值,如果是则使用0x200字节进行对齐。
另一个值得注意的对齐方式来自.NET文件规范。它规定.NET可执行文件的内存对齐值是8KB,而不是x86平台的4KB。这是为了保证在x86平台上创建的可执行文件在IA-64平台上仍然可以运行。如果节的内存对齐值是4KB,IA-64加载器就不能加载这个文件,因为64位Windows的页大小是8KB。
图 6 IMAGE_SECTION_HEADER
大小 | 域 | 描述 |
BYTE | Name[8] | 节的ASCII名称。节名不保证一定是以NULL结尾的。如果你指定了长于8个字符的节名,链接器会把它截短为8个字符。在OBJ文件中存在一个机制允许更长的节名。节名通常以一个句点开始,但这并不是必须的。节名中有一个“$”时链接器会对之进行特殊处理。前面带有“$”的相同名字的节将会被合并。合并的顺序是按照“$”后面字符的字母顺序进行合并的。关于名字中带有“$”的节以及这些节怎样被合并有很多的主题,但这些细节已超出本文所讨论的范围了。 |
DWORD | Misc.VirtualSize | 指出实际被使用的节的大小。这个域的值可以大于或小于SizeOfRawData域的值。如果VirtualSize的值大,SizeOfRawData就是可执行文件中已初始化数据的大小,剩下的字节用0填充。在OBJ文件中这个域被设为0。 |
DWORD | VirtualAddress | 在可执行文件中,是节被加载到内存中后的RVA。在OBJ文件中应该被设为0。 |
DWORD | SizeOfRawData | 在可执行文件或OBJ文件中该节所占用的字节大小。对于可执行文件,这个值必须是PE头中给出的文件对齐值的倍数。如果是0,则说明这个节中的数据是未初始的。 |
DWORD | PointerToRawData | 节在磁盘文件中的偏移。对于可执行文件,这个值必须是PE头部给出的文件对齐值的倍数。 |
DWORD | PointerToRelocations | 节的重定位数据的文件偏移。只用于OBJ文件,在可执行文件中被设为0。对于OBJ文件,如果这个域的值不为0的话,它就指向一个IMAGE_RELOCATION结构数组。 |
DWORD | PointerToLinenumbers | 节的COFF样式行号的文件偏移。如果非0,则指向一个IMAGE_LINENUMBER结构数组。只在COFF行号被生成时使用。 |
WORD | NumberOfRelocations | PointerToRelocations 指向的重定位的数目。在可执行文件中应该是0。 |
WORD | NumberOfLinenumbers | NumberOfRelocations 域指向的行号的数目。只在COFF行号被生成时使用。 |
DWORD | Characteristics | 被或到一起的一些标记,用来表示节的属性。这些标记中很多都可以通过链接器选项/SECTION来设置。常用值在图7中列出。 |
c.Sections的目录
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //标识字,表示是可执行镜像还是ROM镜像 union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //目标文件是重定位的地址,执行文件是镜像的大小
DWORD VirtualAddress; 加载到内存后的相对地址
DWORD SizeOfRawData; Section原始数据的大小,FileAlignment 对齐
DWORD PointerToRawData; 原始数据的文件偏移
DWORD PointerToRelocations; 重定位信息的数据位置
DWORD PointerToLinenumbers; 行数据的位置
WORD NumberOfRelocations; 重定向信息的数目
WORD NumberOfLinenumbers; 行数据的项数
DWORD Characteristics; Section的属性配置 ,详细见下面
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
图 7 Flags
值 | 描述 |
IMAGE_SCN_CNT_CODE | 节中包含代码。 |
IMAGE_SCN_MEM_EXECUTE | 节是可执行的。 |
IMAGE_SCN_CNT_INITIALIZED_DATA | 节中包含已初始化数据。 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA | 节中包含未初始化数据。 |
IMAGE_SCN_MEM_DISCARDABLE | 节可被丢弃。用于保存链接器使用的一些信息,包括.debug$节。 |
IMAGE_SCN_MEM_NOT_PAGED | 节不可被页交换,因此它总是存在于物理内存中。经常用于内核模式的驱动程序。 |
IMAGE_SCN_MEM_SHARED | 包含节的数据的物理内存页在所有用到这个可执行体的进程之间共享。因此,每个进程看到这个节中的数据值都是完全一样的。这对一个进程的所有实例之间共享全局变量很有用。要使一个节共享,可使用/section:name,S 链接器选项。 |
IMAGE_SCN_MEM_READ | 节是可读的。几乎总是被设置。 |
IMAGE_SCN_MEM_WRITE | 节是可写的。 |
IMAGE_SCN_LNK_INFO | 节中包含链接器使用的信息。只在OBJ文件中存在。 |
IMAGE_SCN_LNK_REMOVE | 节中的数据不会成为映像的一部分。只出现在OBJ文件中。 |
IMAGE_SCN_LNK_COMDAT | 节中的内容是公共数据(comdat)。公共数据是指可被定义在多个OBJ文件中的数据。链接器将选择一个包含到可执行文件中。Comdat 对于支持C++模板函数和在函数级别上的链接是至关重要的。Comdat节只出现在OBJ文件中。 |
IMAGE_SCN_ALIGN_XBYTES | 在最终的可执行文件中这个节中数据的对齐大小。它可有许多取值(_4BYTES,_8BYTES,_16BYTES等)。如果没有被指定,缺省是16字节。这些标记只在OBJ文件中被设置。 |
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_PAGED Section不能被页交换出内存
IMAGE_SCN_MEM_SHARED表示所有的实例都共享同一个内存镜像,对于DLL有效,这个只对数据Section有意义的,因为代码Section都是写拷贝(这里拷贝不应该被理解为有代码功能拷贝操作,只是里面的地址会被修正)的,因为在执行重定向的时候,会映射到不同的地址。 IMAGE_SCN_MEM_EXECUTE Section的内容可以被执行
IMAGE_SCN_MEM_READ,可读
IMAGE_SCN_MEM_WRITE, 可写
Section Headers是一个数组,但是绑定section header后会马上跟着Section的内容。这个也是NumberOfSections存在的目的,因为这样才可以精确访问各个Section,而不能简单通过枚举,并当遇到全0的Section Header时停。
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.BSS Section
未初始化数据Section,包括没有初始化的全局变量和静态变量,这种Section的PointToRawData 是0,Section的标志变量包括IMAGE_SCN_CNT_UNINITIALIZED_DATA,IMAGE_SCN_MEM_WRITE, IMAGE_SCN_MEM_READ而整个数据长度由IMAGE_OPTIONAL_HEADER32的SizeOfUninitializedData表示,而且它的初始化需要由PE Loader 来完成。 典型的Section名称有:".bss", "BSS" g.栈Section和堆Section并不保存在PE中,而是由PE Loader根据IMAGE_OPTIONAL_HEADER32设置的堆栈大小创建
h.版权Section
目录结构的IMAGE_DIRECTORY_ENTRY_COPYRIGHT下标的内容是一个以ASCII的描述的字符串,通常它是通过参数的形式传给连接器的,这个串并不以0结尾的。这个Section是不能写的。
i.输入地址表Section
对于编译器而言发现对外部符号的调用时,它只会直接生成对那个符号的调用指令。但是连接器就需要为每个 输入的函数符号设置调用stub,这个stub就是跳转到目的地址,程序员可以通过"__declspec(dllimport)"来 避免生成stub,因为编译器会自己去计算,连接器就不要生成stub了。 stub的集合就是"转移区",通常它位于代码Section中,连接器并不知道这些地址的真正的值,它需要PE loader 在加载的时候修正。 转移区的结构:
_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的内部数据(当然,不会包含一个Section Header)是一个IMAGE_IMPORT_DESCRIPTOR数组
这个Section属性至少包括IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ
通过IMAGE_DIRECTORY_ENTRY_IMPORT可以得到第一个IMAGE_IMPORT_DESCRIPTOR的位置
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
//原来的导入函数名数组数组首地址RVA ORG
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) 数组的成员是IMAGE_THUNK_DATA结构
};
DWORD TimeDateStamp; // 0 if not bound, 时间戳(作用比较复杂) 1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders 中转链 这个数据一般为0,可以不关心
DWORD Name; RVA,指向DLL名字的指针,ASCII字符串
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) 函数转换后的地址,
//指向一个 IMAGE_THUNK_DATA 结构数组的RVA,这个数据与IAT所指向的地址一致
} IMAGE_IMPORT_DESCRIPTOR;
IMAGE_THUNK_DATA 这是一个DWORD类型的集合。通常我们将其解释为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针,
其定义如下:
IMAGE_THUNK_DATA{
union
{ PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;//判定当前结构数据是不是以序号为输出的,如果是的话该值为0x800000000,此时PIMAGE_IMPORT_BY_NAME不可做为名称使用 PIMAGE_IMPORT_BY_NAME AddressOfData;
}u1;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;// 函数输出序号 导入的DLL的输出名字表的索引
BYTE Name1[1];//输出函数名称 BYTE 0结尾的ASCII字符串(函数名)
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME
这里有两个指向导入函数信息的RVA数组ORG和TAR,使用方式是这样的, PE Loader 查造执行文件的导入符号表,优先通过导入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的值,PE Loader就能节省时间;当DLL的版本不对,或者重定向必须发生时,ORG仍然提供足够的信息让PE Loader来修正地址映射。
重定向的发生Loader是知道的,而DLL版本是通过时间戳来判断的,如果时间戳为0,表示没有绑定,如果非0,就需要和DLL里Header的时间戳对比,只有一致时,才不需要进行导入地址的修正。
3.对于中转的情况,也就是引用的DLL中导出了一个不在本身定义的符号,这时修正是必须发生的。
4.中转链的值是TAR的下标,它表示这个符号是中转的,而这TAR的内容就是下一个中转的下标,一直到(-1)表示没有中转了
k.新式绑定
它不是一个独立的Section,而是放到Section Headers后面,第一节之前,通过IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT可以转向到数据开头IMAGE_BOUND_IMPORT_DESCRIPTOR 所有导入符号的地址都已经被事先修正,而不管它是不是中转的。 typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR
{
DWORD TimeDateStamp;// 时间戳,为-1 是有效的,显示版本
WORD OffsetModuleName; // DLL 名称相对目录开头的偏移 模块名称偏移
WORD NumberOfModuleForwarderRefs; //DLL中转的其他DLL的数目 未使用 Array of zero or more IMAGE_BOUND_FORWARDER_REF
//follows多个IMAGE_BOUND_FORWARDER_REF
} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;
从这种新的绑定方式来看,我不明白它能带来什么样的用处,如果只是强制所有的符号都进行重定位的话 那只需要强制PE loader完成地址修正就可以了。
它的作用感觉只是强调了绑定的信息,通过单独列出DLL 的"版本"来决定是否需要重新计算
l.输出符号表Section 输出符号表通常存在于DLL中,通过IMAGE_DIRECTORY_ENTRY_EXPORT可以直接得到数据的入口,它的属性至少包括IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ, 不可丢弃,典型的段名称:".edata"
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;DLL的特性,保留
DWORD TimeDateStamp; 时间戳,不一定有效
WORD MajorVersion; 主版本
WORD MinorVersion; 次版本号
DWORD Name;名字的RVA
DWORD Base;基址(就是导出函数的起始下标)
DWORD NumberOfFunctions; 输出的函数数目
DWORD NumberOfNames; 输出的名字的数目
DWORD AddressOfFunctions; // RVA from base of image 输出的函数地址数组地址的RVA DWORD AddressOfNames; // RVA from base of image 输出函数名字数组地址的RVA DWORD AddressOfNameOrdinals; // RVA from base of image 函数名字对应的输出函数所在 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来描述的 typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; 资源属性,保留
DWORD TimeDateStamp; 时间戳,资源生成时间
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;资源名称的数目
WORD NumberOfIdEntries;资源ID的数目 // IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[]; } 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",如果镜像不能加载到预定的位置,那么重定位信息就是必须的,链接器提供的地址就不再有效,PE Loader需要对静态变量,字符串变量使用绝对的地址进行访问。 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress;重定位目标块基本的RVA DWORD SizeOfBlock;重定位块数据的大小 // WORD TypeOffset[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位值 | 常量表示 | 含义 |
0 | IMAGE_REL_BASED_ABSOLUTE | 使块按照32位对齐,位置为0 |
1 | IMAGE_REL_BASED_HIGH | 高16位必须应用于偏移量所指高字16位 |
2 | IMAGE_REL_BASED_LOW | 低16位必须应用于偏移量所指低字16位 |
3 | IMAGE_REL_BASED_HIGHLOW | 全部32位应用于所有32位 |
4 | IMAGE_REL_BASED_HIGHADJ | 需要32位,高16位位于偏移量,低16位位于下一个偏移量数组元素,组合为一个带符号数,加上32位的一个数,然后加上8000然后把高16位保存在偏移量的16位域内 |
5 | IMAGE_REL_BASED_MIPS_JMPADDR | 资料不详 |
6 | IMAGE_REL_BASED_SECTION | 资料不详 |
7 | IMAGE_REL_BASED_REL32 | 资料不详 |
4、重定位项数 = (SizeOfBlock - 4 - 4) / 2
5、重定位块结束,以IMAGE_BASE_RELOCATION结构的VirtualAddress值为0结束。因此若映像加载00400000h,则代码加载地址为00401000h
本文内容个人通过网络,整理,收集。