pe知识学习----------------连载5,6,7--完
2007-10-23 13:07
pe知识学习(五)
从这贴开始,我介绍几个常用的区段:输入表,输出表和重定位表. 我们知道,程序调用外部的dll函数通常都是下面这种形式: call my_label ... my_label: jmp dword ptr [xxxxxxxx] 对一个dll中的函数的调用总是通过一个地址间接的调用的.这些地址就放在输入表里. 输入表(Import Table),简而言之,就是描述该pe文件从哪几个动态连接库导入了什么函数的一组结构数组.在这里我希望能用最简洁的语言让你明白什么是输入表.输入表的组成并不复杂,只用到三个结构.它们是:IMAGE_IMPORT_DESCRIPTOR,IMAGE_THUNK_DATA,IMAGE_IMPORT_BY_NAME. 我们先看一下框图. IMAGE_IMPORT_DESCRIPTOR |--------------------| |-------------------------| OriginalFirstThunk | | |--------------------| | | TimeDateStamp | | |--------------------| | | ForwarderChain | | |--------------------| | | Name |----> "USER32.DLL" | |--------------------| | | FirstThunk |---------------------------| | |--------------------| | | | | Hint-name table IMAGE_IMPORT_BY_NAME import address table(IAT) | | |------------------| |--------------------| |------------------| | |-> | IMAGE_THUNK_DATA |-->| 44 | "GetMessage" |<--| IMAGE_THUNK_DATA |<---| |------------------| |----|---------------| |------------------| | IMAGE_THUNK_DATA |-->| 72 | "LoadIcon" |<--| IMAGE_THUNK_DATA | |------------------| |----|---------------| |------------------| | ...... |-->| .. | ...... |<--| ...... | |------------------| |----|---------------| |------------------| | NULL | | NULL | |------------------| |------------------| 当然,这是描述从一个dll中引入函数的情形.从几个dll中引入函数,那么就有几个这样的结构.同时,这也是磁盘文件上的结构.装入内存后FirstThunk指向的结构数组会被修改.可以看下面的图. 我们先来熟悉一下这三个结构的定义: typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) }; 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; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef struct _IMAGE_THUNK_DATA32 { union { PBYTE ForwarderString; PDWORD Function; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; } u1; } IMAGE_THUNK_DATA32; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; //指出函数在所在的dll的输出表中的序号 BYTE Name[1]; //指出要输入的函数的函数名 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; 下面我们讲解一下IMAGE_IMPORT_DESCRIPTOR结构的各个域的含义: 1)union { DWORD Characteristics; DWORD OriginalFirstThunk; }; 这个联合指向一个 IMAGE_THUNK_DATA 类型的结构数组.这个联合不是很重要,可以为0. 2)TimeDateStamp 该dll的时间日期戳,一般为0. 3)ForwarderChain 正向连接索引.一般为0. 4)Name dll名字的RVA. 5)FirstThunk 这个域也是一个RVA,指向一个DWORD数组,数组以NULL结束.数组中的每个DWORD实际上是一个IMAGE_THUNK_DATA结构的联合体。IMAGE_THUNK_DATA联合体通常被解释为一个指向IMAGE_IMPORT_BY_NAME结构的RVA. 从上图我们可以看出有两个并行的指针数组都指向IMAGE_IMPORT_BY_NAME结构.事实上,OriginalFirstThunk指向的IMAGE_THUNK_DATA结构数组从来不被修改,该数组有时也叫提示名表(Hint-name table),提示名表总是指向IMAGE_IMPORT_BY_NAME结构数组.而FirstThunk指向的IMAGE_THUNK_DATA结构数组在该pe文件被加载时,加载程序会修改该数组的内容.加载程序迭代搜索数组的每一个指针,找到每一个IMAGE_IMPORT_BY_NAME结构所对应的输入函数的地址,然后加载程序用找到的地址修改相应的IMAGE_THUNK_DATA结构. 如前面提到的 call my_label ... my_label: jmp dword ptr [xxxxxxxx] 其中的xxxxxxxx就是FirstThunk指向的IMAGE_THUNK_DATA数组中的一个的值.因为FirstThunk所指向的数组在加载后是所有输入函数的地址,因此它被称为输入地址表(Import Address Table,IAT). pe文件加载后输入表的情形如下: IMAGE_IMPORT_DESCRIPTOR |--------------------| |-------------------------| OriginalFirstThunk | | |--------------------| | | TimeDateStamp | | |--------------------| | | ForwarderChain | | |--------------------| | | Name |----> "USER32.DLL" | |--------------------| | | FirstThunk |---------------------------| | |--------------------| | | | | Hint-name table IMAGE_IMPORT_BY_NAME import address table(IAT) | | |------------------| |--------------------| |------------------| | |-> | IMAGE_THUNK_DATA |-->| 44 | "GetMessage" | |ptr of GetMessage |<---| |------------------| |----|---------------| |------------------| | IMAGE_THUNK_DATA |-->| 72 | "LoadIcon" | | ptr of LoadIcon | |------------------| |----|---------------| |------------------| | ...... |-->| .. | ...... | | ...... | |------------------| |----|---------------| |------------------| | NULL | | NULL | |------------------| |------------------| 输入表在pe知识里是最重要的一部分.希望你能够结合一下pe工具实际理解这部分的内容. pe知识学习(六) 有输入表就有输出表,本贴开始介绍输出表. 大部分dll都会输出一些函数.有些pe文件也会有输出表.通常输出表都是放在.edata区段的.因此.edata区段的注要成分是函数名表,入口点地址,输出函数的序号. 输出表的开始部分是一个IMAGE_EXPORT_DIRECTORY结构,之后紧接着是由该结构中的某个域所指向的数据. IMAGE_EXPORT_DIRECTORY结构定义如下: 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; 1)Characteristics 这个值总为0. 2)TimeDateStamp 含有这个导出表的文件被生成的时间. 3)MajorVersion 4)MinorVersion 版本信息.总为0. 5)Name 含有这个导出表的pe文件的名字的RVA. 6)Base 输出函数序号的开始值. 7)NumberOfFunctions 数组 AddressOfFunctions 中元素的个数.这个值就是导出表中导出函数的个数. 8)NumberOfNames 以名字输出的函数的个数. 9)AddressOfFunctions 这是一个RVA,指向一个由函数地址组成的数组.每一个函数地址是本模块中的一个输出函数的入口地址. 10)AddressOfNames 这是一个RVA,指向一个由字符串指针组成的数组,每个字符串是本模块中以名字输出的输出函数的函数名. 11)AddressOfNameOrdinals 这是一个RVA,指向一个word类型的数组,该word类型数组是本模块中所有以名字输出的输出函数的输出序号. 假设一个dll有三个导出函数,分别如下: 序号 函数名 1 "myfun1" 2 3 "myfun2" 其中序号为2的函数只能通过序号导出.图示如下: IMAGE_EXPORT_DIRECTORY 函数地址表 |---------------------------| |------->|------------------| | Characteristics | | | 0x400042"myfun1" | |---------------------------| | |------------------| | ...... | | | 0x400085 | |---------------------------| | |------------------| | NumberOfFunctions = 3 | | | 0x400197"myfun2" | |---------------------------| | |------------------| | NumberOfNames = 2 | | |---------------------------| | 函数名表 | AddressOfFunctions |--| |----->|------------| |---------------------------| | | 0xXXXXXXXX |->"myfun1" | AddressOfNames |----| |------------| |---------------------------| | 0xXXXXXXXX |->"myfun2" | AddressOfNameOrdinals |----| |------------| |---------------------------| | | 函数名称地址索引表 |----->|-----------| | 1 | |-----------| | 3 | |-----------| 我们来看一下pe加载程序的工作机制.假设它知道函数名"myfun2",那么加载程序将首先遍历函数名表,找到匹配的函数名"myfun2".由于"myfun2"在函数名表里的索引是2,所以加载函数将在函数名称地址索引表的第二个元素里取得函数在函数地址表里的索引3,然后加载程序就会在函数地址表的第三个元素里取得函数的入口地址0x400197. 这就是以名称导出函数的过程. 如果是以序号导出函数地址的,那将更简单.加载程序将直接用序号在函数地址表里取出函数的入口地址.可以看出,以序号导出函数比以名称导出函数快,但以序号导出函数地址会带来维护的问题.有些api函数在不同的系统上导出序号并不相同.所以微软不推荐使用序号来导出函数. 这里讲的是Base为1时的情形,如果Base域大于1,则在取得函数在函数地址表中的索引后,用这个索引值减去Base就可以得到函数在函数地址表中的偏移值. pe知识学习(七)--完 这一贴介绍一下pe文件中的重定位表. 重定位的概念不难理解.简单的说,就是因为程序被连接后一些变量或者函数调用或跳转指令使用了绝对地址,当装载程序不能把pe映像装到预定的地址(ImageBase)时,那么这些绝对地址就需要调整.否则程序将访问到错误的地址. exe文件一般不需要重定位,因为每个exe文件映像都有自己独立的地址空间,它总能被映射到预定的地址.而dll文件一般是映射到exe文件的地址空间的.当多个dll文件的预定地址发生冲突时,就不能保证会被映射到预定的地址了.所以dll文件一般都需要重定位的. 那么重定位是怎样实现的呢? 在pe文件里用这样一个结构来描述一个重定位数据项: typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; 1)VirtualAddress 这个域包含这个重定位数据项的起始RVA值,紧跟在结构后面的偏移值要加上这个值才是一个真正的需要重定位的数据的RVA值. 如果这个域为0,则代表一系列重定位数据项的结束. 2)SizeOfBlock 重定位数据项的大小. 3)TypeOffset[1] 这是一个WORD类型的数组.数组的元素个数由(SizeOfBlock - 8 ) / 2 得到.每个元素的低12位代表一个偏移值,该偏移值加上VirtualAddress就是需要修正的数据的RVA值.而高4位代表该偏移值的类型.该类型定义如下: #define IMAGE_REL_BASED_ABSOLUTE 0 #define IMAGE_REL_BASED_HIGH 1 #define IMAGE_REL_BASED_LOW 2 #define IMAGE_REL_BASED_HIGHLOW 3 #define IMAGE_REL_BASED_HIGHADJ 4 #define IMAGE_REL_BASED_MIPS_JMPADDR 5 #define IMAGE_REL_BASED_SECTION 6 #define IMAGE_REL_BASED_REL32 7 #define IMAGE_REL_BASED_MIPS_JMPADDR16 9 #define IMAGE_REL_BASED_IA64_IMM64 9 #define IMAGE_REL_BASED_DIR64 10 #define IMAGE_REL_BASED_HIGH3ADJ 11 其中和intel的cpu有关的只有两种类型.其他的都用于i386以外的cpu. 0 (IMAGE_REL_BASED_ABSOLUTE):代表该偏移值无意义.只是为了使所有重定位数据项的大小位DWORD的整数倍. 3 (IMAGE_REL_BASED_HIGHLOW): 把该偏移值加上 VirtualAddress就是要修正的数据的RVA值. 由于WORD中只有低12位表示偏移值,因此一个重定位项只能修正一页的数据(4k).如果需要重定位的数据超过4k,那么一个pe文件里就有多个重定位项. 加载程序修正过程如下:假设IMAGE_OPTIONAL_HEADER.ImageBase的值为0x400000,而实际pe映像被加载的地址为0x500000,实际加载的地址比预定的高0x100000,那么需要修正的数据都会被加上0x100000. 这里所说的需要修正的数据是指前面提到的变量的绝对地址和调用或跳转指令里含有的绝对地址. 到这里pe知识学习就告一段落了.希望这些帖子能帮助你了解pe文件的大概知识.更细节的知识可以到msdn里查找.我的水平有限,错误之处还望你能够给予指正.我将不胜感激. |