PE文件详解七


前面好几篇在讲输入表,今天要讲的是输出表和地址的是地址重定位。有了前面的基础,其实对于怎么找输出表地址重定位的表已经非常熟悉了。

  0x01 输出表结构

  当创建一个DLL文件时,实际上创建了一组能让EXE或者其他DLL调用的一组函数,PE装载器根据DLL文件中输出信息修正正在执行文件的IAT。当一个DLL函数能被EXE或者DLL文件使用时,它被称为输出了。其中输出信息被保存在输出表中,DLL文件通过输出表向系统提供输出函数名,序号和入口信息。

  对于EXE文件一般不存在输出表,而大部分DLL文件都有输出表。输出表是由一个叫做IMAGE_EXPORT_DIRECTORY(简称IED)组成。IED存放着输出函数名,输出序数等信息,他的结构如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {

    DWORD   Characteristics;    // 未使用,总为0 

    DWORD   TimeDateStamp;      // 文件创建时间戳

    WORD    MajorVersion;       // 未使用,总为0

    WORD    MinorVersion;       // 未使用,总为0

    DWORD   Name;               // 指向一个代表此 DLL名字的 ASCII字符串的 RVA

    DWORD   Base;               // 函数的起始序号

    DWORD   NumberOfFunctions;  // 导出函数的总数

    DWORD   NumberOfNames;      // 以名称方式导出的函数的总数

    DWORD   AddressOfFunctions;     // 指向输出函数地址的RVA

    DWORD   AddressOfNames;         // 指向输出函数名字的RVA

    DWORD   AddressOfNameOrdinals;  // 指向输出函数序号的RVA

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

下面介绍几个重要字段:

Name:指向的是的ASCII字符串的RVA

NumberOfFunctions:输出表EAT表中的数据条目数。

NumberOfName:输出表ENT表中的数据条目数,ENT也是一个RVA地址,不过每项指向的是函数名字的ascii码。

AddressOfFunctions:是指向EAT的RVA值EAT是RVA一个数组,每一项指向了被输出的函数的地址。

AddressOfNames:是指向ENT的RVA值,ENT也是一个数组,每项指向被输出函数的名字。

下图是他们之间的关系图:

  0x02实例分析输出表结构

  1)用hexworkshop打开DLLDemo.DLL文件,找到数据目录表的第一项,它在文件头偏移78h处。如下图:

值为RVA=4000h,转化为FileOffset=800h。此处即为输出表结构所在地址。

2)跳往800h,依次读出几个重要字段的值如下图:

由上图可知各个字段的RVA值,Name=0000 4032,NumberOfFunctions=0000 0010 NumberOfNames:0000 0010

AddressOfFunctions=0000 4028 ,AddressOfNames=0000 402c 。两个Address字段全都转化为FileOffset值。

Name=832h,AddressOfFunctions=828h,AddressOfNames=82ch。跳往832h如下图:

 可知输出的DLL名字叫做DLLDemo.DLL,跳往828h即可找到输出函数的EAT表,跳往82ch即可找到输出函数的ENT表。

  0x03 输出实现过程总结

  PE装载器调用GetProcAddress来查找DLLDemo.DLL里的api函数,系统通过定位DLLDemo.DLL的IMAGE_EXPORT_DIRECTORY(出书目录表)结构开始工作,从这个结构中他获得输出函数名称表(ENTb表)的起始地址,进而知道这个数组只有一个元素,他对名字进行二进制查找直到发现字符串“MegBox”。PE装载器发现MsgBox是数组的第一个元素,加载器然后从输出序数表读取相应的第一个值,这个值是MsgBox的输出序数。使用输出序数作为进入EAT的索引,他得到MsgBox的值是1008h,1008h加上DllDemo.DLL的装入地址得到MsgBox的实际地址。

0x04 基址重定位概念

当连接器生成一个PE文件时,他假设这个文件执行时会被装入默认的基址处,并且把code和data的相关地址写入PE文件中。如果装入是按照默认的值作为基址装入,则不需要基址重定位,但是如果可执行文件被装在到内存中的另一个地址,链接器所登记的地址就是错的这时就需要用重定位表来调整。在PE文件中,它往往单独分为一块,用“.reloc”来表示。PE文件重定位过程。

  0x05 详细解读基址重定位

  基址重定位表放在一个位于.relo的区域中,但是找到它需要通过数据目录表的第五项成员Base reloction Table,这项所指向重定位的基本结构IMAGE_BASE_RELOCATION。

IMAGE_BASE_RELOCATION的基本定义如下:

 struct IMAGE_BASE_RELOCATION {

    DWORD   VirtualAddress;//重定位数据开始RVA

DWORD   SizeOfBlock;//重定位块的长度

WORD    TypeOffset;//重定位项位数组

 

}

 IMAGE_BASE_RELOCATION ENDS

下面分别解释一下这几个字段:

VirtualAddress:是这一组重定位数据的开始的RVA地址。其实就是原来的地址加上这个值就完成了重定位。

SizeOfBlock:当前重定位结构的大小,因为VirtualAddress和SizeOfBlock都是固定的四个字节,所以SizeOfBlock的值减去8就得到了重定位块的大小。

TypeOffset:这个字段很有意思,一个两个字节16位,高四位代表重定义类型,低12位装入的是我们需要重定位的地址,即这个地址加上前面的字段VirtualAddress的值完成重定位。注意Typeoffset是一个数组他的值有SizeOfBlock-8决定。

下图位重定位示意图:

 0x06 实例讲解地址重定位

 1)hexWrokShop打开PE文件DllDemo.DLL。通过数据目录表的第五项找到重定位结构IMAGE_BASE_RELOCATION,即在PE文件头偏移地址为A0h处,跳往a00h处,下图标黑地方即为结构IMAGE_BASE_RELOCATION数据。

 我们注意到第一个字段VritualAddress=0000 1000h,SizeOfBlock=0000 0010h,由此可得数组TypeOffset共八个个字节。每个元素两个字节则共四组数据。整理得出下表:

项目

重定位数据1

重定位数据2

重定位数据3

重定位数据4

原始数据

0F 30

23 30

00 00

00 00

TypeOffset值

30 0F

30 23

00 00

00 00

TypeOffset高四位

TypeOffset低12位

00F

023

低12位加上VritualAddress

100F(RVA值)

1023(RVA值)

转换成FileOffset

20F

223

重定位后的地址如下图: