目录 Table of Contents
8 输入表 (导入表) 详解 2
输入表结构
回顾一下, 在 PE 文件头的 IMAGE_OPTIONAL_HEADER 结构中的 DataDirectory(数据目录表) 的第二个成员就是指向输入表的. 而输入表是以一个 IMAGE_IMPORT_DESCRIPTOR (简称 IID) 的数组开始. 每个被 PE 文件链接进来的 dll 文件都分别对应一个 IID 数组结构. 在这个 IID 数组中, 并没有指出有多少个项目 (就是没有明确指明有多少个链接文件), 但它最后是以一个全为 NULL(就是 00) 的 IID 结构作为结束标志.
IMAGE_IMPORT_DESCRIPTOR 结构定义如下 :
IMAGE_IMPORT_DESCRIPTOR struct
union
Characteristics DWORD ?
ends
TimeDateStamp DWORD ?
ForwarderChain DWORD ?
Name DWORD ?
FirstThunk DWORD ?
IMAGE_IMPORT_DESCRIPTOR ends
成员介绍
首先是一个联合类型, 也就是共用体, 就是说两个不能同时存在, 两个人用同一个内存地址.
- OringinalFirstThunk
它指向 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. 在 Original First Thunk 缺席的情况下, 它指向 first thunk : hints 和 the function names 的 thunks.
上面的成员介绍如果看不懂的, 可以暂时忽略.
这个 OriginalFirstThunk 和 FirstThunk 明显就有问题, 它们名字就很相似, 那么它们有什么关系呢 ? 见下图.
IAT 指向函数序号, INT 指向函数名
我们看到 : OriginalFirstThunk 和 FirstThunk 都是两个类型为 IMAGE_THUNK_DATA 的数组, 它是一个指针大小 (4 byte, DWORD) 的联合类型 (union). 每一个 IMAGE_THUNK)DATA 结构定义一个导入函数信息 (即指向结构为 IMAGE_IMPORT_BY_NAME 的家伙, 这家伙等下再说), 然后数组最后以一个内容为 0 的 IMAGE_THUNK_DATA 结构作为结束标志.
我们得到 IMAGE_THUNK_DATA 结构的定义如下 :
IMAGE_THUNK_DATA struct
union u1
ForwarderString DWORD ? ;指向一个转向者字符串的 RVA
Function DWORD ? ;被输入的函数的内存地址
Ordinal DWORD ? ;被输入的 API 的序数值
AddressOfData DWORD ? ;指向
ends
IMAGE_THUNK_DATA ends
由于是 union 结构, 所以 IMAGE_THUNK_DATA 实际上是一个双字大小. 该结构在不同时候赋予不同意义.
其实 union 这个数据结构很容易理解, 说白了就是当时穷, 能省就省, 就是几个人轮流用一个东西去搞事情.
那么我们怎么来区分不同时候是什么不同的意思呢 ?
规定如下 :
当 IMAGE_THUNK_DATA 值的最高位为 1 时, 表示函数以序号方式输入, 这时候低 31 位被看作一个函数序号.
当 IMAGE_THUNK_DATA 值的最高位为 0 时, 表示函数以字符串类型的函数名方式输入, 这时候双字的值是一个 RVA, 指向一个 IMAGE_IMPORT_BY_NAME 结构.
OK , 我们现在讨论下指向的这个 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 数组中的第一个入口, 把这个函数地址放到 FirstThunk 指向的地方, 也就是我们说的输入地址表 (IAT). 所以, 当我们的 PE 文件装载内存后准备执行的时候, 刚刚的图就会转化为下图 :
此时, 输入表中的其它部分就不重要了, 程序依靠 IAT 提供的函数地址就可以正常运行.