导入表的位置和大小可以从PE文件头中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取,对应的项目是DataDirectory字段的第2个IMAGE_DATA_DIRECTORY结构,从结构的VirtualAddress字段得到导入表的RVA值。
导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每个结构对应一个DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。
IMAGE_IMPORT_DESCRIPTOR结构的定义如下:
IMAGE_IMPORT_DESCRIPTOR = packed record
case integer of
0: Characteristics: DWORD;
1: OriginalFirstThunk:DWORD;
end;
TimeDateStamp: DWORD;
ForwarderChain: DWORD;
Name: DWORD;
FirstThunk: DWORD;
end;
结构中的Name字段是一个RVA,它指向此结构所对应的DLL文件的名称,这个文件名是一个以NULL结尾的字符串。
OriginalFirstThunk字段和FirstThunk字段,它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组的最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。
一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同的时刻有不同的含义,结构的定义如下:
IMAGE_THUNK_DATA = record
case integer of
0: ForwarderString: DWORD;
1: Function: DWORD;
2: Ordinal: DWORD;
3: AddressOfData: DWORD;
end;
end;
一个IMAGE_THUNK_DATA结构如何用来指定一个导入函数呢?当双字(就是指结构)的最高位为1时,表示函数是以序号的方式导入的,这时双字的低位就是函数的序号。当双字的最高位为0时,表示函数以字符串类型的函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构的定义如下:
IMAGE_IMPORT_BY_NAME = record
Hint: Word;
Name: Array[0..0] of Char;
end;
结构中的Hint字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为0,Name字段定义了导入函数的名称字符串,这是一个以0为结尾的字符串。
为什么需要两个一模一样的IMAGE_THUNK_DATA数组呢?答案是当PE文件被装入内存的时候,其中一个数组的值将被改作他用,Windows装载器会将指令Jmp dword ptr [xxxxxxxx]指定的xxxxxxxx处的RVA替换成真正的函数地址,其实xxxxxxxx地址正是由FirstThunk字段指向的那个数组中的一员。
实际上,当PE文件被装入内存后,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。