首页 > 其他分享 >【PE文件结构】导入表

【PE文件结构】导入表

时间:2025-01-18 15:43:37浏览次数:1  
标签:文件 函数 IMAGE DLL 地址 导入 PE IAT

导入表(Import Table)是Windows可执行文件中的一部分,它记录了程序所需调用的外部函数(或API)的名称,以及这些函数在哪些动态链接库(DLL)中可以找到。在PE文件运行过程需要依赖哪些模块,以及依赖这些模块中的哪些函数,这些信息就记录在导入表中。在Win32编程中我们会经常用到导入函数,导入函数就是程序调用其执行代码又不在程序中的函数,这些函数通常是系统提供给我们的API,在调用者程序中只保留一些函数信息,包括函数名机器所在DLL路径。当程序需要调用某个函数时,它必须知道该函数的名称和所在的DLL文件名,并将DLL文件加载到进程的内存中,导入表就是告诉程序这些信息的重要数据结构。
导入表(Import Table) 中有两个重要的部分:INTIAT。这两个部分在 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 descriptor
        DWORD   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 forwarders
    DWORD   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 descriptor
        DWORD   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;             // PDWORD
        DWORD 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

标签:文件,函数,IMAGE,DLL,地址,导入,PE,IAT
From: https://www.cnblogs.com/o-O-oO/p/18678508

相关文章

  • 打包前端项目时报错:Task function must be specified
    注意:以下示例是你前端环境安装好的情况下排查的问题,前端环境没安装好请自行安装好报错示例:输入命令: gulp-v查看全局gulp和本地项目的gulp版本  这里可以看出这两个版本不一致,这时我们需要在项目里去修改一下对应的版本,改成3.0.0(这里根据自己的需求更改就好)重新运行......
  • 操作系统进程-进程间通信的概述、匿名管道pipe和有名管道mkfifo函数的介绍及应用
    进程间通信(IPC)概述进程间通信(InterProcessCommunication)是指在两个或多个不同的进程间传递或者交换信息。进程是一个独立的资源管理单元,不同的进程之间资源是独立的,不能在一个进程中直接访问另一个进程的资源,但是进程间不是孤立的,也需要一些信息的交互和状态传递,所以就......
  • CDR文件版本转换器 v1.5 (支持CDR2023-X8)
    CDR版本转换器v1.5是一款非常实用的工具,专为CorelDRAW文件版本转换而设计。它支持从X4、X5、X6、X7、X8等多个版本之间的转换,让你不再需要求助他人,轻松搞定版本转换! 使用说明:1、将压缩文件解压到固定位置,不要随意移动。2、解压后,双击start_CDR.bat来运行软件下载地址(......
  • C++新文件模板
    1.普通模板#include<bits/stdc++.h>usingnamespacestd;#defineinfile"infile.in"#defineoutfile"outfile.out"#definecin_cout_f#definespeedup#ifdefcin_cout_f#definecin_____in_____#definecout_____out_____ifstream____......
  • Linux 常用命令——文件目录篇(保姆级说明)
    文件及目录类列出当前目录中的文件和子目录(ls)ls[-参数][name...]#列出所有根目录ls/#列出所有txt文件ls*.txt参数:-a显示所有文件及目录(.开头的隐藏文件也会列出)-d只列出目录(不递归列出目录内的文件)。-l以长格式显示文件和目录信息,包括权限、所有......
  • 【WRF理论第九期】输出文件:wrfout 和 wrfrst
    【WRF理论第九期】输出文件:wrfout和wrfrst1.wrfout文件wrfout文件读取(Python)2.wrfrst文件参考在WRF(WeatherResearchandForecasting)模型中,wrfout和wrfrst是两种重要的输出文件,分别代表不同类型的模拟结果和功能。1.wrfout文件wrfout文件是......
  • JS上传文件夹的三种解决方案
    要求:免费,开源,技术支持技术:百度webuploader,分块,切片,断点续传,秒传,MD5验证,纯JS实现,支持第三方软件集成前端:vue2,vue3,vue-cli,html5,webuploader后端:asp.net,.netmvc,.netcore,asp,jsp,java,springboot,php,数据库:MySQL,Oracle,SQLServer,达梦,人大金仓,国产数据库平......
  • JSP如何实现文件断点上传和断点下载?
    要求:免费,开源,技术支持技术:百度webuploader,分块,切片,断点续传,秒传,MD5验证,纯JS实现,支持第三方软件集成前端:vue2,vue3,vue-cli,html5,webuploader后端:asp.net,.netmvc,.netcore,asp,jsp,java,springboot,php,数据库:MySQL,Oracle,SQLServer,达梦,人大金仓,国产数据库平......
  • WEBUPLOADER怎样上传文件夹
    要求:免费,开源,技术支持技术:百度webuploader,分块,切片,断点续传,秒传,MD5验证,纯JS实现,支持第三方软件集成前端:vue2,vue3,vue-cli,html5,webuploader后端:asp.net,.netmvc,.netcore,asp,jsp,java,springboot,php,数据库:MySQL,Oracle,SQLServer,达梦,人大金仓,国产数据库平......
  • Arduino 平台下 ESP32-P4 MP3音频文件播放
    ESP32-P4开发板arduino平台下从SD_MMC读取MP3文件播实验程序,初步验证成功。开发板使用斑梨电子JC1060P470_P4,板载ES8311音频解码器和四线SD卡模块。ES8311在Arduino下驱动使用了github上某国外猿的驱动代码,并搭配ESP32-AudioI2S库I2Saudio示例实现。原来想直接使用psch......