导入表(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 函数的区别
①程序中的自定义函数:在程序内部(比如静态库或当前程序中的函数),调用这些函数时,函数地址是固定的。编译器在编译时会确定函数的地址,因为函数的地址在程序加载时就已经确定了。
②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的地址,
CALLDWORDPTRDS:[0x88223344]
定位导入表
在 PE
文件头中,找到 Optional Header
,然后查看其中的 Data Directory
,数组中的第二个元素保存的就是导入表的 RVA
以及大小。回顾之前的文章《PE文件结构:节表》。
DataDirectory
是一个长度为 16 的数组,它包含指向导入表、导出表、资源表等数据的相对虚拟地址(RVA)和该数据的大小,结构如下:
typedefstruct_IMAGE_DATA_DIRECTORY {DWORDVirtualAddress;DWORDSize;}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
定位导入表的流程
①在PE头中找到DataDirectory
②获取DataDirectory
的第二项(下标为1):DataDirectory[1]
中导入表的RVA
③将导出表的RVA
转换为FOA
,在文件中定位到导入表
定位实例
这里我们还是用之前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,点击结构体即可进行结构查看。
导入表的结构如下:
typedefstruct_IMAGE_IMPORT_DESCRIPTOR {union {DWORDCharacteristics; // 0 for terminating null import descriptorDWORDOriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME;DWORDTimeDateStamp; // 0 if not bound,// -1 if bound, and real datetime stamp// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)// O.W. date/time stamp of DLL bound to (Old BIND)DWORDForwarderChain; // -1 if no forwardersDWORDName;DWORDFirstThunk; // RVA to IAT (if bound this IAT has actual addresses)} IMAGE_IMPORT_DESCRIPTOR;typedefIMAGE_IMPORT_DESCRIPTORUNALIGNED*PIMAGE_IMPORT_DESCRIPTOR;
1.DUMMYUNIONNAME(DWORD)
union {DWORDCharacteristics; // 0 for terminating null import descriptorDWORDOriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) } DUMMYUNIONNAME;
DUMMYUNIONNAME
是IMAGE_IMPORT_DESCRIPTOR
结构的一部分,它是一个联合体(union
),在联合体中定义了两个字段,它们在不同的上下文中有不同的意义。我们具体来看这两个字段:
①Characteristics(DWORD)
当 IMAGE_IMPORT_DESCRIPTOR
被用来描述 终止符(即导入表的最后一项)时,Characteristics
字段的值为 0。
这个字段本来是为了存储额外的信息(如库的属性),但是在导入表的最后一个条目(终止条目)中,Characteristics
被设定为 0,用来标识导入表的结束。
②OriginalFirstThunk
OriginalFirstThunk
这个RVA所指向的是INT
表(Import Name Table),这个表每个数据占4个字节。顾名思义就是表示要导入的函数的名字表。通过上面联合体DUMMYUNIONNAME的注释信息可知,该字段指向的IMAGE_THUNK_DATA这个结构数组,其实就是一个4字节数,本来是一个union类型,能表示4个数,但我们只需掌握两种即可,其余两种已经成为历史遗留了。
typedefstruct_IMAGE_THUNK_DATA32 {union {DWORDForwarderString; // PBYTE DWORDFunction; // PDWORDDWORDOrdinal;DWORDAddressOfData; // PIMAGE_IMPORT_BY_NAME } u1;} IMAGE_THUNK_DATA32;typedefIMAGE_THUNK_DATA32*PIMAGE_THUNK_DATA32;
_IMAGE_THUNK_DATA32
数组中每个IMAGE_THUNK_DATA
结构定义了一个导入函数的具体信息,数组的最后以一个内容全为0的IMAGE_THUNK_DATA
结构作为结束。当结构的最高位不为0时,表示函数是以序号的方式导入的,这时双字的低两个字节就是函数的序号,当双字最高位为0时,表示函数以函数名方式导入,这时双字的值是一个RVA,指向一个用来定义导入函数名称的IMAGE_IMPORT_BY_NAME
结构,此结构定义如下:
typedefstruct_IMAGE_IMPORT_BY_NAME{WORDHint; // 函数序号CHARName[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
:
typedefstruct_IMAGE_IMPORT_BY_NAME{WORDHint; // 函数序号CHARName[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工具也可进行分析查看对应的依赖:
原文始发于微信公众号(风铃Sec):PE文件结构:导入表
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论