PE文件结构与函数导出表——详解与实例
随着windows系统从Xp升级到Win7、Win8, 从32位升级到64位,PE文件结构在整体未变的情况下发生了一些小的变动,一方面是推荐的程序装载地址未采用,另一方面,导出函数序号不再是简单的升序,而是一定程度上的进行了乱序。本文首先对PE文件结构进行了详尽的解说,接着介绍了如何得出函数导出表,整个过程采用SysWoW64目录下的wininet.dll实例进行说明。在介绍过程中,明确指出了Win7、Win8等新系统相对Xp带来的区别。
文章链接:http://blog.csdn.net/typ2004/article/details/44227597
第一部分
1、DOS头
DOS头共64字节,最后一个双字代表PE头的文件地址。
2、PE头
WinNT.h 中 PE 头由三部分构成
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //PE ASCII IMAGE_FILE_HEADER FileHeader; //标准头 IMAGE_OPTIONAL_HEADER32 OptionalHeader; //可选头 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
第一部分PE标识
是DOWRD大小的PE标识。
第二部分标准头
是20个字节的PE标准头。
具体结构为
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; //PE中节的数量 DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; //PE可选头的长度 WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
第三部分可选头
第三部分是PE可选头
其中 0x12ch 的DWORD长的 ImageBase(0x63000000)为程序的建议装载地址,在XP系统中可能会使用此地址,但在Win7、Win8等系统中,此地址废弃。
其中 0x16ch 的DWORD长的NumberOfRvaAndSizes(0x00000010,通常为此值)为下面数据目录结构的项目数量。
注:对于64位系统来说,文件中NumberOfRvaAndSizes的位置比32位的系统靠后16个字节(前面SizeOfStackReserver等4个值在64位系统中为8字节,32位中为4字节)。
之后的8*IMAGE_NUMBEROF_DIRECTORY_ENTRIES(8*16)大小的字段为数据目录字段,定义了导出表、导入表、资源表、异常表等各类地址和大小(共16项地址和大小对)。其中,可以看到0x170h处的地址0x3D44即是导出表的虚拟地址。
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //地址 DWORD Size; //大小 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
PE可选头的具体结构为
typedef struct _IMAGE_OPTIONAL_HEADER { // // Standard fields. // WORD Magic; BYTE MajorLinkerVersion; BYTE MinorLinkerVersion; DWORD SizeOfCode; DWORD SizeOfInitializedData; DWORD SizeOfUninitializedData; DWORD AddressOfEntryPoint; DWORD BaseOfCode; DWORD BaseOfData; // // NT additional fields. // DWORD ImageBase; DWORD SectionAlignment; //内存中对齐粒度 DWORD FileAlignment; //文件中对齐粒度 WORD MajorOperatingSystemVersion; WORD MinorOperatingSystemVersion; WORD MajorImageVersion; WORD MinorImageVersion; 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]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
3、节表
PE头后紧跟着节目录表。
typedef 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; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
此表中包含的的信息如下:
节点名:.text
虚拟大小:00180454
虚拟偏移:00001000
实际大小:00180600
实际偏移:00000400
文件特征:60000020
节点名:.orpc
虚拟大小:0000009E
虚拟偏移:00182000
实际大小:00000200
实际偏移:00180A00
文件特征:60000020
节点名:.wpp_sf
虚拟大小:00009E3F
虚拟偏移:00183000
实际大小:0000A000
实际偏移:00180C00
文件特征:60000020
节点名:.data
虚拟大小:0000755C
虚拟偏移:0018D000
实际大小:00003E00
实际偏移:0018AC00
文件特征:C0000040
节点名:.idata
虚拟大小:00002444
虚拟偏移:00195000
实际大小:00002600
实际偏移:0018EA00
文件特征:40000040
节点名:.didat
虚拟大小:0000057C
虚拟偏移:00198000
实际大小:00000600
实际偏移:00191000
文件特征:C0000040
节点名:.rsrc
虚拟大小:0002BE40
虚拟偏移:00199000
实际大小:0002C000
实际偏移:00191600
文件特征:40000040
节点名:.reloc
虚拟大小:0000F8BC
虚拟偏移:001C5000
实际大小:0000FA00
实际偏移:001BD600
文件特征:42000040
4、各节
节表之后便是各节,需要注意的是,从节表到第一个节直接用0填充,其他节之间同样用0填充,各节只需要对齐到PE可选头的文件对齐粒度即可(本例中为0x0200)。
第二部分
函数导出表
1、导出目录
从上面已经知道0x3D44即是导出表的虚拟地址,其大小为0x255F。
依次查看各节的【虚拟偏移,虚拟大小),发现导出表位于.text 节中,.text节的 (虚拟偏移-实际偏移)=0x0C00,所以导出表的实际偏移为 0x3D44-0x0C00=0x3144。
导出目录的具体结构为
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;
从此结构可以看出,导出函数数目为0x0143,以函数名导出的函数个数为0x0123,导出函数地址表的地址为0x3d6c-0x0c00=0x316c,导出函数名称地址表的地址为0x4278-0x0c00=0x3678,导出序号表地址为0x4704-0x0c00=0x3b04。
2、导出函数地址表
由地址0x316c开始遍历,0x0FC6A0, 0x154D40, 0x154E30, 0x11C180, 0x11C600, 0x0FEF30, 0x081290等等,即为地址列表。
3、导出函数名称地址表
由地址0x3678开始遍历,得到0x4956, 0x496c, 0x4980等等虚拟偏移,对应的实际偏移为0x3d56, 0x3d6c, 0x3d80等等,查找这些位置的名称字符串如下。
4、导出序号地址表
导出序号 AddressOfNameOrdinals 指向的也是到处序号列表地址,其中的每个序号,与导出函数名称一一对应,代表了这个函数名称对应的函数在导出函数列表里序号。
本例中地址为0x3b04的导出序数表的第一项为0x0006,代表AppCacheCheckManifest 对应函数地址的下标为6(从0开始),即地址0x081290。
5、导出函数在内存中的地址
如果在Xp系统中,导出函数AppCacheCheckManifest 在内存的地址只需加上推荐的程序装在地址 ImageBase(1.2PE头的可选头里的字段)即可。
但是在Win7、Win8等系统中,导出函数在内存中的地址应加上 真正的程序装载地址。这个地址可以用 (DWORD) LoadLibrary(DllFilePath)求得。
至此完成~~