PE文件结构:导入表

admin 2025年1月15日19:26:50评论7 views字数 8682阅读28分56秒阅读模式

导入表(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文件结构:导入表

定位导入表

在 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打开样例文件。

PE文件结构:导入表

在NT头部中定位到DataDirectory:

PE文件结构:导入表

DataDirectory中第二个元素就记录着导入表的RVA和大小。

PE文件结构:导入表

接着我们可以通过RVA去计算出转化为FOA,这边直接使用CFF Explorer.exe进行计算。使用CFF Explorer打开样例程序文件,选中Address Converter,接着在RVA处输入我们刚刚获取到的导入表的RVA,此时我们就能够获得对应的FOA

PE文件结构:导入表

此处我们获得的FOA000653E0,接着在010 Editor中进行(Ctrl + G)定位即可。

PE文件结构:导入表

在定位到导入表后我们就可以对导入表的结构进行解析。

导入表的结构

查看导入表的结构只需要我们打开Visual Studio任意项目的任意C/C++文件,接着在文件中输入:

_IMAGE_IMPORT_DESCRIPTOR

随后按住ctrl,点击结构体即可进行结构查看。

PE文件结构:导入表

导入表的结构如下:

PE文件结构:导入表
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;

DUMMYUNIONNAMEIMAGE_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个数,但我们只需掌握两种即可,其余两种已经成为历史遗留了。

PE文件结构:导入表
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结构,此结构定义如下:

PE文件结构:导入表
typedefstruct_IMAGE_IMPORT_BY_NAME{WORDHint;          // 函数序号CHARName[1];        // 导入函数的名称IMAGE_IMPORT_BY_NAME*PIMAGE_IMPORT_BY_NAME;

IMAGE_IMPORT_BY_NAME:前两个字节是一个序号,不是导入序号,一般无用,后面接着就是导入函数名字的字符串,以0结尾。光看文字一定很懵,笔者这边就目前情况做了以下总结,如下图。

PE文件结构:导入表
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肯定会有多个导入表,一个程序中的导入表关系可以用下图来表示。

PE文件结构:导入表

所以对应Data Directory里的VirtualAddress(RVA)指向的是所有导入表的首地址,每个导入表占20字节,最后以一个空结构体作为结尾(20字节全0结构体)。

PE文件结构:导入表
5.FirstThunk(DWORD)

在 PE 文件 中,IMAGE_IMPORT_DESCRIPTOR 结构的 FirstThunk 字段用于指向该 DLL 的 导入地址表(IAT,Import Address Table)。在PE文件加载前,IAT表和INT表的完全相同的,所以此时IAT表也可以判断函数导出序号,或指向函数名字结构体。这个阶段可以通过下图表示。

PE文件结构:导入表

在PE加载后,IAT表就会发生变化,系统会先根据结构体变量Name字段加载对应的dll,读取dll的导出表,对应原程序的INT表,匹配dll导出函数的地址,返回其地址,记录在对应的IAT表上。实际上,在程序加载完成并且链接器已经解析了函数地址后,IAT 表中的条目会被更新为实际的函数地址。这时,IAT 表中存储的内容就是我们运行时用来直接调用函数的地址,而 INT 表中的内容可以忽略不计。PE文件加载后的个字段的关系如下图:

PE文件结构:导入表

导入表解析

在介绍完导入表的结构之后,接着回到我们定位到的导入表位置,对样例文件的导入表进行解析。首先先看第一个导入表信息(高亮部分):

PE文件结构:导入表

首先我们可以先定位到Name字段,查看该导入表属于哪个DLL。

PE文件结构:导入表

通过导入表的结构我们可以直接获得Name字段指向的地址:0009 140A(RVA)。通过该RVA我们可以使用CFF explore计算其FOA为:0006560A

PE文件结构:导入表

此时定位到0006560A,可知该表为user32.dll的导入表。由此方法我们可以获取第二个导入表对应的DLL信息,可知该表为Kernel32.dll的导入表。

第二个导入表:

PE文件结构:导入表

获取到的名称:

PE文件结构:导入表
导入名称表定位:

第一个(User32.dll)导入表的OriginalFirstThunk字段的值为000913CC

PE文件结构:导入表

通过计算可知该字段指向的INT表的FOA为:000655CC(_IMAGE_THUNK_DATA32结构)

PE文件结构:导入表

并且通过定位我们可以发现INT表中仅有一个数值0009 13FC,在这个数值后即出现了0000 0000结束标识,样本程序仅使用了user32.dll中的一个函数。

PE文件结构:导入表

由于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即可获得导入函数的名称。

PE文件结构:导入表

由此可知,样例程序调用了User32.dll中的MessageBoxA()。接着以同样的方法查看第二个(Kernel32.dll)导入表的INT表RVA为0009 121C:

PE文件结构:导入表

由RVA定位到文件中INT表(FOA),得到下图:

PE文件结构:导入表

由该图可知,此时INT表中的函数有81个(双字一函数),且最高位均为0,可知全为名称导入,由于数量较多,我们只查看前两个函数。第一个函数名称RVA地址0009 1732,转化为FOA即可获得该函数名称。

PE文件结构:导入表

第二个函数名称RVA地址0009 19EE

PE文件结构:导入表

定位导入地址表(IAT)

要定位IAT就需要用到导入表结构中的FirstThunk字段,这边以第一个导入表为例子进行说明,样例程序的第一个导入表结构中的FirstThunk字段的值为0009 11B0

PE文件结构:导入表

将RVA转为FOA得到如下值:

PE文件结构:导入表

①此时由于PE文件还未载入,所以这个时候获取到的值0009 13FC是指向函数名(_IMAGE_IMPORT_BY_NAME)结构。

PE文件结构:导入表

②但是当PE文件载入后,FirstThunk字段就会被替换为函数地址。此时将样例程序载入x64dbg中进行分析。通过FirstThunk字段(值为0009 11B0)进行定位。这个时候FirstThunk值为RVA,我们需要算出VA:

VA = ImageBase + RVA
PE文件结构:导入表
VA = ImageBase(0068 0000) + 0009 11B0 = 0071 11B0

通过VA进行定位,ctrl + G输入地址:

PE文件结构:导入表

此时成功定位到IAT,定位到的值就是函数的地址76E7 AF50

PE文件结构:导入表

在内存窗口右击,选择地址,就可以看到该地址指向的函数:

PE文件结构:导入表

第二个导入表查看函数地址的方法也一样。在导入表中获得IAT的RVA地址0009 1000

PE文件结构:导入表

接着计算VA:

00680000 + 0009 1000 = 0071 1000

在x64dbg中进行定位:

PE文件结构:导入表

通过工具Denpendency Walker工具也可进行分析查看对应的依赖:

PE文件结构:导入表

原文始发于微信公众号(风铃Sec):PE文件结构:导入表

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月15日19:26:50
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   PE文件结构:导入表https://cn-sec.com/archives/3632108.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息