目录
一、延迟载入表
1.1 延迟加载简介
延迟加载的DLL属于隐含链接的DLL,它的加载机制很特别。一般来说,程序运行到某个阶段,代码里要是引用了这种DLL包含的特定符号,像函数名或者变量名,系统才会开始加载这个DLL。
这种延迟加载和直接用`LoadLibrary`、`GetProcAddress`函数达到的效果差不多。拿大型图形处理软件来说,软件启动的时候要加载好多DLL,才能实现各种复杂功能。要是采用延迟加载机制,软件启动时就只加载核心模块。等用户进行特定图形编辑操作,触发对特定DLL符号的引用时,相关DLL才会被加载。这么做,软件启动速度会快很多,而且在不同Windows系统版本上,还能更好地解决因为系统DLL版本不一致产生的兼容性问题。
要知道,使用延迟加载机制,对DLL本身没什么特殊要求。不管是系统自带的DLL,还是开发者自己编写的DLL,都能通过这种机制实现延迟加载。
1.2 延迟加载实现
若要在EXE或其他DLL中启用延迟加载机制,可按以下步骤操作:
(1)包含必要的头文件及静态库
#include <windows.h>
#include <delayimp.h>
#pragma comment(lib,"Delayimp.lib")
`windows.h`是Windows系统编程里很关键的头文件,里面有大量Windows API的声明。`delayimp.h`专门针对延迟加载机制,定义了相关的数据结构和函数。用`#pragma comment(lib,"Delayimp.lib")`这个指令,可以把`Delayimp.lib`静态库链接到项目里,这样就能实现延迟加载功能了。
(2)设置连接器选项
在Visual Studio这类集成开发环境里,要在项目属性中找到“连接器”设置,然后进入“输入”选项卡。在“延迟加载的DLL”这一项里,填上需要延迟加载的DLL名称,注意名称的大小写要和实际DLL名称一模一样,不然没办法正确实现延迟加载。
(3)设置卸载功能(如果有需要)
要是项目需要卸载延迟加载的DLL,还是在项目属性的“连接器”设置里,找到“高级”选项卡,把“卸载延迟加载”勾选上。
完成上面这些设置后,就可以像用显式加载的DLL那样,调用这个DLL导出的函数。两者只有一个区别,就是延迟加载的DLL,只有在实际用到它导出的具体API时,托管代码才会开始加载它。
另外,如果确定在很长一段时间内都不会再用某个延迟加载的DLL了,就可以调用`FUnloadDelayLoadedDLL2("xxx.dll")`函数把它卸载掉。要特别留意,这个函数的参数只能是单纯的DLL文件名,不能有路径信息。而且,延迟加载的DLL只能用这个函数来释放,要是错误地用`FreeLibrary`去卸载,会让导入函数的地址没办法正常清除,在后续程序运行的时候,访问相关函数就会出现访问异常错误。
1.3 延迟加载的机理
DLL延迟加载机制的关键是`delayLoadHelper()`函数。程序调用延迟加载函数时,其实调用的是`delayLoadHelper()`。这个函数会去引用可执行文件里特殊的`Delay Import`节,这个节存着延迟加载DLL的相关信息。在`delayLoadHelper()`函数里面,会自动按顺序调用`LoadLibrary()`和`GetProcAddress()`函数,`LoadLibrary()`用来加载DLL,`GetProcAddress()`用来获取DLL里目标函数的地址。
`delayLoadHelper()`函数成功拿到延迟加载函数的地址后,会巧妙处理后续对该函数的调用,让后续调用能直接指向延迟加载函数,避免重复加载和获取地址,提升程序运行效率。
延迟加载结构体
typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR {
DWORD AllAttributes; // 1(没用)为0
DWORD DllNameRVA; // 2(有用)延迟加载的DLL名字
DWORD ModuleHandleRVA; // 3(有用)
DWORD ImportAddressTableRVA;// 4(重要)延迟载入IAT的RVA (PIMAGE_THUNK_DATA)
DWORD ImportNameTableRVA; // 5(重要)延迟载入INT的RVA
DWORD BoundImportAddressTableRVA; // 6(有用)绑定IAT的RVA
DWORD UnloadInformationTableRVA; // 7(有用)
DWORD TimeDateStamp; // 8(若未绑定)
} IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;
在这个结构体里,`ImportAddressTableRVA`和`ImportNameTableRVA`这两个部分特别重要。它们的解析方法,跟导入表的解析方法是一样的。系统通过解析这些信息,就能精准定位到延迟加载DLL的相关内容,从而实现延迟加载功能 。
二、导入地址表IAT
2.1 IAT简介
在Windows可执行文件(PE文件)的数据目录表中,第13项直接指向IAT表(Import Address Table,导入地址表)。从数据目录出发,可找到`IMAGE_THUNK_DATA`数组,该数组构成了IAT表的主体。
IAT表在PE文件中扮演着关键角色,主要用于记录程序运行时所需调用的外部DLL函数的实际地址。以一款游戏程序为例,在程序启动阶段,Windows加载器会依据IAT表中的信息,遍历各个需要导入的DLL,并将这些DLL中函数的实际地址填充到IAT表中。当游戏程序运行过程中调用外部函数时,便能通过IAT表快速找到对应的正确地址,确保游戏功能正常实现。
数据目录表的第12项是可选的。若存在第12项,它指向的导入表和从第2项得到的IAT表功能相同。这一设计主要是为了兼容不同的PE文件格式,或是满足特定的编程需求。在实际的PE文件分析和处理工作中,第13项指向的IAT表更受关注,因为它是程序运行时动态解析外部函数地址的核心环节。
导入地址表(Import Address Table,IAT)在Windows系统中通常通过`IMAGE_IMPORT_DESCRIPTOR`和`IMAGE_THUNK_DATA`等结构体来表示,下面对相关结构体进行详细定义及说明:
// 定义IMAGE_IMPORT_DESCRIPTOR结构体
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
// 定义IMAGE_THUNK_DATA结构体
typedef struct _IMAGE_THUNK_DATA {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
};
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;
`IMAGE_IMPORT_DESCRIPTOR`结构体用于描述一个导入库的详细信息:
- `OriginalFirstThunk`:指向导入名称表(Import Name Table,INT)的相对虚拟地址(RVA)。若该值为0,表明该导入库没有导入名称表。- `TimeDateStamp`:导入库的时间戳,在进行版本控制时,开发人员可通过对比时间戳,判断导入库的版本是否符合要求。
- `ForwarderChain`:用于处理转发导入的链信息,当一个DLL将函数转发到另一个DLL时,该字段发挥重要作用。
- `Name`:指向导入库名称字符串的相对虚拟地址(RVA),通过该地址,系统能够确定需要导入的DLL名称。
- `FirstThunk`:指向导入地址表(IAT)的相对虚拟地址(RVA),在程序运行时,系统会将实际的函数地址填充到这个表中。
`IMAGE_THUNK_DATA`结构体用于表示导入函数的相关信息,作为一个联合体,在不同场景下可表示不同信息:
- `ForwarderString`:若为转发导入,这里指向转发字符串的地址,通过该字符串,系统能够找到转发的目标。
- `Function`:在导入地址表中,这里存储着实际导入函数的地址,程序运行时通过该地址调用函数。
- `Ordinal`:当函数通过序号导入时,这里存储着函数的序号,系统可根据序号找到对应的函数。
- `AddressOfData`:指向`IMAGE_IMPORT_BY_NAME`结构体的地址,该结构体包含了导入函数的名称等详细信息。
2.2 IAT的作用
借助这些结构体,程序在运行时能够动态解析和调用导入的函数,实现对外部库函数的高效引用。
IAT表的结构和内容在逆向工程、病毒分析、程序调试等领域有着极高的应用价值。在逆向工程中,研究人员通过分析IAT表,能够深入了解程序的功能架构和依赖关系,为程序的逆向分析提供重要线索。在病毒分析工作中,通过分析病毒程序的IAT表,安全人员可以发现病毒调用的系统API函数,进而判断病毒的攻击方式、传播途径以及潜在危害。
在实际编程过程中,开发人员也可通过修改IAT表实现一些特殊功能,如API拦截。通过将IAT表中某个函数的地址修改为自定义函数地址,当程序调用该函数时,会转而执行自定义函数,从而实现对程序行为的精准监控和灵活修改。
总之,IAT表是PE文件中极为重要的数据结构,不仅对程序的正常运行和动态链接起着决定性作用,还为开发人员和安全研究人员分析、修改程序提供了关键切入点 。