最近做逆向破解的学习需要有关PE格式的相关知识,之前看过一点但还是模模糊糊的,刚好借这个机会来个彻底的学习,对PE的结构争取有个更深刻一点的认识
下面是我自己的学习笔记.
1. DOS文件头,这是PE文件最开始的一个结构体,定义了一个DOS小程序(注意在磁盘中是0x200对齐而内存中0x1000对齐,所以DOS文件头中不足的地方补0)
_IMAGE_DOS_HEADER
{
WORD e_magic; // MZ 0x5A4D
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; // 指向PE文件头的地址的指针,在这里是0x200h
}
2. IMAGE_NT_HEADERS文件头,这是一个大架构体,也就是e_lfanew = 0x200h指向的地方,即NT文件头,这里包含了PE文件的很多重要的信息。
_IMAGE_NT_HEADERS
{
DWORD Signature; //PE文件头的标志:PE\0\0 0x00004550
IMAGE_FILE_HEADER FileHeader; //PE文件头的结构体,这是一种结构体中的结构体,它包含了关于文件的一些基本信息。最重要的是,其中有一个域指明了跟在这个结构后面的可选文件头的大小
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}
下面开始以递归的方式探究PE的一层层结构
2.2 IMAGE_FILE_HEADER PE文件头
_IMAGE_FILE_HEADER
{
WORD Machine; //目标平台CPU的类型:0x014c // Intel 386 0x0200 // Intel 64
WORD NumberOfSections; //指示节表中节的数目。节表紧跟着IMAGE_NT_HEADERS结构, 也即是说节表是第 3 部分
DWORD TimeDateStamp; //指示文件创建时间。这个值是从格林尼治时间(GMT)1970年1月1日00:00以来的总秒数
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader; //就是上面说到的那个域,指示了OptionalHeader数据的大小对于32位PE文件来说,它通常是224;对于64位PE32+文件来说,它通常是240。但是,它们只是最小值,可能有更大的值
WORD Characteristics; //指示文件属性的一组位标志,它的取值来自与下面的宏定义
}
文件属性宏定义:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // 文件是 DLL, 这个域用来区分exe和dll.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
#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
2.3 IMAGE_OPTIONAL_HEADER32 PE可选文件头,虽说是可选,但是PE要求必须要有这个结构体域
_IMAGE_OPTIONAL_HEADER
{
//
// Standard fields.
//
WORD Magic; //一个特征字,用于表明文件头的类型 IMAGE_NT_OPTIONAL_HDR32_MAGIC:0x10b IMAGE_NT_OPTIONAL_HDR64_MAGIC:0x2b
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode; //带有IMAGE_SCN_CNT_CODE 属性的所有节的总大小。
DWORD SizeOfInitializedData; //所有由已初始化的数据组成的节的总大小。
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;//这个域很重要!文件中首先被执行的代码的第一个字节的RVA(注意是RVA)
DWORD BaseOfCode; //加载进内存之后代码的第一个字节的RVA(这个值一般和AddressOfEntryPoint是一样的)。
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase; //这个文件在内存中的首选加载地址,对于EXE来说,默认的ImageBase为0x400000;对于DLL来说,它是0x10000000
DWORD SectionAlignment; //加载进内存(注意是内存)之后节(注意是节)的对齐值。这个对齐值必须大于或等于文件对齐值
DWORD FileAlignment; //节在PE文件中的对齐值。对于x86可执行文件来说,它或者是0x200,或者是0x1000。不同版本的Microsoft链接器的默认值不一样。这个值必须是2的幂
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage; //SizeOfImage包含了假设存在于最后一个节之后的那个节的RVA。这等效于把此文件加载进内存时系统需要保留的内存数量。这个域的值必须是节的对齐值的倍数,如果这个域被恶意修改了,造成内存分配错误
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes; //在IMAGE_NT_HEADERS结构末尾处是一个IMAGE_DATA_DIRECTORY结构数组。这个域包含了这个数组的元素数目。由于以前发行的Windows NT的原因,它被设置为16。即导入表导出表那些东西
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //这可以说是PE中最重要的部分了,IMAGE_DATA_DIRECTORY结构数组。每个结构包含可执行文件中一些重要部分(例如导入表、导出表以及资源等)的RVA和大小。下面会重点介绍这个结构
}
2.3.1 IMAGE_OPTIONAL_HEADER结构末尾的DataDirectory数组就像是可执行文件中重要位置的地址簿。
IMAGE_DATA_DIRECTORY 数据表
_IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress; //数据的RVA
DWORD Size; //数据的大小
}
3. 紧跟着IMAGE_NT_HEADERS结构的是节表(section table)。这是PE结构的第3部分,既然是表,顾名思义,只是一个存放一些信息的结构体,下面还有节。节表是一个IMAGE_SECTION_HEADER结构数组。此结构提供了与它相关的节的信息,其中包括位置、长度和属性
_IMAGE_SECTION_HEADER
{
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //节的名称(ASCII码)。节名并不保证以NULL结尾。如果你指定的节名大于8个字节,链接器在生成可执行文件时将其截断为8个字符。在OBJ文件中存在一种机制可以让节名更长。节名通常以圆点开始,但这并不是必需的
union {
DWORD PhysicalAddress;
DWORD VirtualSize; //指示节实际占用的内存大小。这个域的值可能比SizeOfRawData域的值大或小
} Misc;
DWORD VirtualAddress; //在可执行文件中,它表示在内存中节的起始RVA。在OBJ文件中它被设置为0
DWORD SizeOfRawData; //可执行文件或OBJ文件中的节中存储的数据的大小(以字节计)。对于可执行文件来说,它必须是PE文件头中给出的文件对齐值的倍数
DWORD PointerToRawData; //节中数据起始的文件偏移。对于可执行文件来说,它必须是PE文件头中给出的文件对齐值的倍数
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; //指示节属性的标志(可以用“或”连接,这也是这种用bit位表示属性信息的常用思路,linux中的文件属性744之类的也是这种属性位图思想)下面列出了常用的节属性标志
}
Section characteristics:
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 //这个节不能被交换到页面文件中,因此它应该总是存在于物理内存中。经常用于内核模式驱动程序,可以使用#pragma page来声明。
IMAGE_SCN_MEM_SHARED //包含这个节的物理页面将在加载这个可执行文件的所有进程之间共享。因此每个进程看到的这个节中的数据的值完全一样。对于在进程的所有实例之间共享全局变量比较有用。要共享某个节,使用/SECTION:节名,S链接器选项
IMAGE_SCN_MEM_READ //节是可读的。几乎总是设置这个值。
IMAGE_SCN_MEM_WRITE //节是可写的。
IMAGE_SCN_LNK_INFO //节中包含链接器使用的信息。仅存在于OBJ文件中。
IMAGE_SCN_LNK_REMOVE //这个节中的内容将不成为最终的映像的一部分。仅用于OBJ文件。
IMAGE_SCN_LNK_COMDAT //节中的内容是公共数据(comdat)。公共数据(Communal data)是可以被定义在多个OBJ文件中的数据(或代码)。链接器只将其中的一份副本包含进最终的可执行文件中。Comdats对于支持C++的模板函数和函数级的链接至关重要。它仅存在于OBJ文件中。
IMAGE_SCN_ALIGN_xBYTES //这个节中的数据在最终的可执行文件中的对齐值。有各种各样的值(_4BYTES,_8BYTES,_16BYTES等)。如果不指定,默认为16字节。仅在OBJ文件中才设置这些标志。
4. PE的第4部分: 节。紧接着节表就是具体的节。它们是一个个单独的IMAGE_SECTION_DATA结构(这是从010editor上看到的,在winnt.h中貌似没有看到这个结构,我的理解是节没有一个固定的结构了,是具体的代码或着数据)
下面列出常见的节:
.text 默认的代码节。
.data 默认的可读/可写数据节。全局变量通常在这个节中。
.rdata 默认的只读数据节。字符串常量和C++/COM虚表就放在这个节中。
.idata 导入表。实际上,链接器经常把.idata节合并到其它节中(或者是明确指定的,或者是通过链接器的默认行为)。默认情况下,链接器仅在创建发行版的程序时才把.idata节合并到其它节中。
.edata 导出表。当创建要导出函数或数据的可执行文件时,链接器会创建一个.EXP文件。这个.EXP文件包含一个.edata节,这个节被添加到最后的可执行文件中。与.idata节一样,.edata节也经常被合并到.text节或.rdata节中。
.rsrc 资源节。这个节是只读的。它不应该被命名为其它名称,也不应该被合并到其它节中。
.bss 未初始化的数据节。在最新的链接器创建的可执行文件中很少见到。链接器扩展可执行文件的.data节的VirtualSize域以便容纳未初始化的数据。
.crt 添加到可执行文件中的数据,用来支持C++运行时库(CRT)。一个比较好的例子就是用于调用静态C++对象的构造函数和析构函数的指针。要获取更详细的信息,可以参考2001年1月的Under The Hood专栏。
.tls 这个节中的数据用来支持使用__declspec(thread)语法创建的线程局部存储变量。它包括数据的初始值,以及运行时需要的附加变量。
.reloc 可执行文件中的基址重定位节。通常DLL需要基址重定位信息而EXE并不需要。在创建发行版的程序时,链接器并不为EXE文件生成基址重定位信息。可以使用/FIXED链接器选项移除基址重定位信息。
以上就是PE结构的大致全貌,下面开始我们要重点介绍一下_IMAGE_NT_HEADERS文件头中的IMAGE_FILE_HEADER结构体中的IMAGE_DATA_DIRECTORY结构,因为我们以后在研究逆向破解,进程注入之类的时候会频繁的涉及关于导入表和导出表的相关知识,而这些表都存在这个DataDirectory数组中。
关于导出,导入表:
这里有一点要注意的是,在文件头中的那个IMAGE_DATA_DIRECTORY[16]数组中存的并不是实际的导入导出表,而只是存了RVA和SIZE,而实际存放数据的地方是节区:.idata和.edata这两个节,所以我们查找导出导出函数的具体地址要通过导入导出表的指针跳转到具体的节区才能实际寻址。
IMAGE_DATA_DIRECTORY[0]:导出表
IMAGE_EXPORT_DIRECTORY就是导出表中的指针指向的导出节对应的结构体,导出目录(Export Directory)指向三个数组和一个ASCII码字符串表
当一个EXE或DLL导出函数或变量时,其它EXE或DLL就可以使用这些导出的函数或变量。为了简单起见,我把导出的函数和导出的变量统称为“符号”。当导出一些符号时,最起码导出符号的地址需要能够以一种已定义好的方式被获取。每个导出的符号都有一个与之关联的序数,它可以用来查找这个符号。同时,几乎总有一个ASCII码格式的字符串名称与这个导出的符号关联。一般来说,导出的符号名与源文件中的符号名是一样的,尽管它们可以被修改的不一样。
_IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics; //导出标志。当前未定义任何值。
DWORD TimeDateStamp; //导出数据的创建时间。这个域的定义与IMAGE_NT_HEADERS.FileHeader.TimeDateStamp相同
WORD MajorVersion;
WORD MinorVersion;
DWORD Name; //与导出符号相关的DLL的名称ASCII字符串的RVA(例如KERNEL32.DLL),这个字段很重要,shellcode中通过PEB搜索dll文件获取cmd的时候就回用到这个字段
DWORD Base; //这个域包含了这个可执行文件的导出符号所使用的序数值的起始值。通常情况下这个值为1,但并不总是这样。当通过序数查找导出符号时,将序数值减去这个域的值就得到了这个导出符号在导出地址表(Export Address Table ,EAT)中的索引。
DWORD NumberOfFunctions; //EAT中的元素数。注意EAT中的某些元素可能为0,这表明没有 代码/数据使用那个序数值导出。
DWORD NumberOfNames; //导出名称表(Export Names Table,ENT)中的元素数。这个域的值总是小于或等于NumberOfFunctions域的值。当某些符号仅使用序数导出时,它就小于那个域的值。如果导出序数之间有间隔,它同样也小于那个域的值。这个域的值也是导出序数表的大小
DWORD AddressOfFunctions; //EAT的RVA。EAT中的每个元素都是一个RVA。其中每个非0的RVA都对应一个导出符号。
DWORD AddressOfNames; //ENT的RVA。ENT中的每个元素都是一个ASCII码字符串的RVA。其中的每个ASCII码字符串都对应一个由名称导出的符号。这些字符串是按一定顺序排列的。这就使得加载器在查找导出符号时可以进行二进制搜索
DWORD AddressOfNameOrdinals; //导出序号表的RVA。这个表是一个WORD类型的数组。它将ENT中的索引映射到导出地址表中相应的元素上。
}
导出目录(Export Directory)指向三个数组(EAT,ENT,EOT)和一个ASCII码字符串表。其中只有导出地址表是必需的,它是一个由指向导出函数的指针组成的数组
假设你调用GetProcAddress来获取KERNEL32中的AddAtomA这个API的地址。这时系统开始查找KERNEL32的IMAGE_EXPORT_DIRECTORY结构。它从那里获取了导出名称表的起始地址,知道了在这个数组中有0x3A0个元素,它通过二进制搜索来查找字符串“AddAtomA”。
假设加载器发现AddAtomA是这个数组中的第二个元素。然后它从导出序数表(Export Ordinal Table)中读取相应的第二个值。这个值就是AddAtomA的导出序数。将这个导出序数作为EAT的索引(加上Base域的值),它最终获取AddAtomA的相对虚拟地址(RVA)是0x82C2。将此值与KERNEL32的加载地址相加就得到了AddAtomA的实际地址。
这就是函数寻址的过程。可见,我们通过名称对ENT进行查找,得到在EOT中的index,又通过EOT获得导出函数的真实序号,在到EAT中得到RVA,最后加上IMAGE_OPTIONAL_HEADER.ImageBase得到函数在内存中的VA,完成寻址。
导出转发
导出表一个特别聪明的地方是它能将一个导出函数转发(Forwarding)到其它DLL。例如在Windows NT®、Windows® 2000和Windows XP中,KERNEL32中的HeapAlloc函数被转发到了NTDLL导出的RtlAllocHeap函数上。转发是在链接时通过.DEF文件中的EXPORTS节中的一种特殊语法形式来实现的。对于HeapAlloc这个例子,KERNEL32的.DEF文件一定包含下面的内容: EXPORTS ••• HeapAlloc = NTDLL.RtlAllocHeap
怎样才能区别转发的函数与正常导出的函数呢?这需要一些技巧。通常EAT中包含的是导出符号的RVA。但是如果这个RVA位于导出表中(通过相应的DataDirectory中的VirtualAddress域和Size域进行判断),那么它就是转发的。
当转发一个符号时,它的RVA很明显不能是当前模块中的代码或数据的地址。实际上,它的RVA指向一个由DLL和转发到的符号名称组成的字符串。在前面的例子中,这个字符串就是NTDLL.RtlAllocHeap。
IMAGE_DATA_DIRECTORY[1]:导入表
与导出函数或变量相反的就是导入它们。为了与前面保持一致,我仍然使用“符号”这个术语来指代导入的函数和变量。
与导出表有一点不太一样。
导入数据被保存在IMAGE_IMPORT_DESCRIPTOR结构中。对应着导入表的数据目录项就指向由这个结构组成的数组。每个IMAGE_IMPORT_DESCRIPTOR结构都与一个导入的可执行文件对应
也就是说PE头中的那个IMAGE_DATA_DIRECTORY[16]数组只是一个指针,真正的导入表在.idata这个节中。
在.idata节中对应的结构体是一连串的IMAGE_IMPORT_DESCRIPTOR,每一个IMAGE_IMPORT_DESCRIPTOR一般都对应着一个dll文件爱你,编程中一般是强制类型转换获取到。
_IMAGE_IMPORT_DESCRIPTOR
{
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)。它包含导入名称表的RVA。导入名称表是一个IMAGE_THUNK_DATA结构数组。这个域被设置为0表示IMAGE_IMPORT_DESCRIPTOR结构数组的结尾,这里我的理解是一个程序可以导出不止一个的dll文件,而一个dll文件又有不止一个的导出函数,所以会有IMAGE_IMPORT_DESCRIPTOR结构体数组,且每个IMAGE_IMPORT_DESCRIPTOR中的OriginalFirstThunk又指向一个IAT导入名称表
};
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
DWORD Name; // 导入的DLL名称字符串(ASCII码格式)的RVA
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses).导入地址表的RVA。IAT是一个IMAGE_THUNK_DATA结构数组
}
每个IMAGE_IMPORT_DESCRIPTOR结构指向两个数组,这两个数组实际上是一样的。它们有好几种叫法,但最常用的名称是导入地址表(Import Address Table,IAT)和导入名称表(Import Name Talbe,INT)
OriginalFirstThunk->INT
FirstThunk->IAT
IAT个INT一样,都是一个IMAGE_THUNK_DATA的结构体数组,这两个数组最后都以一个值为0的IMAGE_THUNK_DATA结构作为结尾
IMAGE_THUNK_DATA这个结构是一个与指针大小相同的共用体(或者称为联合)。每个IMAGE_THUNK_DATA结构对应着从可执行文件中导入的一个函数。
IMAGE_THUNK_DATA可以有如下几种含义:
DWORD ForwarderString;// 转发函数字符串的RVA(见上文)
DWORD Function; // 导入函数的内存地址
DWORD Ordinal; // 导入函数的序数
DWORD AddressOfData; // IMAGE_IMPORT_BY_NAME和导入函数名称的RVA
IAT中的IMAGE_THUNK_DATA结构的用途可以分为两种。在可执行文件中,它们或者是导入函数的序数,或者是一个IMAGE_IMPORT_BY_NAME结构的RVA。IMAGE_IMPORT_BY_NAME结构只是一个WORD类型的值,它后面跟着导入函数的名称字符串。这个WORD类型的值是一个“提示(hint)”,它提示加载器导入函数的序号可能是什么。
当加载器加载可执行文件时,它用导入函数的实际地址来覆盖IAT中的每个元素。这一点是理解下文的关键(这句话很重要)
_IMAGE_IMPORT_BY_NAME
{
WORD Hint;
BYTE Name[1];
}
另一个数组INT,本质上与IAT是一样的。它也是一个IMAGE_THUNK_DATA结构数组。关键的区别在于当加载器将可执行文件加载进内存时,它并不覆盖INT。为什么对于从DLL中导入的每组API都需要有两个并列的数组呢?答案在于一个称为绑定(binding)的概念。当在绑定过程(后面我会讲到)中覆盖可执行文件的IAT时,需要以某种方式保存原来的信息。而作为这个信息的副本的INT,正是这个用途。
绑定
当可执行文件被绑定时(例如通过Bind程序),其IAT中的IMAGE_THUNK_DATA结构中是导入函数的实际地址。也就是说,磁盘上的可执行文件的IAT中存储的就是其导入的DLL中的函数在内存中的实际地址。当加载一个被绑定的可执行文件时,Windows加载器可以跳过查找每个导入函数并覆盖IAT这一步。因为IAT中已经是正确的地址了。但是这只有正确对齐时才行
你也许会怀疑将可执行文件绑定是否保险。你可能会想,如果绑定了可执行文件,但它导入的DLL发生了变化,这时怎么办呢?当这种情况发生时,IAT中的地址已经失效了。加载器会检查这种情况并随机应变。如果IAT中的地址已经失效,加载器会根据INT中的信息重新解析导入函数的地址。
确定绑定信息有效性的一个关键数据结构是IMAGE_BOUND_IMPORT_DESCRIPTOR。被绑定的可执行文件中有一个此结构的列表。每个IMAGE_BOUND_IMPORT_DESCRIPTOR结构表示一个绑定到的
DLL的日期/时间戳。这个列表的RVA由数据目录中索引为IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT的元素给出。
_IMAGE_BOUND_IMPORT_DESCRIPTOR
{
DWORD TimeDateStamp; //这是包含导入的DLL的日期/时间戳的一个DWORD类型的值
WORD OffsetModuleName; //这是包含导入的DLL的名称字符串偏移地址的一个WORD类型 的值。这个域是相对于首个IMAGE_BOUND_IMPORT_DESCRIPTOR结构的偏移(而不是RVA)。
WORD NumberOfModuleForwarderRefs;
}
一般情况下,每个导入的DLL对应的IMAGE_BOUND_IMPORT_DESCRIPTOR结构简单地组成一个数组。但是当绑定的API转发到了另一个DLL上时,这个转发到的DLL的有效性也需要检查。在这种情况下,IMAGE_BOUND_FORWARDER_REF结构就与IMAGE_BOUND_IMPORT_DESCRIPTOR结构交叉在了一起
基址重定位
基址重定位(Base Relocations)信息告诉加载器可执行文件不能被加载到其首选地址时需要进行修改的每一个位置。对于加载器来说,幸运的是它并不需要知道地址使用的细节问题。它只知道有一个地址列表,其中的每一个地址都需要以同样的方式进行修改
简而言之,基址重定位信息只是可执行文件中的一个地址列表,当加载进内存时,这些地址中的值都要再加上△。为了提高系统性能,可执行文件的页面只有在需要时才会被加载进内存(可执行文件的加载与内存映射文件类似),基址重定位信息的格式就反映了这个特性
基址重定位信息所在的节通常被称为.reloc节,但是查找它的正确方法是通过数据目录中索引为IMAGE_DIRECTORY_ENTRY_BASERELOC的那个元素。
基址重定位信息是一些非常简单的IMAGE_BASE_RELOCATION结构
_IMAGE_BASE_RELOCATION
{
DWORD VirtualAddress; //需要进行重定位的内存范围的起始RVA
DWORD SizeOfBlock; //重定位信息的大小,其中包括IMAGE_BASE_RELOCATION自身的大小
}
至此,对PE文件格式的大致剖析结束了,以后要用到更详细的内容的时候以后再说。