Window PE -- 输出表 基址重定位

Window PE – 输出表 基址重定位

输出表

创建一个DLL时,实际上创建了一组能让XE或其他DLL调用的函数,此时PE装载器根据DLL文件中输出的信息修正被执行文件的IAT。当一个LL函数能被EXE或另一个DLL文件使用时,它就被“输出了”(Exported)。其中,输出信息被保存在输出表中,DLL文件通过输出表向系统提供输出函数名、序号和人口地址等信息。

EXE文件中一般不存在输出表,而大部分DLL文件中存在输出表。当然,这也不是绝对的,有些EE文件中也存在输出函数。

输出表结构

输出表(ExportTable)主要内容是一个表格,其中包括函数名称、输出序数等。序数是指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。在此不提倡仅通过序数引出函数,这会带来DLL维护上的问题。一旦DLL升级或被修改,调用该DLL的程序将无法工作。

输出表是数据目录表的第I个成员,指向IMAGE_EXPORT_DIRECTORY(简称“IED”)结构。IED结构定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
IMAGE_EXPORT_DIRECTORY STRUCT【导出表,共40字节】
{
+00 h DWORD Characteristics ; 未使用,总是定义为0
+04 h DWORD TimeDateStamp ; 文件生成时间
+08 h WORD MajorVersion ; 未使用,总是定义为0
+0A h WORD MinorVersion ; 未使用,总是定义为0
+0C h DWORD Name ; 模块的真实名称
+10 h DWORD Base ; 基数,加上序数就是函数地址数组的索引值
+14 h DWORD NumberOfFunctions ; 导出函数的总数
+18 h DWORD NumberOfNames ; 以名称方式导出的函数的总数
+1C h DWORD AddressOfFunctions ; 指向输出函数地址的RVA
+20 h DWORD AddressOfNames ; 指向输出函数名字的RVA
+24 h DWORD AddressOfNameOrdinals ; 指向输出函数序号的RVA
};IMAGE_EXPORT_DIRECTORY ENDS
  • Characteristics:输出属性的旗标。前还没有定义,总是为0。
  • TimeDateStamp:输出表创建的时间(GMT时间)。
  • MajorVersion:输出表的主版本号未使用,设置为0。
  • MinorVersion:输出表的次版本号。未使用,设置为。
  • Name:指向一个ASCII字符串的RVA。这个字符串是与这些输出函数相关联的DLL的名字(例如KERNEL32.DLL)。
  • Base:这个字段包含用于这个PE文件输出表起始序数值(基数)。在正常情况下这个值是1,但并非必须如此。当通过序数来查询一个输出函数时,这个值从序数里被减去,其结果将为进入输出地址表(EAT)的索引。
  • NumberOfFunctions:EAT中的条目数量。注意,一些条目可能是0,这个序数值表明没有代码或数据被输出。
  • NumberOfNames:输出函数名称表(ENT)里的条目数量。NumberOlNames的值总是小于或等于NumberOfFunctions的值,小于的情况发生在符号只通过序数输出的时候。另外,当被赋值的序数里有数字间距时也会是小于的情况,这个值也是输出序数表的长度。
  • AddressOfFunctions:EAT的RVA。EAT是一个RVA数组,数组中的每一个非零的RVA都对应于一个被输出的符号。
  • AddressOlNames:ENTRVA。ENT一个指向ASCII字符串的RVA组。每一个ASCII字符串对应于一个通过名字输出的符号。因为这个表是要排序的,所以ASCII字符串也是按顺序排列的。这允许加载器在查询一个被输出的符号时使用进制查找方式,名称的排序是二进制的(就像C++RTL中strcmp函数提供的一样),而不是一个环境特定的字母顺序。
  • AddressOfNameOrdinals:输出序数表的RVA。这个表是字的数组。个表将ENT中的数组索引映射到相应的输出地址表条目。

设计输出表是为了方便PE装载器工作。首先,模块必须保存所有输出函数的地址,供PE装载器查询。块将这些息保存在AddressOfFunctions域所指向的数组中,而数组元素数目存放在NumberOfFunctions域中。如果模块引出了40个函数,那么在AddressOfFunctions指向的数组中必定有40个元素,NumberOfFunctions的值为40。如果有些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些名字的RVA值存放在一个数组中,供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames中包含名字数目。PE装载知道函数名,并想、以此获取这些函数的地址。目前已有两个模块,分别是名字数组和地址数组,但两者之还没有联系的纽带,需要一些联系函数名及其地址为它们建立联系。PE文档指出,可以使用指向地址数组的索引作为连接,因此PE装载器在名字数组中找到匹配名字的同时,也获了指向地址表中对应元素的索引。这些索引保存在由AddressOfNameOrdinals域所指向的另一个数组(最后一个)中。由于该数组起联系名字和地址的作用,其元素数目一定与名字数组相同。例如,每个名字有且仅有1个相关地址,反过来则不一定(一个地址可有好几个名字来对应)因此,需要给同一个地址取“别名”。为了发挥连接作用,名字数组和索引数组必须并行成对使用,例如索引数组的第1个元素必定含有第1个名字的索引,依此推。

1554867753098

基址重定位

当链接器生成一个PE文件时,会假设这个文件在执行时被装载到默认的基地址处,并把code和data的相关地址都入PE文件。如果载入时将默认的值作为基地址载入,则不需要重定位。但是,如果PE文件被装载到虚拟内存的另一个地址中,链接器登记的那个地址就是错误的,这时就需要用重定位表来调整。在PE文件中,重定位表往往单独作为一块,用“.reloc”表示。

概念

和NE格式的重定位方式不同,PE格式的做法十分简单。PE格式不参考外部DLL或模块中的其区块,而是把文件中所有可能需要修改的地址放在一个数组里。如果PE文件不在首选的地址载入,那么文件中的每一个定位都需要被修正。对加载器来说,它不需要知道关于地址使用的任何细节,只要知道有一系列的数据需要以某种一致的方式来修正就可以了。下面以实例DllDemo.DLL为例讲述其重定位过程。如下代码中两个加粗的地址指针就是需要重定位的数据。

1554867886934

分析一下0040100Eh处,其作用是将一个指针压人枝,00402000h是某一字符串的指针。这句指令有5字节长,第l个字节(68h)是指令的操作码,后4个字节用来保存一个DWORD大小的地址(00402000h)。在这个例子中,指令来自-个基址为00400000h的DLL文件,因此这个字符串的RVA值是2000h。如果PE文件确实在00400000h处载人,指令就能够按照现在的样子正确执行。但是,当DLL执行时,Windows加载器决定将其映射到00870000h处(映射基址由系统决定),加载器就会比较基址和实际的载入地址,计算出一个差值。在这个例子中,差值是470000h,这个差值能被加载到DWORD大小的地址里以形成新地址。在前面的例子中,地址0040100Fh是指令中双字的定位,对它将有一个基址重定位,实际上字符串的新地址就是00872000h。为了让Windows有能力进行这样的调整,可执行文件中有多个“基址重定位数据”。本例中的Windows加载器应把470000h加给00402000h,并将结果00872000h写回原处。这个过程如下图所示。

1554867929435

DllDemo.DLL在内存中进行重定位处理后的代码如下。

1554867947723

对EXE文件来说,每个文件总是使用独立的虚拟地址空间,所以EXE总是能够按照这个地址载人,这意味着EXE文件不再需要重定位信息。对DLL来说,因为多个DLL文件使用宿主EXE文件的地址空间,不能保证载入地址没有被其他DLL使用,所以DLL文件中必须包含重定位信息,除非用一个/FIXED开关来忽略它们。在VisualStudio.NET中,链接器会为Debug和Release模式的EXE文件省略基址重定位,因此,在不同系统中跟踪同一个DLL文件时,其虚拟地址是不同的,也就是说,在读者的机器里运行DllDemo.DLL,Windows加载器映射的基址可能不是00870000h,而是其他地址。

重定位表的结构

基址重定位表(BaseRelocationTable)位于一个.reloc区块内,找到它们的正确方式是通过数据目录表的IMAGE_DIRECTORY_ENTRY_BASERELOC条目查找。基址重定位数据采用类似按页分割的方法组织,是由许多重定位块串接成的,每个块中存放4KB(lOOOh)的重定位信息,每个重定位数据块的大小必须以DWORD(4字节)对齐。它们以一个IMAGE_BASE_RELOCATION结构开始,格式如下。

1
2
3
4
5
6
7
IMAGE_BASE_RELOCATION STRUC 【基址重定位位于数据目录表的第六项,共8+N字节】
{
+00 h DWORD VirtualAddress ;重定位数据开始的RVA 地址
+04 h DWORD SizeOfBlock ;重定位块得长度,标识重定向字段个数
+08 h WORD TypeOffset ;重定项位数组相对虚拟RVA,个数动态分配
};
IMAGE_BASE_RELOCATION ENDS
  • Virtua!Address:这组重定位数据的开始RVA地址。各重定位项的地址加这个值才是该重定位项的完整RVA地址。
  • SizeOfBlock:当前重定位结构的大小。因为Virtua!Address和SizeOfBlock的大小都是固定的4字节,所以这个值减8就是TypeOffset数组的大小。
  • TypeOffset:一个数组。数组每项大小为2字节,共16位。这16位分为高4位和低12位。高4位代表重定位类型;低12位是重定位地址,它与VirtualAddress相加就是指向PE映像中需要修改的地址数据的指针。

常见的重定位类型如下表所示。虽然有多种重定位类型,但对x86可执行文件来说,所有的基址重定位类型都是IMAGE_REL_BASED_HIGHLOW。在一组重定位结束的地方会出现一个类型是IMAGE_REL_BASED_ABSOLUTE的重定位,这些重定位什么都不做,只用于填充,以便下一个IMAGE_BASE_RELOCATION按4字节分界线对齐。所有重定位块以一个V川ua!Address段为0的IMAGE_BASE_RELOCATION结构结束。

1554868151561

重定位表的结构如下图所示,它由数个IMAGE_BASE_RELOCATION结构组成,每个结构由VirtualAddress、SizeOfBlock和TypeOffset3部分组成。

1554868174075

对于IA-64可执行文件,重定位类型似乎总是IMAGE_REL_BASED_DIR64。就像x86重定位,也用IMAGE_REL_BASED_ABSOLUTE重定位类型进行填充有趣的是,尽管IA-64的EE页大小是8KB,但基址重定位仍是4KB的块。

1571159890689

Author: YuanBi
Link: https://www.basicbit.cn/2018/11/01/2018-11-01-Windows PE 输出表/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.