PE结构详解(二)

         那么继续上次未讲完的,我们先跳过导出表,先讲讲导入表。

        

首先,我们知道PE 文件中的数据被载入内存后根据不同页面属性被划分成很多区块(节),并有区块表(节表)的数据来描述这些区块。

这里我们需要注意的问题是:一个区块中的数据仅仅只是由于属性相同而放在一起,并不一定是同一种用途的内容。

例如输入表、输出表等就有可能和只读常量一起被放在同一个区块中,因为他们的属性都是可读不可写的。

其次,由于不同用途的数据有可能被放入同一个区块中,因此仅仅依靠区块表是无法确定和定位的。PE 文件头中 IMAGE_OPTIONAL_DEADER32 结构的数据目录表来指出他们的位置。

我们可以由数据目录表来定位的数据包括输入表、输出表、资源、重定位表和TLS等15 种数据。

数据目录表的第二个结构就是记录的导入表。

首先我们需要知道的是为什么需要导入表,当我们用windowsAPI编程的时候,调用的各种函数其实不在程序里面,比如messagebox函数很多程序都会用到,如果一定要写入程序里面才能使用时很浪费内存空间的,所以微软把这些函数都写进了DLL文件里,你要使用这些函数就直接在内存里向这个DLL要就行了。

但是程序怎么知道这个函数在内存的那个地方呢?

所以这就是导入表的目的!——对于磁盘上的PE 文件来说,它无法得知这些输入函数在内存中的地址,只有当PE 文件被装入内存后,Windows 加载器才将相关DLL 装入,并将调用输入函数的指令和函数实际所处的地址联系起来。

这就是“动态链接”的概念。动态链接是通过PE 文件中定义的“输入表”来完成的,输入表中保存的正是函数名和其驻留的DLL 名等。

废话不多说,直接上导入表的结构。

输入表是以一个 IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。

每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。

在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。

IMAGE_IMPORT_DESCRIPTOR STRUCT 
    union 
        Characteristics           DWORD   ? 
        OriginalFirstThunk        DWORD   ? 
    ends 
    TimeDateStamp                 DWORD   ? 
    ForwarderChain                DWORD   ? 
    Name                          DWORD   ? 
    FirstThunk                    DWORD   ?
IMAGE_IMPORT_DESCRIPTOR ENDS

成员介绍:

OriginalFirstThunk

它指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function name 的地址。

TimeDateStamp

该字段可以忽略。如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。

在最近,它被设置为0xFFFFFFFF以表示绑定发生。

ForwarderChain

一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。

它可被设置为0xFFFFFFFF以代表没有forwarder。

Name

它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称。

如:KERNEL32.DLL)。

FirstThunk

它包含由IMAGE_THUNK_DATA定义的 first thunk数组的虚地址,通过loader用函数虚地址初始化thunk。

在Orignal First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。

这个OriginalFirstThunk 和 FirstThunk明显是亲家,两家伙首先名字就差不多哈。那他们有什么不可告人的秘密呢?

正如我们看到的,他们分别指向2个不同的数组,但数组中每个元素都是IMAGE_THUNK_DATA这个结构,

然后每个IMAGE_THUNK_DATA又都指向了IMAGE_IMPORT_BY_NAME。

那让我们来看看IMAGE_THUNK_DATA这个结构长什么样:

IMAGE_THUNK_DATA STRUC
    union u1
    ForwarderString      DWORD  ?        ; 指向一个转向者字符串的RVA
    Function             DWORD  ?        ; 被输入的函数的内存地址
    Ordinal              DWORD  ?        ; 被输入的API 的序数值
    AddressOfData        DWORD  ?        ; 指向 IMAGE_IMPORT_BY_NAME
    ends
IMAGE_THUNK_DATA ENDS

我们发现他是个共用体。

每一个导入表结构都对应着一个动态链接库。

当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。

当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。

IMAGE_IMPORT_BY_NAME 结构

IMAGE_IMPORT_BY_NAME STRUCT
    Hint      WORD      ? 
    Name      BYTE      ?
IMAGE_IMPORT_BY_NAME ENDS

结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0。

Name 字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

输入地址表(IAT)

为什么由两个并行的指针数组同时指向 IMAGE_IMPORT_BY_NAME 结构呢?第一个数组(由 OriginalFirstThunk 所指向)是单独的一项,而且不能被改写,我们前边称为 INT。

第二个数组(由 FirstThunk 所指向)事实上是由 PE 装载器重写的。

好了,那么 PE 装载器的核心操作时如何的呢?这里就给大家揭秘啦~

PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,因此我们称为输入地址表(IAT)。

所以,当我们的 PE 文件装载内存后准备执行时,刚刚的图就会转化为下图:

原文地址:https://www.cnblogs.com/You0/p/4238829.html