导入表(Import Table)是Windows可执行文件中的一部分,它记录了程序所需调用的外部函数(或API)的名称,以及这些函数在哪些动态链接库(DLL)中可以找到。在PE文件运行过程需要依赖哪些模块,以及依赖这些模块中的哪些函数,这些信息就记录在导入表中。在Win32编程中我们会经常用到导入函数,导入函数就是程序调用其执行代码又不在程序中的函数,这些函数通常是系统提供给我们的API,在调用者程序中只保留一些函数信息,包括函数名机器所在DLL路径。当程序需要调用某个函数时,它必须知道该函数的名称和所在的DLL文件名,并将DLL文件加载到进程的内存中,导入表就是告诉程序这些信息的重要数据结构。
导入表
(Import Table) 中有两个重要的部分:INT
和 IAT
。这两个部分在 PE 文件的导入表中扮演不同的角色,但它们紧密配合以实现程序的动态链接。
INT(Import Name Table)
INT 是导入表的一个组成部分,它包含了程序需要调用的 DLL 函数的名称或序号。INT 的作用是为程序提供一个简单的方式来表示导入的函数。exe程序为了表明自身需要哪些dll的函数,也会生成一张表,那这张表就是导入表。
具体含义:
-
INT
存储的是 DLL 中每个被导入函数的名称。 -
在 32 位 PE 文件中,INT 中的每个条目是一个
RVA(相对虚拟地址)
,指向一个字符串,该字符串是函数的名称,或者在某些情况下,指向一个函数的序号(这种情况通常出现在使用了 Ordinal 导入的情况)。 -
INT 表示的函数是
未绑定
的,也就是说,程序并不直接知道函数的实际内存地址。它只知道函数的名称或序号,但这个名称/序号会在程序加载时被解析。
IAT(Import Address Table)
为什么需要IAT❓
一般程序在调用自身函数的时候,自身函数地址RAV是固定的;但是当程序在调用dll里的函数的时候,由于dll的地址会发生重定位,导致dll里的函数地址每次都会发生变化。
【自定义函数与 DLL 函数的区别】:
1、程序中的自定义函数
:在程序内部(比如静态库或当前程序中的函数),调用这些函数时,函数地址是固定的。编译器在编译时会确定函数的地址,因为函数的地址在程序加载时就已经确定了。
2、DLL 中的函数
:不同于程序中的函数,动态链接库(DLL
) 中的函数地址在程序加载时无法确定,因为 DLL 的加载地址是不固定的。
操作系统可能将不同的 DLL 加载到内存的不同位置,这就导致了 DLL 中的函数地址会发生变化。
【为什么DLL函数地址会发生变化】:
由于操作系统在加载 DLL 时,会根据可用内存和其他因素来决定 DLL 的加载地址。不同的程序或不同的运行环境可能会将 DLL 加载到不同的内存地址。假设你有两个程序都依赖于 kernel32.dll,但操作系统可能会将 kernel32.dll 加载到不同的内存位置。
在 Windows 操作系统中,DLL 文件是一种共享库,它包含了多个函数和数据,供不同的程序调用。当多个程序需要调用同一个函数或资源时,它们可以共享一个 DLL 文件,从而减少内存的使用和磁盘空间的浪费。
这种变化称为 地址重定位
(Relocation),也就是每次程序启动时,操作系统决定 DLL 中每个函数的实际内存地址。
IAT(Import Address Table) 的作用
为了确保程序能够准确调用 DLL 中的函数,程序需要一种机制来查找 DLL 函数的实际地址。IAT
(Import Address Table) 就是用来存储这些函数地址的表格。
-
IAT 的构建
:当程序编译时,程序并不知道 DLL 中函数的实际内存地址。编译时,它只会在导入表(Import Table)中填入一些占位符,如函数名称或序号。 -
IAT 的更新
:当程序加载时,操作系统的加载器会查找并加载需要的 DLL,解析 DLL 中的函数地址,并将这些地址填充到 IAT 中。这样,当程序运行时,它就能够通过 IAT 中的地址准确调用 DLL 中的函数,而不需要担心 DLL 函数的实际内存地址。
一、如何使用 IAT 来调用 DLL 函数
程序加载时
:
程序的导入表(Import Table)告诉操作系统它需要调用哪些外部 DLL 函数。
操作系统加载这些 DLL,并将 DLL 中的函数地址映射到内存中的某个位置。
更新 IAT
:
操作系统查找 DLL 中每个需要的函数的地址,并将这些地址填充到 IAT(Import Address Table) 中。
IAT 中每个条目都对应一个函数的地址,程序可以通过这些条目找到实际的函数地址。
程序运行时调用 DLL 函数
:
程序在执行时,并不直接知道 DLL 函数的地址,它通过访问 IAT 中的指针
来获得函数的实际地址。
这个指针就像一个 指向函数地址的指针
,程序可以使用这个指针来准确地调用 DLL 中的函数。
例如,如果程序要调用 CreateFileA 函数,它不会直接去查找 CreateFileA 在 kernel32.dll 中的内存地址,而是会查找 IAT 中的 CreateFileA 函数的地址。-IAT 中存储的是 DLL 中 CreateFileA 函数的实际地址,程序可以通过访问这个地址来调用它。
类似这样的调用函数。这里的0x88223344就是IAT的地址,
CALL DWORD PTR DS:[0x88223344]
附此图便于理解:
二、定位导入表
在 PE 文件头中,找到 Optional Header,然后查看其中的 Data Directory,数组中的第二个元素保存的就是导入表的 RVA 以及大小。回顾之前的文章《PE文件结构:节表》。
DataDirectory是一个长度为 16 的数组,它包含指向导入表、导出表、资源表等数据的相对虚拟地址(RVA)和该数据的大小,结构如下:
typedefstruct_IMAGE_DATA_DIRECTORY {DWORD VirtualAddress;DWORD Size;
}IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
VirtualAddress
:指向数据的相对虚拟地址(RVA),即该数据在内存中的位置。通过该地址,加载器可以找到该数据。
Size
:该数据的大小(以字节为单位)。如果该字段为 0,表示数据不存在或没有相关内容。
数据在数组中的位置入下:可以看到导入表的位置和大小信息保存在数据目录项的第2项(下标为1),数据目录项相关宏定义如下,可以自行查看。
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory导出表
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory导入表
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory资源表
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory异常
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory安全表
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table基址重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory调试
// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory TLS表
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers 存储程序与 DLL 文件绑定的符号信息。
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table 存储函数的实际地址。
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors 延迟导入描述符
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
2.1 定位导入表的流程
1、在PE头中找到DataDirectory
2、获取DataDirectory的第二项(下标为1):DataDirectory[1]中导入表的RVA
3、将导出表的RVA转换为FOA,在文件中定位到导入表
2.2 定位实例
这里我们还是用之前PE系列文章中使用的样例程序进行导入表的定位演示,此处使用010 Editor打开样例文件。
在NT头部中定位到DataDirectory:
DataDirectory中第二个元素就记录着导入表的RVA和大小。
接着我们可以通过RVA去计算出转化为FOA,这边直接使用CFF Explorer.exe进行计算。使用CFF Explorer打开样例程序文件,选中Address Converter,接着在RVA处输入我们刚刚获取到的导入表的RVA,此时我们就能够获得对应的FOA
此处我们获得的FOA为000653E0,接着在010 Editor中进行(Ctrl + G)定位即可。
在定位到导入表后我们就可以对导入表的结构进行解析。
三、导入表的结构
查看导入表的结构只需要我们打开Visual Studio任意项目的任意C/C++文件,接着在文件中输入:
_IMAGE_IMPORT_DESCRIPTOR
随后按住ctrl,点击结构体即可进行结构查看。
导入表的结构如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {union {DWORD Characteristics; // 0 for terminating null import descriptorDWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)} DUMMYUNIONNAME;DWORD TimeDateStamp; // 0 if not bound,// -1 if bound, and real date\time stamp// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)// O.W. date/time stamp of DLL bound to (Old BIND)DWORD ForwarderChain; // -1 if no forwardersDWORD Name;DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
1、DUMMYUNIONNAME(DWORD)
union {DWORD Characteristics; // 0 for terminating null import descriptorDWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)} DUMMYUNIONNAME;
DUMMYUNIONNAME是IMAGE_IMPORT_DESCRIPTOR 结构的一部分,它是一个联合体(union),在联合体中定义了两个字段,它们在不同的上下文中有不同的意义。
我们具体来看这两个字段:
(1)Characteristics(DWORD)
当 IMAGE_IMPORT_DESCRIPTOR 被用来描述 终止符
(即导入表的最后一项)时,Characteristics 字段的值为 0。
这个字段本来是为了存储额外的信息(如库的属性),但是在导入表的最后一个条目(终止条目)中,Characteristics 被设定为 0,用来标识导入表的结束。
(2)OriginalFirstThunk
OriginalFirstThunk这个RVA所指向的是INT表(Import Name Table),这个表每个数据占4个字节。顾名思义就是表示要导入的函数的名字表。通过上面联合体DUMMYUNIONNAME的注释信息可知,该字段指向的IMAGE_THUNK_DATA这个结构数组,其实就是一个4字节数,本来是一个union类型,能表示4个数,但我们只需掌握两种即可,其余两种已经成为历史遗留了。
typedef struct _IMAGE_THUNK_DATA32 {union {DWORD ForwarderString; // PBYTE DWORD Function; // PDWORDDWORD Ordinal;DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
_IMAGE_THUNK_DATA32
数组中每个IMAGE_THUNK_DATA结构定义了一个导入函数的具体信息,数组的最后以一个内容全为0的IMAGE_THUNK_DATA结构作为结束。当结构的最高位不为0时,表示函数是以序号的方式导入的,这时双字的低两个字节就是函数的序号,当双字最高位为0时,表示函数以函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME结构,此结构定义如下:
typedef struct _IMAGE_IMPORT_BY_NAME
{WORD Hint; // 函数序号CHAR Name[1]; // 导入函数的名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
IMAGE_IMPORT_BY_NAME:前两个字节是一个序号,不是导入序号,一般无用,后面接着就是导入函数名字的字符串,以0结尾。光看文字一定很懵,笔者这边就目前情况做了以下总结,如下图。
2.TimeDateStamp(DWORD)
IMAGE_IMPORT_DESCRIPTOR 结构的 TimeDateStamp 字段是用来记录导入的 动态链接库(DLL) 的 编译时间戳。它表示的是程序编译时所依赖的 DLL 的时间戳,通常是 Unix 时间戳(自 1970 年 1 月 1 日以来的秒数)。
3.ForwarderChain(DWORD)
ForwarderChain 字段用于 函数转发(function forwarding)机制,它在某些情况下指向 下一个导入描述符,而不是直接指向某个函数。这意味着某个 DLL 可能将其部分或所有的函数转发到另一个 DLL 中。通过 ForwarderChain,程序可以知道如何跳转到正确的 DLL 或正确的函数。
4.Name(DWORD)
Name 字段用于存储导入的 动态链接库(DLL) 的 名称,它是一个 相对虚拟地址(RVA),指向一个以空字符(null-terminated)结尾的字符串,这个字符串表示了被导入的 DLL 的文件名。至此基本可以明确一件事情,一个导入表结构对应一个DLL文件,而一个exe肯定会有多个导入表,一个程序中的导入表关系可以用下图来表示。
所以对应Data Directory里的VirtualAddress(RVA)指向的是所有导入表的首地址,每个导入表占20字节,最后以一个空结构体作为结尾(20字节全0结构体)。
5.FirstThunk(DWORD)
在 PE 文件 中,IMAGE_IMPORT_DESCRIPTOR 结构的 FirstThunk 字段用于指向该 DLL 的 导入地址表(IAT,Import Address Table)。在PE文件加载前,IAT表和INT表的完全相同的,所以此时IAT表也可以判断函数导出序号,或指向函数名字结构体。这个阶段可以通过下图表示。
在PE加载后,IAT表就会发生变化,系统会先根据结构体变量Name字段加载对应的dll,读取dll的导出表,对应原程序的INT表,匹配dll导出函数的地址,返回其地址,记录在对应的IAT表上。实际上,在程序加载完成并且链接器已经解析了函数地址后,IAT 表中的条目会被更新为实际的函数地址。这时,IAT 表中存储的内容就是我们运行时用来直接调用函数的地址,而 INT 表中的内容可以忽略不计。PE文件加载后的个字段的关系如下图:
四、导入表解析
在介绍完导入表的结构之后,接着回到我们定位到的导入表位置,对样例文件的导入表进行解析。首先先看第一个导入表信息(高亮部分):
首先我们可以先定位到Name字段,查看该导入表属于哪个DLL。
通过导入表的结构我们可以直接获得Name字段指向的地址:0009 140A(RVA)。通过该RVA我们可以使用CFF explore计算其FOA为:0006560A。
此时定位到0006560A,可知该表为user32.dll的导入表。由此方法我们可以获取第二个导入表对应的DLL信息,可知该表为Kernel32.dll的导入表。
第二个导入表:
获取到的名称:
导入名称表定位:
第一个(User32.dll)导入表的OriginalFirstThunk字段的值为000913CC:
通过计算可知该字段指向的INT表的FOA为:000655CC(_IMAGE_THUNK_DATA32结构)
并且通过定位我们可以发现INT表中仅有一个数值0009 13FC,在这个数值后即出现了0000 0000结束标识,样本程序仅使用了user32.dll中的一个函数。
由于INT表的第一个数值(0009 13FC)此时的高位为0,那么表示此时dll的导入方式为名称导入,所以这个时候FOA地址存储的值就是指向函数名称。
对应结构_IMAGE_IMPORT_BY_NAME
:
typedef struct _IMAGE_IMPORT_BY_NAME
{WORD Hint; // 函数序号CHAR Name[1]; // 导入函数的名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
这个时候将RVA0009 13FC转化为FOA即可获得导入函数的名称。
由此可知,样例程序调用了User32.dll中的MessageBoxA()。接着以同样的方法查看第二个(Kernel32.dll)导入表的INT表RVA为0009 121C:
由RVA定位到文件中INT表(FOA),得到下图:
由该图可知,此时INT表中的函数有81个(双字一函数),且最高位均为0,可知全为名称导入,由于数量较多,我们只查看前两个函数。第一个函数名称RVA地址0009 1732,转化为FOA即可获得该函数名称。
第二个函数名称RVA地址0009 19EE。
五、定位导入地址表(IAT)
要定位IAT就需要用到导入表结构中的FirstThunk字段,这边以第一个导入表为例子进行说明,样例程序的第一个导入表结构中的FirstThunk字段的值为0009 11B0。
将RVA转为FOA得到如下值:
①此时由于PE文件还未载入,所以这个时候获取到的值0009 13FC是指向函数名(_IMAGE_IMPORT_BY_NAME)结构。
②但是当PE文件载入后,FirstThunk字段就会被替换为函数地址。此时将样例程序载入x64dbg中进行分析。通过FirstThunk字段(值为0009 11B0)进行定位。这个时候FirstThunk值为RVA,我们需要算出VA:
VA = ImageBase + RVA
VA = ImageBase(0068 0000) + 0009 11B0 = 0071 11B0
通过VA进行定位,ctrl + G输入地址:
此时成功定位到IAT,定位到的值就是函数的地址76E7 AF50。
在内存窗口右击,选择地址,就可以看到该地址指向的函数:
第二个导入表查看函数地址的方法也一样。在导入表中获得IAT的RVA地址0009 1000。
接着计算VA:
00680000 + 0009 1000 = 0071 1000
在x64dbg中进行定位:
通过工具Denpendency Walker工具也可进行分析查看对应的依赖:
原创 wolven Chan 风铃Sec