如何探索win32可执行文件格式?
深度探索win32可执行文件格式
Matt Pietrek 翻译:姜庆东
摘要: 对可执行文件的深入认识将带你深入到系统深处。如果你知道你的exe/dll里是些什么东东,你就是一个更有知识的程序员。作为系列文章的第一章,将关注这几年来PE格式的变化,同时也简单介绍一下PE格式。经过这次更新,作者加入了PE格式是如何与.NET协作的及PE文件表格(PE FILE SECTIONS),RVA,The DataDirectory,函数的输入等内容。
一
很久以前,我给Microsoft Systems Journal(现在的MSDN)写了一篇名为“Peering Inside the PE: A Tour of the Win32 Portable Executable File Format, “的文章,后来比我期望的还流行,到现在我还听说有人在用它(它还在MSDN里)。不幸的是,那篇文章的问题依旧存在,WIN32的世界静悄悄地变了好多,那篇文章已显得过期了。从这个月开始我将用这两篇文章来弥补。
你可能会问为什么我应当了解PE格式,答案依旧:操作系统的可执行文件格式和数据结构暴露出系统的底层细节。通过了解这些,你的程序将编的更出色。
当然,你可以阅读微软的文档来了解我将要告诉你的。但是,像很多文档一样,‘宁可晦涩,但为瓦全‘。
我把焦点放在提供一些不适合放在正式文档里的内容。另外,这篇文章里的一些知识不见得能在官方文档里找到。
1. 裂缝的撕开。
让我给你一些从1994年我写那篇文章来PE格式变化的例子。WIN16已经成为历史,也就没有必要作什么比较和说明了。另外一个可憎的东西就是用在WINDOWS 3.1 中的WIN32S,在它上面运行程序是那么的不稳定。
那时候,WINDOWS 95 (也叫Chicago)还没有发行。NT还是3.5版。微软的连接器还没开始大规模的优化,尽管如此,there were MIPS and DEC Alpha implementations of Windows NT that added to the story.
那么究竟,这么些年来,有些什么新的东西出来呢?64位的WINDOWS有了它自己的PE变种,WINDOWS CE 支持各种CPU了,各种优化如DLL的延迟载入,节表的合并,动态捆绑等也已出台。
有很多类似的东西发生了。
让我们最好忘了。NET。它是如何与系统切入的呢?对于操作系统,.NET的可执行文件格式是与旧的PE格式兼容的。虽然这么说,在运行时期,.NET还是按元数据和中间语言来组织数据的,这毕竟是它的核心。这篇文章当中,我将打开.NET元数据这扇门,但不做深入讨论。
如果WIN32的这些变化都不足以让我重写这篇文章,就是原来的那些错误也让我汗颜。比如我对TLS的描述只是一带而过,我对时间戳的描述只有你生活在美国西部才行等等。还有,一些东西已是今是作非了,我曾说过.RDATA几乎没排上用场,今天也是,我还说过.IDATA节是可读可写的,但是一些搞API拦截的人发现好像是错的。
在更新这篇文章的过程当中,我也检查了PEDUMP这个用来倾印PE文件的程序.这个程序能够在0X86和IA-64平台下编译和运行。
2. PE 格式概览
微软的可执行文件格式,也就是大家熟悉的PE 格式,是官方文档的一部分。但是,它是从VAX/VMS上的COFF派生出来的,就WINDOWS NT小组的大部分是从DEC转过来的看来,这是可以理解的。很自然,这些人在NT的开发上会用他们以往的代码。
采用术语’PORTABLE EXECUTABLE’是因为微软希望有一个通用在所有WINDOWS平台上和所有CPU上的文件格式。从大的方面讲,这个目标已经实现。它适用于NT及其后代,95及其后代,和CE.
微软产生的OBJ文件是用COFF格式的。当你看到它的很多域都是用八进制的编码的,你会发现她是多么古老了。COFF OBJ文件用到了很多和PE一样的数据结构和枚举,我马上会提到一些。
64位的WINDOWS只对PE格式作了一点点改变。这个新的格式叫做PE32+.没有增加一个字段,且只删了一个字段。其他的改变就是把以前的32位字段扩展成64位。对于C++代码,通过宏定义WINDOWS的头文件已经屏蔽了这些差别。
EXE与DLL的差别完全是语义上的。它们用的都是同样一种文件格式-PE。唯一的区别就是其中有一个字段标识出是EXE还是DLL.还有很多DLL的扩展比如OCX,CPL等都是DLL.它们有一样的实体。
你首先要知道的关于PE的知识就是磁盘中的数据结构布局和内存中的数据结构布局是一样的。载入可执行文件(比如LOADLIBARY)的首要任务就是把磁盘中的文件映射到进程的地址空间.因此像IMAGE_NT_HEADER(下面解释)在磁盘和内存中是一样的。关键的是你要懂得你怎样在磁盘中获得PE文件某些信息的,当它载入内存时你可以一样获得,基本上是没什么不同的(即内存映射文件)。但是知道与映射普通的内存映射文件不同是很重要的。WINDOWS载入器察看PE文件才决定映射到哪里,然后从文件的开始处往更高的地址映射,但是有的东西在文件中的偏移和在内存中的偏移会不一样。尽管如此,你也有了足够的信息把文件偏移转化成内存偏移。见图1
Figure 1 Offsets
当windows载入器把PE载入内存,在内存中它称作模块(MODULE),文件从HMODULE这个地址开始映射。记住这点:给你个HMODULE,从那你可以知道一个数据结构(IMAGE_DOS_HEADER),然后你还可以知道所有得数据结构。这个强大的功能对于API拦截特别有意义。(准确地说:对于WINDOWS CE,这是不成立的,不过这是后话)。
内存中的模块代表着进程从这个可执行文件中所需要的所有代码,数据,资源。其他部分可以被读入,但是可能不映射(如,重定位节)。还有一些部分根本就不映射,比如当调试信息放到文件的尾部的时候。有一个字段告诉系统把文件映射到内存需要多少内存。不需要的数据放在文件的尾部,而在过去,所有部分都映射。
在WINNT.H描述了PE 格式。在这个文件中,几乎有所有的关于PE的数据结构,枚举,#DEFINE。当然,其它地方也有相关文档,但是还是WINNT.H说了算。
有很多检测PE文件的工具,有VISUAL STUDIO的DUMPBIN,SDK中的DEPENDS,我比较喜欢DEPENDS,因为它以一种简洁的方式检测出文件的引入引出。一个免费的PE察看器,PEBrowse,来自smidgenosoft。我的pedump也是很有用的,它和dumpbin有一样的功能。
从api的立场看,imagehlp.dll提供了读写pe文件的机制。
在开始讨论pe文件前,回顾一下pe文件的一些基本概念是有意义的。在下面几节,我将讨论:pe 节,相对虚拟地址(rva),数据目录,函数的引入。
3. PE节
PE节以某钟顺序表示代码或数据,代码就是代码了,但是却有多种类型的数据,可读写的程序数据(如全局变量),其它的节包含API的引入引出表,资源,重定位。每个节有自己的属性,包括是否是代码节,是否只读还是可读可写,节的数据是否全局共享。
通常,节中的数据逻辑上是关联的。PE文件一般至少要有两个节,一个是代码,另一个为数据,一般还有一个其它类型的数据的节。后面我将描述各种类型的节。
每个节都有一个独特的名字。这个名字是用来传达这个节的用途的。比如,.RDATA表示一个只读节,节的名字对于操作系统毫无意义,只是为了人们便于理解。把一个节命名为FOOBAR和.TEXT是一样有用的。微软给他们的节命名了个有特色的名字,但是这不是必需的。Borland的连接器用的是code和data。
一般编译器将产生一系列标准的节,但这没有什么不可思议的。你可以建立和命名自己的节,连接器会自动在程序文件中包含它们。在visual c++中,你能用#pragma指令让编译器插入数据到一个节中。像下面这样:
#pragma data_seg( "MY_DATA" )
。。。有必要初始化
#pragma data_seg()
你也可以对.data做同样的事。大部分的程序都只用编译器产生的节,但是有时候你却需要这样。比如建立一个全局共享节。
节并不是全部由连接器确定的,他们可以在编译阶段由编译器放入obj文件。连接器的工作就是合并所有obj和库中需要的节成一个最终的合适的节。比如,你的工程中的所有obj可能都有一个包含代码的.text节,连接器把这些节合并成一个.text节。同样对于.data等;这些主题超出了这篇文章的范围了。还有更多的规则关于连接器的。在obj文件中是专门给linker用的,并不放入到pe文件中,这种节是用来给连接器传递信息的。
节有两个关于对齐的字段,一个对应磁盘文件,另一个对应内存中的文件。Pe文件头指出了这两个值,他们可以不一样。每个节的偏移从对齐值的倍数开始,比如,典型的对齐值时0x200,那么每个节的的偏移必须是0x200的倍数。一旦载入内存,节的起始地址总是以页对齐。X86cpu的页大小为4k,al-64为8k
下面是pedump倾印出的Windows XP KERNEL32.DLL.的.text .data节的信息:
Section Table
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
建立一个节在文件中的偏移和它相对于载入地址的偏移相同的pe文件是可能的。在98/me中,这会加速大
文件的载入。Visual studio 6.0 的默认选项 /opt:win98j就是这样产生文件的。在Visual studio.net中是否用/opt:nowin98取决于文件是否够校
一个有趣的连接器特征是合并节的能力。如果两个节有相似兼容的属性,连接的时候就可以合并为一个节。这取决于是否用/merger开关。像下面就把.rdata和.text合并为一个节.text
/MERGE:.rdata=.text
合并节的优点就是对于磁盘和内存节省空间。每个节至少占用一页内存,如果你可以把可执行文件的节数从4减到3,很可能就可以少用一页内存。当然,这取决于两个节的空余空间加起来是否达到一页。
当你合并节事情会变得有意思,因为这没有什么硬性和容易的规则。比如你可以合并.rdata到.text,
但是你不可以把.rsrc.reloc.pdata合并到别的节。先前Visual Studio .NET允许把.idata合并,后来又不允许了。但是当发行的时候,连接器还是可以把.idata合并到别的节。
因为引入节的一部分在载入器载入事将被写入,你可能惊奇它是如何被放入一个只读节的。是这样的,在载入的时候系统会临时改变那些包含引入节的页为可读可写,初始化完成后,又恢复原来属性。
4. 相对虚拟地址
在可执行文件中,有很多地方需要指定内存地址,比如,引用全局变量时,需要指定它的地址。Pe文件尽管有一个首选的载入地址,但是他们可以载入到进程空间的任何地方,所以你不能依赖于pe的载入点。由于这点,必须有一个方法来指定地址而不依赖于pe载入点的地址。为了避免把内存地址硬编码进pe文件, 提出了RVA。RVA是一个简单的相对于PE载入点的内存偏移。比如,PE载入点为0X400000,那么代码节中的地址0X401000的RVA为
(target address) 0x401000 - (load address)0x400000 = (RVA)0x1000.
把RVA加上PE的载入点的实际地址就可以把RVA转化实际地址。顺便说一下,按PE的说法,内存中的实际地址称为VA(VIRTUAL ADDRESS).不要忘了早点我说的PE的载入点就是HMODULE。
想对探索内存中的任意DLL吗?用G etModuleHanle(LPCTSTR)取得载入点,用你的PE知识来干活吧
5. 数据目录
PE文件中有很多数据结构需要快速定位,显然的例子有,引入函数,引出函数,资源,重定位。这些东西是以一致的方式来定位的,这就是数据目录。
数据目录是一个结构数组,包含16个结构。每个元素有一个定义好的标识,如下:
// Export Directory
#define IMAGE_DIRECTORY_ENTRY_EXPORT
0
// Import Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT
1
// Resource Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE
2
// Exception Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION
3
// Security Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY
4
// Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_BASERELOC
5
// Debug Directory
#define IMAGE_DIRECTORY_ENTRY_DEBUG
6
// Description String
#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT
7
// Machine Value (MIPS GP)
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR
8
// TLS Directory
#define IMAGE_DIRECTORY_ENTRY_TLS
9
// Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
10
typedef struct _IMAGE_DATA_DIRECTORY {
ULONG
VirtualAddress;
ULONG
Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
6. 引入函数
当你使用别的DLL中的代码或数据,称为引入。当PE载入时,载入器的工作之一就是定位所有引入函数及数据,使那些地址对于载入的PE可见。具体细节在后面讨论,在这里只是大概讲一下。
当你用到了一个DLL中的代码或数据,你就暗中连接到这个DLL。但是你不必为把这些地址变得对你的代码有效做任何事情,载入器为你做这些。方法之一就是显式连接,这样你就要确定DLL已被载入,及函数的地址。调用LOADLIBARY和GETPROCADDRESS就可以了。
当你暗式连接DLL,
本文地址:http://www.45fan.com/dnjc/67979.html