边看边翻译,其中敲字错误或表述有误再所难免。如果您发现错误,而且也有时间,敬请留言告之,以便更正。 请尊重作者及译者工作之艰辛,若转摘,请务必注明出处如下:
------------------------------------------------------------------
《代码反汇编:IDA Pro与Soft ICE之应用》
英文名称:Disassembling Code IDA Pro and Soft ICE
作者:Vlad Pirogov
译者:罗祥勇 <E-mail:solo_lxy@126.com>
出自:CSDN Blog <背你走天涯>专栏
------------------------------------------------------------------
1.5 PE模块的结构
本节的主要目的是描述PE(Portable Executable)模块的结构――一种可执行的模块。任何一个代码挖掘者的主要目的就是学习可执行模块,因此就有必要知道它们的结构。这些信息相当的重要,因为这些结构不但用于可执行文件,也可用语DLL,对象文件(OBJ),和驱动程序。
1.5.1 一般方法
PE格式是从UNIX操作系统中引入的,也被称为通用对象文件格式(COFF)。Microsoft使用了这种格式,并做了大的修改。现在,它依然被广泛应用。正如上面提到的,这个格式不仅仅用于可执行模块,也可用于DLL,同时也可用于内核模式的驱动程序。最让人感兴趣的是PE也用于OBJ文件。你主要目的就是掌握PE的格式,理解其结构并能进行实际应用。
PE模块的主要特征就是很容易被加载至内存,而不需要更多的额外操作。本质上说,PE模块就是主内存区域的快照。
图1.7显示了PE格式的一般设计。第一节(图1.7中最上面的部分)值得特别注意。这里,开发者考虑了对MS-DOS操作系统的兼容。为了更好的理解PE格式的操作机制,有必要对此节做深入的研究。因此,任何可执行模块都以DOS节开始,这样在MS-DOS环境下启动程序时就很必要了。开始的2个字节(MZ)是个签名,表示你正在处理的是MS-DOS可执行模块。MZ签名是微软第一代程序员,Mark Zbikowski,他开发了MS-DOS可执行文件的格式。如果你在MS-DOS下运行PE程序,操作系统的加载器将会读这个签名,识别其为一般的MS-DOS程序,并正常执行。之所以会这样是因为,一个正确的PE模块中MZ签名以后就是MS-DOS头,也就是说,它包含了一小段存根程序。这个存根程序通常会显示一段文字告诉用户当前的程序不能在MS-DOS环境下执行,之后结束程序的执行。标准的存根程序如列表1.28。
列表1.28 标准MS-DOS存根程序
PUSH CS ; Data register matches the code register. POP DS MOV DX, OFFSET MSG MOV AH, 9 ; Output the MSG text string. INT 21H MOV AX, 4C01H ; Exit the program with code 1. INT 21H MSG DB ' This program cannot be run in DOS mode $' |
图1.7 PE文件格式
代码可能不同。但是,影响不大,因为纯正的MS-DOS程序不会遇到了。因此,这个存根程序从来没得到过控制。分析MZ文件头最方面的方法就是分析winnt.h文件中的IMAGE_DOS_HEADER结构。该结构显示如下(列表1.29)。
列表1.29 IMAGE_DOS_HEADER结构
struct IMAGE_DOS_HEADER { // DOS EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on the last page of the file WORD e_cp; // Pages in the file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of the 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_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of the 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 the new EXE header } |
只有三个域对分析MZ文件头有用。e_magic域表示MZ签名。e_kfarkc
域(文件开始偏移18H的地方)的初衷是保存可重定位表的地址。可重定位表被MS-DOS加载器使用,以配置程序中的相对地址。如果本域包含40H个字节,则该文件就是PE模块[8]。但显然,Windows操作系统并不检查这个域的内容;结构导致,即使该域为40H也不能确定就是PE文件。最后一个就是e_lfanew域,它包含PE文件头(见图1.7)的相对地址(从文件开始的偏移)。这个地址必须包含PE模块的签名,字符P和E。
列表1.30显示了一段可用于确定某个文件是否为PE模块的小程序。被检查的模块名字需要在命令行中指定。
列表1.30 一段可用于确定某个文件是否为PE模块的小程序
#include <windows.h> #include <stdio.h> HANDLE openf(char *) ; HANDLE hf; IMAGE_DOS_HEADER id; IMAGE_NT_HEADERS iw; // The main function int main(int argc, char* argv[]) { DWORD n; int er = 0; LARGE_INTEGER 1; // Check whether parameters are present. if(argc < 2){printf("No parameters!/n"); er = 1; goto _exit;}; // File name is the first in the list. if((hf = openf(argv[l])) == INVALID_HANDLE_VALUE) { printf("No file!/n"); er = 2; goto _exit;}; // Determine the file length. GetFileSizeEx(hf, &1); // Read the MS-DOS header. if(!ReadFile(hf, &id, sizeof(id), &n, NULL)) { printf("Read DOS_HEADER error 1!/n"); er = 3; goto _exit;}; if(n < sizeof(id)) { printf("Read DOS_HEADER error 2!/n"); goto _exit;}; // Check the MS-DOS signature ('MZ'). if(id.e_magic != IMAGE_DOS_SIGNATURE) { printf("No DOS signature!/n"); er = 5; goto _exit;} printf("DOS signature is OK!/n"); if(id.e_lfanew > l.QuadPart) { printf("No NT signature!/n"); er = 6; goto _exit;}; // Move the pointer. SetFilePointer(hf, id.e_lfanew, NULL, FILE_BEGIN); // Read the NT header. if(!ReadFile(hf, &iw, sizeof(iw), &n, NULL)) { printf("Read NT_HEADER error 1!/n"); er = 7; goto _exit;}; if(n < sizeof(iw)) { printf("Read NT_HEADER error 2!/n"); er = 8; goto _exit;}; // Check the NT signature ('PE'). if(iw.Signature != IMAGE_NT_SIGNATURE) { printf("No NT signature!/n"); er = 9; goto _exit;} printf("NT signature is OK!/n"); // Close the file descriptor. CloseHandle(hf); return er; }; // Function opens the file for reading. HANDLE openf(char *nf) { return CreateFile(nf, GENERIC_READ, FILE_SHARE_WRITE | FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL); }; |
在知晓了IMAGE_DOS_HEADER之后,我们来看看表示PE文件头的IMAGE_NT_HEADERS结构。这个结构定义在windows.h文件中。同时定义MZ(5A4DH)和PE(4550H)的常量IMAGE_DOS_SIGNATURE和IMAGE_NT_SIGNATURE也包含在此文件中。很自然,列表1.30中的程序并不能保证你处理的是正确的PE文件头。为了达到这个目的,关于PE文件头的更多细节需要学习。
附录1中,有个例子程序,它对PE文件头做了更细致的分析。这个程序基于列表1.30。除了分析文件头,该程序还显示了输入,输出和资源节。
1.5.2 PE文件头
现在,我们来研究PE文件头。先前提到过,这个头用IMAGE_NT_HEADERS结构表示(列表1.31)。
列表1.31 IMAGE_NT_HEADERS结构
struct IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } |
如你所见,本结构由两部分组成,IMAGE_FILE_HEADER和IMAGE_OPTIONAL-HEADER32。还包含签名域,PE。先研究主要的 IMAGE_FILE_HEADER(列表1.32)。
列表1.32 IMAGE_FILE_HEADER结构
Struct IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } |
这个结构的域描述如下:
■ Machine - 处理器类型。对于i80x86,本值为014ch。
■ NumberOfSections – PE模块中节的个数。
■ TimeDateStamp – 文件创建时的日期和时间。
■ PointerToSymbilTable – 本域用于调试。作为规则,本值为0。
■ NumberIfSymbols – 本域用于调试。作为规则,本值为0。
■ SizeOfOptionalHeader – 表示PE头中第二个部分的大小(参见IMAGE_OPTIONAL_HEADERS32结构)。作为规则,本值为224字节。
■ Characteristics – 本域包含标志位信息。特别是第13位表示模块是DLL(0)还是EXE(1)。
现在,看看PE文件头的第二部分 – 可选的文件头(IMAGE_OPTIONAL_HEADERS32)。这个结构见列表1.33。
列表1.33 IMAGE_OPTIONAL_HEADERS32结构
struct IMAGE_OPTIONAL_HEADER { WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOflnitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; DWORD ImageBase; DWORD SectionAlignment; DWORD FileAlignment; WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorlmageVersion; WORD MinorlmageVersion; WORD MajorSubsystemVersion; WORD MinorSubsystemVersion; DWORD Win32VersionValue; DWORD SizeOfImage; DWORD SizeOfHeaders; DWORD CheckSum; WORD Subsystem; WORD DllCharacteristics; DWORD SizeOfStackReserve; DWORD SizeOfStackCommit; DWORD SizeOfHeapReserve; DWORD SizeOfHeapCommit; DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } |
这个结构的域描述如下:
■ Magic – 描述本模块的主要目的。特别地,对PE模块本域为010BH。
■ MajorLinkerVersion – 构建本文件所使用连接器的主要版本。
■ MinorLinkerVersion – 构建本文件所使用连接器的次要版本。
■ SizeOfCode – 指定文件中以字节为单位的可执行代码的大小。
■ SizeOfInitializeData – 指定初始数据节的大小。
■ SizeOfUninitializeData -指定未初始数据节的大小。
■ AddressOfEntryPoint – 指定程序开始执行的指令的相对虚拟地址,这个地址位于可执行文件模块的虚拟地址空间中。如果相对地址(相对于模块开始执行的位置),为1000H并且模块被加载至400000H(参见ImageBase域),则进入点将位于401000H处。
■ BaseOfCode – 第一个代码段的相对虚拟地址。
■ BaseOfData - 第一个数据段的相对虚拟地址。一般情况下,数据段位于代码段之后。
■ ImageBase – 指定模块加载的虚拟地址(不是相对地址)。当加载器放置模块的时候,就从此地址开始,这样将来就不用纠正地址了,而且加载地址也会很快。如果加载器不能将其放置于此位置,就需要额外的地址调节。对于可执行模块,本值通常为400000H。
■ SectionAlignment –指定内存中节的对齐。内存中的所有节必须对齐于这个值的倍数上。
■ FileAlignment -指定文件中节的对齐。文件中的所有节必须对齐于这个值的倍数上。
■ MajorOperationSystemVersion – 启动此程序的Win32子系统的主数字码。
■ MinorOperationSystemVersion – 启动此程序的Win32子系统的次数字码。
■ MajorImageVersion – 指定连接期的主要版本号(即n)。对Link.exe,指定这个数字的命令行选项为/VERSION:n.m。
■ MinorImageVersion – 指定连接期的次要版本号(即m)。对Link.exe,指定这个数字的命令行选项为/VERSION:n.m。
■ MajorSubSystemVersion, MinorSubSystemVersion – 指定子系统的相关版本号。这些域典型情况下未用。
■ Win32VersionValue – 从该域就可以看出它的用途,很多讨论PE文件的文章都表明此域的值必须为0。
■ SizeOfImage – 指定内存中PE文件头(头和节)总的大小,使用SectionAlignment对齐。
■ SizeOfHeaders – 指定文件头大小和节表大小的和。
■ CheckSum – 文件的检验和。对可执行文件,本域为0。
■ SubSystem – 指定模块用于什么子系统。本域的各值描述如下:0000H用于未知子系统;0001H用于设备驱动;0002H用于Windows GUI;0003H用于控制台应用程序;0005H用于OS/2;0007H用于Posix。
■ DllCharacteristics – 从Windows NT 3.5后,无用。
■ SizeOfStackReseve – 指定需要的堆栈大小。
■ SizeOfStackCommit – 指定为堆栈分配的内存空间大小。
■ SizeOfHeapReserve – 指定需要的本地堆的大小。
■ SizeOfHeapCommit – 指定分配的堆的大小。
■ LoaderFlags –从Windows NT 3.5后,无用。
■ NumberIfRvaAndSize – 本域(包含某个结构的数组的大小)用于将来扩展使用。作为规则,本值被设置为10H。
■ DataDirectory – 结构数组(列表1.34)。当前,IMAGE_NUMBEROF_DIRECTORY_ENTRIES的值为16。每个结构包含两个元素,每个元素4个字节。只有开始的12个结构被使用。结构的第一个元素描述了数据的文职(相对虚拟地址),而第二个元素指定数据大小。数组元素的作用描述如下:
0 – 输出函数表
1 – 输入函数表
2 – 资源表
3 – 异常表
4 - 安全表
5 – 节表
6 – 调试表
7 – 字符串描述
8 – 计算机操作速度,单位百万指令/每秒(MIPS)
9 – 线程局部存储(TLS)
10 – 配置表区 域
11 – 输入地址表
列表1.34 IMAGE_ DATA_DIRECTORY结构
struct IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } |
1.5.3 节表
节表紧接在可选PE文件头之后。比较SizeOfOptionalHeader(参照IMAGE_FUKE_HEADER结构)的值和sizeof(IMAGE_NT_HEADER) – sizeof(IMAGE_FILE_HEADER) – 4的值是可能的。然后就可以通过下面的值从文件开始访问该表了:e_lfanew + sizeof(IAMGE_NT_HEADERS)。
节表由40个字节的结构组成。节的个数由NumberIfSection字段(参见列表1.32中的IMAGE_FILE_HEADER结构)指定。因此,获取节的列表就很简单了。列表1.35显示节表中每个元素的结构。
列表1.35 IMAGE_ DATA_DIRECTORY结构
struct IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } |
每个字段的解释如下:
■ Name – 节的名字。IMAGE_SIZEOF_SHORT_NAME等于8。如果节的名字短于8个字节,则剩下的字节填充为0。
■ VirtualSize – 节需要的内存数量。
■ VirtualAddress – 相对虚拟地址,指示加载器从何处加载节的数据。
■ SizeOfRawData – 虚拟节对齐(根据列表1.33中的结构IAMGE_OPTIONAL_HEADER结构中的FileAlignment的字段)后的大小。
■ PointerToRawData – 节在文件中的偏移。
■ PointerToRelocatons, PointerToLineNumbers, NumberOfRelocations和NumberOfLineNumbers – 用于OBJ文件,这里不予考虑。
■ Characteristics – 节的特性标志(见表1.29)。
表1.29 特性标志的值
值 | 描述 |
00000020H | 本节包含程序代码 |
00000040H | 本节包含初始化数据 |
00000080H | 本节包含未初始化数据 |
00000200H | 本节被编译器使用 |
00000800H | 本节被编译器使用 |
04000000H | 本节不能被缓存 |
08000000H | 本节没有按页组织 |
10000000H | 为共享节 |
20000000H | 为可执行节 |
40000000H | 为只读节 |
80000000H | 为可写节 |
节的名字和目的是为了个编译器使用。
注意:你可以创建自定义节,而且给予它任何名字。例如,你可以写一个汇编程序,并赋予它一个任意名字。程序是可以正常运行的,但是,某些调试器和反汇编器可能被弄糊涂,因为程序进入点在一个未知名的节中。
这里还有一个Microsoft和Borland编译器创建的不完整的节列表:
■ .text – 包含可执行代码(Microsoft)。
■ CODE – 包含可执行代码(Borland)。
■ .DATA – 包含未初始化的全局数据(Microsoft)。
■ DATA – 包含未初始化的全局数据(Borland)。
■ .bss – 本节中的所有数据都未初始化。本节在文件中的大小为0。
■ .CRT – 另外一个包含初始化数据的节(Microsoft)。
■ CRT –数据节(Borland)。
■ .rdata – 包含只读数据的节(常量和调试信息)。
■ .rsrc – 包含资源的节。
■ .edata – 包含导出函数的节。
■ .idata - 包含导入函数的节。
■ .reloc – 包含设置的表。这里包含的信息在Windows加载器由于某些原因加载模块到指定地址而不是PE文件头中指定的位置时使用。本表中包含的是程序中使用的地址在内存中的相对地址,他们的值在加载的过程中可能被修改。本表也可以称之为重定位表。更多关于重定位表的信息在2.1.1节我们再做详细阐述。
■ .icode - 老版本的link32.exe中用于跳转到输出函数。
■ .debug – 本节包含了调试信息。
因此,使用重定位表,你可以计算文件中节的位置,同时也可计算它的大小。当达到目的后,你可以查看节中的数据,获取它的列表,或者反汇编代码。
对输入/输入函数节和资源节我们需要特别注意。这些问题我们在接下来的节中继续讨论。但是,在考察这些问题之前,有必要弄清楚PE文件映像是如何存在于虚拟内存中的。这和直接拷贝PE文件是不同的。加载PE文件的简单算法可描述如下:
1、所有的文件头,包括DOS文件头、PE文件头(IMAGE_NT_HEADER)、节表都被加载至内存。
2、节开始被加载至内存。同时,他们的相对虚拟地址必须依据SectionAlignment对齐(参见结构IMAGE_OPTIONAL_HEADER)。
从以上描述中可得到什么结论呢?首先,有必要理解文件中某个对象的偏移通过虚拟地址确定。这是个非常重要的问题,和输入/输出函数相关。通常情况下,获取偏移的算法如下:
1、给定对象的所处的节可以通过虚拟地址确定。
2、基于节表,节表在文件中的偏移可以确定。
3、确定节中的偏移。
4、对象的偏移可以通过将文件中节的偏移和对象在节中的偏移相加得到。
列表1.36列举了一个使用C++编写的函数,用于确定相对虚拟地址在PE文件中的偏移。同时它假设iw = IMAGE_NT_HEADERS全局结构,ais全局数组包含IMAGE_SECTION_HEADER结构(见列表1.35)。输入参数vsm是要获取对象的相对虚拟地址。该函数返回PE文件中指定对象的偏移。
列表1.36 用于确定相对虚拟地址在PE文件中的偏移用C++编写的函数
DWORD getoffs(DWORD vsm) { DWORD fi = 0; if(vsm < ais[0].VirtualAddress)return fi; for(int i = 0; I < iw.FileHeader.NumberOfSections; i++) { if(vsm < ais[i].VirtualAddress && i > 0){ fi = ais[i - 1].PointerToRawData + (vsm - ais[i -1].VirtualAddress); break; }; }; if(i = iw.FileHeader.NumberOfSections) fi = ais[i - 1].PointerToRawData + (vsm - ais[i - 1].VirtualAddress); return fi; } |
1.5.4 输入节
需要指出的是,如果你想通过搜索.idata找到节表将会失败。连接器(至少Microsoft提供的)并不创建这样的节。因此,有必要使用IMAGE_OPTIONAL_HEADER结构中的DataDirectory数组(见列表1.33)。为了对可执行模块做初步的探索,可能需要使用附录1中的程序。你立马就会发现尽管输入表存在,许多可执行文件中都没有.idata节。如果.idata节存在,输入表就在这个位置。
回忆以下,DataDirectory中有12个重要的元素(总的元素个数为16)。每个元素包含两个字段:VirtualAddress,对象的虚拟地址;Size,对象的大小(见列表1.34)。输入表通过第二个字段定义(索引为1)。这是确定输入表的唯一可信来源。但是,这就够了。回想以下上节介绍的内容,在好好看看列表1.36。因此,查找输入表是没什么问题的。现在,就剩下理解它的结构了。
输入表的前面有一个结构数组,参见1.37。
列表1.37输入表的前面有一个结构数组
struct IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } |
数组以全为0的字段结束。有必要再次指出,至少有两个字段必须确定为0,例如,Characteristics和Name。现在,研究下列表1.37:
■ Characteristics – 另外一个包含输入函数相对虚拟地址数组的相对虚拟地址。
■ TimeDateStamp – DLL创建的日期和时间,或为0。
■ ForwarderChain – 通常为0FFFFFFFFH
■ Name – 包含输入库(DLL)名字字符串的地址。因此,数组中的每个元素都和DLL相关。
■ FirstThunk – 包含输入函数名字数组的相对虚拟地址。它是Characteristics字段的第二个拷贝。如果Charateristics字段为0(Microsoft编译器之外的编译器),则有必要检查FirstThunk字段。
注意:希望你理解这里遇到的和可执行模块相关的DLL不是通过调用LoadLibrary API函数得到的。
现在,Characteristics和FirstThunk指向的数组。有再次必要指出,有两个不同数组,尽管他们的元素指向输入函数的同一个名字。这些数组的元素由以下结构组成(列表.38)。
列表1.38 IMAGE_THUNK_DATA32结构
struct IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } ul; } |
如你所见,本质上,IMAGE_THUNK_DATA32结构只包含一个单独的字段;但是它有四种形式。本字段指定DLL输入函数的相对虚拟地址。如果字段的高位字为8000H,则低位的字包含原始输入函数的个数(通过原始数据导出)。这个数组以双字为0结束。
最后,有必要研究以下输入函数的名字结构。不必深入,注意函数名字是简单的ASCII字符串,以0结束。但是,名字的开始地址通过IMAGE_THUNK_DATA32结构加2计算得来。前面的两个字节包含给定DLL输入函数的数目。
IMAGE_IMPORT_DISCRIPTOR结构(见列表1.37)中通过FirstThunk指定的数组值得特别关注。CALL指令,直接指向数组中元素(例如,CALL DWORD PTR [address]或者MOV ESI, address;CALL ESI)或通过调用存根(JMP DWORD PTR [address])。模块加载的时候,加载器通过数组中名字或原始地址确定内存中函数的实际地址。通过Charateristics字段指定的数组在加载的过程中不被改变。一个更详细的通过输入函数查找名字的过程将在1.6.1节中介绍。
1.5.5 输出节
对于DLL,输出表是必要的,它用来确保应用程序可以正确调用DLL提供的函数。和输入表一样,探索输出表,有必要使用IMAGE_NT_HEADER结构中的DataDirectory数组,因为可执行模块中可能丢失了.edata节。这种情况下,你就需要这个数组的第一个元素了(索引为0)。
IMAGE_EXPORT_DIRECTORY结构位于特定位置。这个结构包含了探索输出函数表的所有信息(列表1.39)。
列表1.39 IMAGE_EXPORT_DIRECTORY结构
struct IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNarnes; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } |
各个字段描述如下:
■ Characteristics – 本字段保留,总设置为0。
■ TimeDataStamp – 创建输出函数时的日期和时间,或为0。
■ MajorVersion – 输出函数表版本的主要部分,通常为0。
■ MinorVersion – 输出函数表版本的次要部分,通常为0。
■ Name – 输出模块的名字。原则上,它和文件名相同。
■ Base – 输出函数的原始个数。输出函数,除了名字,有一个原始数目,也可访问。
■ NumberOfNames – 给出数组输出函数名的个数。
■ NumberOfFunctions -给出数组输出函数地址的个数。
■ AddressOfFunctions – 输出函数虚拟地址数组的相对虚拟地址。
■ AddressOfNames -输出函数名字数组的相对虚拟地址。
■ AddressOfNameOrdinals – 16位(原始数组)相对虚拟地址数组,通过获取数组的索引可以得到输出函数。为了获得原始的函数,有必要在索引值上加上Base字段。
为了更好的理解获取输出函数信息的机制,有必要理解以下三个数组之间的关系:函数地址数组,名字数组,原始数组。原始数组连接前两个数组。名字数组中元素的个数也原始数组中元素个数是相等的。因此,通过名字获取函数地址,有必要完成以下步骤:
1、通过函数名字找到名字数组中的函数。
2、通过从名字数组中获取到的索引,从原始数组中此索引的值。
3、通过上步中获取的值,将其视为索引访问函数地址数组即可得到相应的地址。
分析附录1中的程序,看输出函数是如何处理的。针对不同的可执行文件和DLL分别做不同的实验。
1.5.6 资源节
和前面的讲的一样,为了获取资源块,有必要IMAGE_NT_HEADER结构中的DataDirectory数组。你需要这个数组的第三个元素了(索引为2)。比较先前探讨的PE模块中的对象,资源节有个层次结构的结构体。实际情况下,使用四层结构。除此之外,资源节中使用的所有地址都从资源节的开始寻址(也就是说,这里不再是相对虚拟地址了)。这是很自然地,因为当资源访问的时候才被载如内存,而不是模块加载期间。
本质上说,为了理解资源的结果,只需要列表1.40和1.41中的结构。
列表1.40 IMAGE_RESOURCE_DIRECTORY结构
struct IMAGE_RESOURCE_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; WORD NumberOfNamedEntries; WORD NumberOfIdEntries; } |
列表1.41 IMAGE_RESOURCE_DIRECTORY_ENTRY结构
struct IMAGE_RESOURCE_DIRECTORY_ENTRY { ULONG Name; ULONG OffsetToData; } |
IMAGE_RESOURCE_DIRECTORY结构的字段描述如下:
■ Characteristics – 标志字段,现在几乎没用。
■ TimeStamp – 指定创建资源的日期和时间。
■ MajorVersion和MinorVersion – 指定资源的主要和次要版本。实际未用。
■ NumberOfNameEntries – 命名资源的个数。
■ NumberOfIdEntries – 通过资源id指定的资源个数。
IMAGE_RESOURCE_DIRECTORY_ENTRY结构的字段描述如下:
■ Name – 这个字段可以有不同的解释,依赖于它所处的级别和最要位的值。下面将会所有这些情况做介绍。
■ OffsetToData – 指定相对于资源起始地址的地址。被该地址指定的对象可以相互独立。
因此,为了到达DataDirectory数组第二个元素(索引为2)指定的地址,就可以访问实际的资源。这里就是第一个层次结构的开始。有必要指出,如果地址为0,则表明资源可能丢失。
层次结构中的第一层
在资源结构的顶层,是IMAGE_RESOURCE_DIRECTORY结构(见列表1.42)。可用来探索资源的唯一字段是NumberOfEntries。在第一层中,这个字段包含了存储在PE文件头中的资源类型的个数。NumberOfNameEntries字段在第一层中没有任何意义。
列表1.42 winuser.h文件片段
#define RT_CURSOR 1 #define RT_BITMAP 2 #define RT_ICON 3 #define RT_MENU 4 #define RT_DIALOG 5 #define RT_STRING 6 #define RT_FONTDIR 7 #define RT_FONT 8 #define RT_ACCELERATOR 9 #define RT_RCDATA 10 #define RT_MESSAGETABLE 11 #define RT_GROUP_CURSOR 12 #define RT_GROUP_ICON 14 #define RT_VERSION 16 #define RT_DLGINCLUDE 17 #define RT_PLUGPLAY 19 #define RT_VXD 20 #define RT_ANICURSOR 21 #define RT_ANIICON 22 #define RT_HTML 23 #define RT_MANIFEST 24 |
知道了资源类型对应的数字后能干什么呢?事实上,它就是关键字段,因为IMAGE_RESOURCE_DIRECTORY结构直接跟在IMAGE_RESOURCE_DIRECTORY_ENTRY结构的后面(见列表.41)。他们和NumberOfIdEntries字段相等,因此,一个个的读取他们是没任何问题的。顶层IMAGE_RESOURCE_DIRECTORY_ENTRY的Name字段包含了资源类型的标识符。资源类型标识符可以在Visual Studio .NET中的winuser.h文件中找到。
因此,在资源层次结构的顶层,可以在模块中找到有多少中资源类型,然后一个个的标识他们。
所有元素指向IMAGE_RESOURCE_DIRECTORY结构的OffseToData字段都位于层次结构的第二级。
层次结构中的第二层
层次结构的第二层也是以IMAGE_RESOURCE_DIRECTORY开始。结构体的数目等于模块中资源类型的个数。在这个结构中,下面的两个字段很重要:NumberOfEntries和NumberOfIdEntries。第一个字段包含命名资源的个数,第二个字段指定以ID标识的资源的个数。因此,在第二层中,每个IMAGE_RESOURCE_DIRECTORY结构后面直接跟着IMAGE_RESOURCE_DIRECTORY_ENTRY结构。元素的个数等于NumberOfEntries+NumberOfIdEntries。这个由IMAGE_RESOURCE_DIRECTORY_ENTRY组成的数组值得关注。Name字段现在必须解释为不同的意思。如果这个字段的高为设置为0,则字段本身表示资源ID。如果字段的高位设置为1,则其他的位必须解释为给定资源相对于资源开始位置的名称的偏移。名字的结构描述如下:开始的两个字节字顶名称的长度(以字符记而不是字节),接下来是Unicode字符。
再次看看OffsetToData字段。第二层次结构中每个IMAGE_RESOURCE_DIRECTORY_ENTRY结构的该字段都指向同一个结构,除非它属于第三个层次结构。
层次结构中的第三层
分支停止于第二层。第三层的IMAGE_RESOURCE_DIRECTORY_ENTRY结构数组和第二层一样。看看这些结构的字段在第三层中是如何解释的。Name字段定义了资源描述语言的标识。所有的标识数都定义在winnt.h文件中,他们以LANG_前缀开始,这里就不再列出了。至于OffsetToData字段,它再次指向IMAGE_RESOURCE_DIRECTORY_ENTRY结构,除非它属于第四层。
层次结构中的第四层
在层次结构的第四层中,IMAGE_RESOURCE_DIRECTORY_ENTRY的Name字段定义给定资源中二进制图象的大小。它的地址(同样也是以资源的开始位置记数)就是二进制资源描述在内存中的地址。这个使用OffsetToData表示。
到此,我们对资源的描述就完了。有必要指出附件1中的程序只分析了两层资源,大多数情况下这也足够了。
1.5.7 关于调试信息
如果不对调试信息进行描述的话,那对PE模块结构的描述就不算完整。附件1中的程序只会告诉你PE模块中是否有这样的信息(符号表和调试信息)。
符号表
符号表的位置可以通过文件头中的FileHeader字段确定。PointerToSymbalTable字段包含了符号表的相对虚拟地址。如果本字段为0,则没有符号表。符号表是什么呢?从名字中并没反映出其用法。符号表包含以下信息:符号的名字(函数或变量的名字),符号的相对虚拟地址,符号的类型(函数或变量),其内存类别(自动的,寄存器的,标识符,等等)。这些信息都用IMAGE_SYMBAL结构标识,其定义在winnt.h文件中。
调试信息
本质上说,调式信息必须理解为指定程序各代码行的信息。这些信息保存在PE模块中,但不在符号表中。定位这些信息并不简单。达到此目的需要额外的努力。首先,有必要先定位IAMGE_DEBUG_DIRECTORY结构。在IMAGE_NT_HEADER结构中,它位于此结构DataDirectory数组的第6位(索引为6)。如果PE文件包含多种类型的调试信息,则每种类型对应一个IMAGE_DEBUG_DIRECTORY结构。该结构的TYPE字段定义调试信息的类型。调式信息的类型定义在winnt.h文件中,常量以IMAGE_DEBUG_TYPE_为前缀。例如,值1表示COFF调式信息,值9(IMAGE_DEBUG_TYPE_BORLAND)表示对应Borland调试信息。如果类型字段设置为1,IMAGE_DEBUG_DIRECTORY结构的PointerToRawData字段定义了COFF中调试信息的偏移,此偏移从调试信息的开始块记数。在这个位置必须存在IMAGE_COFF_SYMBOLS_HEADER结构都必须存在。这是很关键的。这个结构不仅包含符号表还包含代码行表。NumberOfSymbols字段包含符号表中标识符的个数。这个数字和IMAGE_FILE_HEADER结构中的NumberOfSymbols字段(参见列表1.32)相等。LvaToFirstSymbol字段包含符号表的偏移,从IMAGE_COFF_SYMBOL_HEADERS结构处开始记数。你还可以使用其他的理论访问符号表。最后,LvaToFirstLinenumber字段包含了COFF中代码行表的偏移,以此结构体开始记数。
[7] 所有PE文件使用的结构都从头文件中得来的。
[8] 或者这个文件在Windows 3.1中用于NE模块。这样的程序现在已经不存在了。