【PE文件结构】导入表

news/2025/1/18 15:37:57/文章来源:https://www.cnblogs.com/o-O-oO/p/18678508

导入表(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 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/871264.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ciscn_2019_es_2(栈迁移)

看一下ida两个read函数都是读取0x30(48),然后s距离ebp有0x28(40),所以虽然有溢出但只溢出了两个4字节,也就是只能覆盖到ebp和ret。 这时候就需要运用栈迁移 栈迁移就是当溢出不够多的时候,这时候可以考虑把栈给迁移去其它地方,利用leave_ret指令控制ebp,使其指向我们…

Nexpose 7.3.0 for Linux Windows - 漏洞扫描

Nexpose 7.3.0 for Linux & Windows - 漏洞扫描Nexpose 7.3.0 for Linux & Windows - 漏洞扫描 Rapid7 on-prem Vulnerability Management, released Jan 15, 2025 请访问原文链接:https://sysin.org/blog/nexpose-7/ 查看最新版。原创作品,转载请保留出处。 作者主页…

ABB机器人3HNE00313-1示教器黑屏故障维修

随着工业自动化的快速发展,ABB机器人示教器在生产线上的应用越来越广泛。然而,在使用过程中,示教器偶尔也会出现故障,其中比较常见的一种是ABB工业机械手示教器黑屏故障。 一、ABB工业机器人示教盒黑屏故障原因分析 1. 硬件故障:硬件故障是导致示教器黑屏的主要原因之一。…

windows双击查看ip

如何方便查看本地电脑的ip? 直接上干货: 1、在桌面右击,新建 => 文本文档; 2、重命令为ip.txt 3、双击打开,输入 ipconfig pause , 然后保存; 4、右击文档,重命令为“ip.bat”;5、再次双击此文档,ip地址就出来了; ps: ipconfig:是系统命令,用于查看ip的地址…

一个日h站的Nday

0x00 前言 今天先来无事的我翻起了qq收藏夹。忽然发现了一个去年EDUSRC群里一个老表发的洞。今天就给大家发出来耍耍。 抵制黄色网站人人有责,打造绿色上网环境。 面对正规网站时候,请不要做非法测试!!! 废话就不多说了。 0x01正文 fofa语句 body="<script type=t…

【触想智能】工业电脑一体机在数控机床设备上应用的注意事项以及工业电脑日常维护知识分享

数控技术的应用不但给传统制造业带来了革命性的变化,使制造业成为工业化的象征,而且随着数控技术的不断发展和应用领域的扩大,它对国计民生的一些重要行业(IT、汽车、轻工、医疗等)的发展起着越来越重要的作用,因为这些行业所需装备的数字化已是现代发展的大趋势。随着数控…

day0java准备

Java-001 Markdown 暂时跳过,vscode里未能实现编译 java 1.已配置环境 2.继续学黑马程序员:已到50/200 3.内存4.学到方法(C里函数)

NB!一款基于java开发的漏洞检测工具,集合了泛微、用友、大华、海康、致远、红帆、万户、帆软等漏洞

1、工具介绍 基于 https://github.com/yhy0/ExpDemo-JavaFX 上添加poc 2、工具下载链接: 工具下载:工具下载 3、新增检测漏洞用友NC-Cloud系统接口getStaffInfo存在SQL注入漏洞 用友U8-Cloud ReleaseRepMngAction存在SQL注入漏洞复现(CNVD-2024-33023) 用友U8-CRM系统getDeptN…

网络购物数据分析

#获取数据 import pandas as pd infor=pd.read_csv(buy_input_1.csv) infor.head(20) #选取预观察数据#方法1 # x1=infor["Annual Income"] # print(x1) # x2=infor["Spending Score"] # print(x2)# """ #方法2 # x1=infor.iloc[:,3] # …

了解ESP32睡眠模式及其功耗

转载自:https://lastminuteengineers.com/esp32-sleep-modes-power-consumption/ Insight Into ESP32 Sleep Modes & Their Power ConsumptionThe ESP32 is undeniably a worthy competitor to many WiFi/MCU SoCs, outperforming them in both performance and price. Ho…

某公交管理系统简易逻辑漏洞+SQL注入挖掘

某公交管理系统挖掘 SQL注入漏洞 前台通过给的账号密码,进去 按顺序依次点击1、2、3走一遍功能点,然后开启抓包点击4当点击上图的4步骤按钮时,会抓到图下数据包,将其转发到burp的重放模块构造以下注入poc,可见注入延时了五秒,用户输入的语句成功拼接到原有的SQL语句上执行…

记一次常规的网络安全渗透测试

前言 上个月根据领导安排,需要到本市一家电视台进行网络安全评估测试。通过对内外网进行渗透测试,网络和安全设备的使用和部署情况,以及网络安全规章流程出具安全评估报告。本文就是记录了这次安全评估测试中渗透测试部分的内容,而且客户这边刚刚做过了一次等保测评,算一下…