Window PE -- 输入表

Window PE – 输入表

输入表

可执行文件使用来自其他DLL的代码或数据的动作称为输入。当PE文件被载入时,Windows加载器的工作之一就是定位所有被输入的函数和数据,并让正在入的文件可以使用那些地址。这个过程是通过PE文件的输入表(Impo1tTable,简称“IT”,也称导人表)完成的。输入表中保存的是函数名和其驻留的DLL名等动态链接所需的信息。输入表在软件外亮技术中的地位非常重要,读者在研究与外壳相关的技术时一定要彻底掌握这部分知识。

输入表的调用

在代码分析或编程中经常会遇到输入函数(ImportFunctions,或称导人函数)。输入函数就是被程序调用但其执行代码不在程序中的函数,这些函数的代码位于相关的DLL文件中,在调用者程序中只保留相关的函数信息,例如函数名、DLL文件名等。对磁盘上的PE文件来说,它无法得知这些输入函数在内存中的地址。只有当PE文件载入内存后,Windows加载器才将相关DLL载人,并将调用输入函数的指令和函数实际所处的地址联系起来。

当应用程序调用一个DLL的代码和数据时,它正在被隐式地链接到DLL,这个过程完全由Windows加载器完成。另一种链接是运行期的显式链接,这意味着必须确定目标DLL已经被加载,然后寻找API的地址,这几乎总是通过调用LoadLibra巧和GetProcAddress完成的。

当隐含地链接一个API时,类似LoadLibrary和GetProcAddress的代码始终在执行,只不过这是由Windows加载器自动完成的。Windows加载器还保证了PE文件所需的任何附加的DLL都巳载入例如,Windows2000/XP上每个由VisualC++创建的正常程序都要链接KERNEL32.DLL,而它又从NTDLL.DLL中输入函数。同样,如果链接了GDI32.DLL,它又依赖USER32、ADVAPI32、NTDLL和KERNEL32等DLL的函数,那么都要由Windows加载器来保证载入并解决输入问题。

在PE文件内有一组数据结构,它们分别对应于被输入的DLL。每一个这样的结构都给出了被输入的DLL的名井指向一组函数指针。这组函数指针称为输入地址表(ImportAddressTable,IAT)。每一个被引人的API在IAT里都有保留的位置,在那里它将被Windows加载器写人输入函数的地址。最后一点特别重要:一旦模块被载入,IAT中将包含所要调用输入函数的地址。

把所有输入函数放在IAT中的同一个地方是很有意义的。这样,无论在代码中调用一个输入函数多少次,都会通过IAT中的同一个函数指针来完成。

现在看看怎样调用一个输入函数。需要考虑两种情况,即高效和低效。最好的情况是像下面这样,直接调用00402010h处的函数,00402010h位于IAT中。

1
CALL  DWORD  PTR  [00402010]

而实际上,对一个被输入的API的低效调用像下面这样(实例PE.exe中调用LoadlconA函数的代码)。

1
2
3
4
	Call  00401164
...
:00401164
Jmp dword ptr [00402010]

这种情况下,CALL令把控制权转交给一个子程序,子程序中的JMP指令跳转到IAT中的00402010h。简单地说就是:使用5字节的额外代码;由于使用了额外的JMP指令,将花费更多的执行时间。

有人可能会问:为什么要采用此种低效的方法?对这个问题有一很好的解释:编译器元法区分输入函数调用和普通函数调用。对每个函数调用,编译器使用同样形式的CALL指令,示例如下。

1
CALL  XXXXXXXX

“xxxxxxxx”是一个由链接器填充的实际地址。注意,这条指令不是从函数指针来的,而是从代码中的实际地址来的。为了实现因果平衡,链接器必须产生一块代码来取代“xxxxxxxx”,简单的方法就是像上面一样调用一个JMPstub。

JMP指令来自为输入函数准备的输入库。如果读者检查过输入库,在输入函数名字的关联处就会发现与上面的JMPstub相似的指令,即在默认情况下,对被输入API的调用将使用低效的形式。

如何得到优化的形式?答案来自一个给编译器的提示形式。可以使用修饰函数的_declspec(dllimport)来告诉编译器,这个函数来自另一个DLL,这样编译器就会产生指令

1
CALL  DWORD  PTR  [XXXXXXXX]

而不是指令

1
CALL  XXXXXXXX

此外,编译器将给函数加上“_imp_"前缀,然后将函数送给链接器,这样就可以直接把一imp_xxx送到IAT中,而不需要调用JMPstub了。

如果要编写一个输出函数,井为它们提供一个头文件,不要忘了在函数的前面加上修饰符“_declspec(dllimport)”,在winnt.h等系统头文件中就是这样做的,示例如下。

1
_declspec(dllimport) void Foo(void)

输入表结构

PE文件头的可选映像头中,数据目录表的第2个成员指向输入表。入表以一个IMAGE_IMPORT_DESCRI凹、OR(IID)数组开始。每个被PE文件隐式链接的DLL都有一个IID。在这个数组中,没有字段指出该结构数组的项数,但它的最后一个单元是“NULL”,由此可以计算出该数组的项数。如,某个PE文件从两个DLL文件中引人函数,因此存在两个IID结构来描述这些DLL文件,并在两个IID结构的最后由一个内容全为0的IID结构作为结束。IID的结构如下。

1
2
3
4
5
6
7
8
9
10
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; //INT(Import Name Table) address (RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; //library name string address (RVA)
DWORD FirstThunk; //IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;
  • OriginalFirstThunk(Characte1istics):包含指向输入名称表(INT)的RVA。INT是一个IMAGETHUNK_DATA结构的数组,数组中的每个MAGE_THUNK_DATA结构都指向IMAGEIMPORT_BY_NAME构,数组以一个内容为0的IMAGE_THUNK_DATA结构结束。
  • TimeDateStamp:一个32位的时间标志,可以忽略。
  • ForwarderChain:这是第l个被转向的A凹的索引,一般为0,在程序引用一个DLL中的API,而这个API又在引用其他DLL的API时使用(但这样的情况很少出现)。
  • Name:DLL名字的指针。它是一个以“00”结尾的ASCII字符的RVA地址该字符串包含输入的DLL名,例如“KERNEL32.DLL”USER32.DLL”。
  • FirstThunk:包含指向输入地址表(IAT)的RVA。IAT是一个IMAGE_THUNK_DATA结构的数、

OriginalFirst 和 FirstThunk结构类似。他们分别指向两个本质上相同的数组IMAGE_THUNK_DATA,这些数组有好几种叫法,最常见的是输入名称表(ImportNameTable,INT)和输入地址表(lmpot Address Table,IAT)。如下图所示为一个可执行文件正在从USER32DLL里输入一些API。

1554866976205

两个数组中都有IMAGE_THUNK_DATA结构类型的元素,它是一个指针大小的联合(union)。每个IMAGE_THUNK_DATA元素对应于一个从可执行文件输入的函数。两个数组的结束都是由一个值为0的IMAGE_THUNK_DATA元素表示的。IMAGE_THUNK_DATA结构实际上是一个双字,该结构在不同时刻有不同的含义,具体如下。

1
2
3
4
5
6
7
8
9
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE 指向一个转向者字符串的RVA
DWORD Function; // PDWORD 被输入的函数的内存地址
DWORD Ordinal; // 被输入的 API 的序数值
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME 指向 IMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时低31位(或者一个64位可执行文件的低63位)被看成一个函数序号。当双字的最高位为0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个RVA,指向一个IMAGE_lMPORT_BY_NAME结构。

IMAGE_IMPORT_BY_NAME结构仅有1个字大小,存储了一个输入函数的相关信息,结构如下。

1
2
3
4
  IMAGE_IMPORT_BY_NAME STRUCT
  Hint WORD ?
  Name BYTE ?
  IMAGE_IMPORT_BY_NAME ENDS
  • Hint:本函数在其所驻留DLL的输出表中的序号。该域被PE装载器用来在DLL的输出表里快速查询函数。该值不是必需的,一些链接器将它设为0。
  • Name:含有输入函数的函数名。函数名是一个ASCII字符串,以“NULL”结尾。注意,这里虽然将Name的大小以字节为单位进行定义,但其实它是一个可变尺寸域,由于没有更好的表示方法,只好在上面的定义中写成“BYTE”。

输入地址表

为什么会有两个并行的指针数组指向IMAGE_IMPORT_BY_NAME结构呢?第1个数组(由OriginalFirstThunk所指向)是单独的一项,不可改写,称为INT,有时也称为提示名表(Hint-nameTable)。第2个数组(由FirstThunk所指向)是由PE装载器重写的。PE装载器先搜索OriginalFirstThunk,如果找到,加载程序就迭代搜索数组中的每个指针,找出每个IMAGE_IMPORT_BY_NAME结构所指向的输入函数的地址。然后,加载器用函数真正的人口地址来替代由FirstThunk指向的IMAGE_THUNK_DATA数组里元素的值。“Jmpdwordptr[xxxxxxxx]”语句中的“[口xxxxxx]”是指FirstThunk数组中的一个人口,因此称为输入地址表(Import Address Table,IAT)。所以,当PE文件装载内存后准备执行时,图11.13己转换成如图11.14所示的状态,所有函数人口地址排列在一起。此时,输入表中的其他部分就不重要了,程序依靠IAT提供的函数地址就可以正常运行。

1554867171146

在某些情况下,一些函数仅由序号引出。也就是说,不能用函数名来调用它们,只能用它们的位置来调用它们。此时,IMAGE_THUNK_DATA值的低位字指示函数序数,最高有效位(MSB)设为1。微软提供了一个方便的常量IMAGE_ORDINAL_FLAG32来测试DWORD值的M钮,其值为80000000h(在PE32+中是IMAGE_ORDINAL_FLAG64,其值为8000000000000000h)。

另一种情况是程序OrignalFirstThunk的值为0。在初始化时,系统根据FirstThunk的值找到指向函数名的地址串,根据地址串找到函数名,再根据函数名得到人口地址,然后用入口地址取代FirstThunk指向的地址串中的原值。

1571159894317

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.