JIURL PE 格式学习总结(二)-- PE文件中的输出函数
作者: JIURL
日期: 2003-4-24
一般来说输出函数都是在dll中。我们将详细介绍关于输出函数的各种结构,通过一个例子来说明输出函数及其相关结构是怎么放在PE文件中的。以及如何在PE文件中找到这些东西。
一 找到输出函数在文件中位置。
1.1 得到PE Header在文件中的位置。
通过DOS Header结构的成员e_lfanew,可以确定PE Header的在文件中的位置。
1.2 得到文件中节的数目。
确定PE Header的在文件中的位置之后,就可以确定PE Header中的成员FileHeader和成员OptionalHeader在文件中的位置。根据 FileHeader 中的 成员NumberOfSections 的值,就可以确定文件中节的数目,也就是节表数组中元素的个数。
1.3 得到节表在文件中的位置。
PE Header在文件中的位置加上PE Header结构的大小就可以得到节表在文件中的开始位置。PE Header结构的大小可以由Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定。其实到目前为止SizeOfOptionalHeade也就是结构Optional Header的大小也是固定的,所以整个PE Header结构的大小也是固定。不过为了安全起见,还是用Signature的大小加上FileHeader的大小再加上FileHeader中的SizeOfOptionalHeade来确定比较保险。
1.4 得到输出函数在文件中的位置。
第1.2步中我们确定了文件中节的数目,第1.3步中我们确定了节表在文件中的位置。
现在来确定输出函数在文件中的位置。
取得PE Header中的Optional Header中的DataDirectory数组中的第一项,
也就是输出函数项。DataDirectory[]数组的每项都是IMAGE_DATA_DIRECTORY结构,该结构定义如下。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
取得DataDirectory数组中的第一项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。
如果这个RVA的值为0表示这个PE文件中没有输出函数。
然后根据节的数目,遍历节表数组。也就是从0到(节表数-1)的每一个节表项。
每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),
到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。
我们遍历整个节表,看我们取得的输出函数的RVA,在哪个节表项的RVA范围之内。
如果在范围之内,就找到了输出函数所在节的节表项。
这个节表项中的 PointerToRawData 中的值,就是输出函数所在节在文件中的位置。这个节表项中的VirtualAddress 中的值,就是输出函数所在节在内存中的RVA。用输出函数的RVA减去输出函数所在节的RVA,就可以得到输出函数在该节内偏移。用这个偏移加上该节的在文件中的位置,就可以得到输出函数在文件中的位置。即DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress - SectionTable[i].VirtualAddress + SectionTable[i].PointerToRawData 。
这样我们就得到了输出函数在文件中开始的位置。
二 PE文件中的输出函数。
输出函数是用来给其他程序使用的。其他程序如果知道了某个输出函数的入口地址(就是实现这个函数功能的代码开始的地方),就可以转到那里去执行。一个PE文件中,如果有有输出函数,一般都不是一个。所以有一个数组来保存每个输出函数的入口地址。在PE文件中,提供两种方法,来找到某个输出函数的入口地址。第一种方法是通过入口地址数组序号,就是说知道是入口地址数组中的第几个元素,这样就可以得到里面的入口地址。第二种方法是通过函数名,通过比较函数名,然后得到对应该函数名的入口地址数组的序号,从而得到该函数名的对应函数的入口地址。为了能够通过函数名得到序号,就需要一些相关的结构。具体内容后面讲。总得来说PE文件的输出函数部分中就是这些东西。
前面我们已经得到了输出函数部分在文件中开始的位置,在输出函数部分的最开始,是一个IMAGE_EXPORT_DIRECTORY 结构,这个结构提供很多重要的信息。这个结构的后面紧跟着的是 输出函数入口地址数组 。输出函数入口地址数组之后紧跟着的是输出函数名的指针数组。输出函数名的指针数组之后紧跟着的是输出函数名对应的序号的数组。输出函数名对应的序号的数组之后紧跟着dll的名字和输出函数的名字。注意,他们之间是紧挨着的。并且顺序为IMAGE_EXPORT_DIRECTORY,输出函数入口地址的数组,输出函数名的指针的数组,输出函数名对应的序号的数组。最后是dll的名字的字符串和那些输出函数名的字符串。
先看IMAGE_EXPORT_DIRECTORY 结构,在WINNT.H中定义如下。
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
这个结构长度为40个字节,共有11个字段。
各字段含义如下:
Characteristics:一个保留字段,目前为止值为0。
TimeDateStamp:产生的时间。
MajorVersion:
MinorVersion:
Name:一个RVA,指向一个dll的名称的ascii字符串。
Base:输出函数的起始序号。一般为1。
NumberOfFunctions:输出函数入口地址的数组 中的元素个数。
NumberOfNames:输出函数名的指针的数组 中的元素个数,也是输出函数名对应的序号的数组 中的元素个数。
AddressOfFunctions:一个RVA,指向输出函数入口地址的数组。
AddressOfNames:一个RVA,指向输出函数名的指针的数组。
AddressOfNameOrdinals:一个RVA,指向输出函数名对应的序号的数组。
输出函数入口地址的数组,这个数组是一个DWORD数组,每个元素都是一个RVA,指向一个输出函数的入口地址,每个元素长4个字节。
输出函数名的指针的数组,这个数组是一个DWORD数组,每个元素都是一个RVA,指向一个输出函数名的ascii字符串,每个元素长4个字节。
输出函数名对应的序号的数组,这个数组是一个WORD数组,每个元素都是某个输出函数名函数对应的索引,这个索引是输出函数入口地址的数组的索引(已经用序号减去起始序号了),这个每个元素长2个字节。
dll名字符串和输出函数名字符串,都是ascii字符串,以空结束。一个紧挨着一个。dll名字符串的地址存在IMAGE_EXPORT_DIRECTORY 的 Name 中。输出函数名字符串 的地址存在 输出函数名的指针的数组 中。
还有要注意的是:
输出函数入口地址的数组包含着输出函数的入口点地址,一个序号减去起始序号(起始序号就是 IMAGE_EXPORT_DIRECTORY 中的 Base),用来索引这个数组。比如,起始序号为1,要找序号为1的函数的入口地址,那么该函数的入口地址为 输出函数入口地址数组[0](0是由1-1算出来的)序号为3的函数的入口地址为 输出函数入口地址数组[2](2是由3-1算出来的)。
当载入器要修正一个函数的调用,而这个函数是用序号输入的,载入器只要用序号减去起始序号,得到输出函数入口地址的数组的索引,就可以了。
当载入器要修正一个函数的调用,而这个函数是用函数名输入的,载入器比较输出函数名的指针的数组每个元素所指的函数名,比如在第3个元素中比较,发现相同。载入器就会从 输出函数名对应的序号的数组 的第三个元素的值 得到该函数的序号。用这个序号就可以在象前面那样用序号得到入口地址。输出函数名的指针的数组 和 输出函数名对应的序号的数组 有相同的元素个数(IMAGE_EXPORT_DIRECTORY 中的 NumberOfNames)。并且是有所关联的,函数名指针数组的第i个元素的序号,在序号数组的第i个元素中。输出函数名的指针的数组和输出函数名对应的序号的数组,分开成两个数组,而不是合并成一个结构体的数组(这个结构体第一个成员是指针,第二个成员是序号),是因为,那样的话数组的一个元素长6个字节,不利于对齐。
下面我们来通过一个例子,来看上面所介绍的内容。
我们的例子是Win2k中的dll文件routetab.dll。为了防止大家版本不同,本文附带了这个PE文件。
用开始讲到的寻找输出部分在文件中位置的方法,我们找到了输出部分在文件中的位置为00001460h。
由于第一个结构IMAGE_EXPORT_DIRECTORY比较长,一行方不下,所以放了三行,结构的不同成员用 / 分开。
其他每行是一个结构。可以用16进制编辑器打开附带的 routetab.dll 对照着看。
我们来算一下 Name,AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals 在文件中的位置。
输出部分的开始rva(由DataDirectory[1]得到)为1e60h。输出部分在文件中的位置为1460h。
Name为rva(值从结构中可以看到是00001eec,如果你不明白为什么是00001eec而不是ec1e0000的话,请看 《JIURL PE 格式学习总结(一)》中关于 big-endian和little-endian的介绍),则Name相对于输出部分开始处的偏移为1eec-1e60。而Name在文件中的位置为Name在相对于输出部分开始的偏移加上输出部分开始处在文件中的位置。所以Name在文件中的位置为1EEC-1E60+1460=14ECh。同样方法我们可以算出, AddressOfFunctions:
1e88-1e60+1460=1488。AddressOfNames:1eb0-1e60+1460=14b0 。AddressOfNameOrdinals:1ed8-1e60+1460=14d8。 从结构中还可以看到有0000000a(十进制10)个输出函数。
00001460: {00 00 00 00 / dc 5b ec 37 / 00 00 / 00 00 / ec 1e 00 00 /
00001470: 01 00 00 00 / 0a 00 00 00 / 0a 00 00 00 / 88 1e 00 00 /
00001480: b0 1e 00 00 / d8 1e 00 00 }
(我们用大括号括起来了,IMAGE_EXPORT_DIRECTORY结构,长度为40个字节)
00001488: 41 1a 00 00 (函数入口点的RVA,长4个字节)
0000148C: 64 1a 00 00
00001490: 02 18 00 00
00001494: 02 18 00 00
00001498: 71 16 00 00
0000149C: 07 16 00 00
000014A0: 26 18 00 00
000014A4: 84 1a 00 00
000014A8: 06 17 00 00
000014AC: 5b 19 00 00
000014B0: f9 1e 00 00 (函数名的指针,长4个字节,指向 1ef9-1e60+1460=14f9)
000014B4: 02 1f 00 00
000014B8: 0e 1f 00 00
000014BC: 21 1f 00 00
000014C0: 30 1f 00 00
000014C4: 42 1f 00 00
000014C8: 4d 1f 00 00
000014CC: 5b 1f 00 00
000014D0: 6c 1f 00 00
000014D4: 81 1f 00 00
000014D8: 00 00 (索引,说明的1个函数名的函数,入口地址在 地址数组[0])
(并不是每个PE文件序号数组的第0个元素值就是0,第1个元素值就是1,ntdll.dll中就不是)
000014DA: 01 00
000014DC: 02 00
000014DE: 03 00
000014E0: 04 00
000014E2: 05 00
000014E4: 06 00
000014E6: 07 00
000014E8: 08 00
000014EA: 09 00
000014EC: 52 4f 55 54 45 54 41 42 2e 64 6c 6c 00 ROUTETAB.dll.
000014F9: 41 64 64 52 6f 75 74 65 00 AddRoute.
00001502: 44 65 6c 65 74 65 52 6f 75 74 65 00 DeleteRoute.
0000150E: 46 72 65 65 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00 FreeIPAddressTable.
00001521: 46 72 65 65 52 6f 75 74 65 54 61 62 6c 65 00 FreeRouteTable.
00001530: 47 65 74 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00 GetIPAddressTable.
00001542: 47 65 74 49 66 45 6e 74 72 79 00 GetIfEntry.
0000154D: 47 65 74 52 6f 75 74 65 54 61 62 6c 65 00 GetRouteTable.
0000155B: 52 65 66 72 65 73 68 41 64 64 72 65 73 73 65 73 00 RefreshAddresses.
0000156C: 52 65 6c 6f 61 64 49 50 41 64 64 72 65 73 73 54 61 62 6c 65 00 ReloadIPAddressTable.
00001581: 53 65 74 41 64 64 72 43 68 61 6e 67 65 4e 6f 74 69 66 79 45 76 65 6e 74 00
SetAddrChangeNotifyEvent.
0000159A: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
000015AA: ...
000015F0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
还是比较清楚的,就不再讲了。
三 遍历PE文件中的输出
根据前面的方法得到输出部分的开始地址,最开始是一个IMAGE_EXPORT_DIRECTORY,根据这个结构中的内容,可以得到,和输出相关的三个数组的开始地址,和元素个数。用for循环可以很简单的遍历。
实现遍历输出的源程序,可以参考 PEDUMP - Matt Pietrek 1995 。《Windows95系统程式设计大奥秘》附书源码中有。
完
本文所使用的PE文件routetab.dll