延迟导入表,顾名思义就是有一些函数或模块,不需要再程序一启动就加载导入,而是等到真正用到的时候才进行导入的。
延迟导入表和导入表结构基本类似也存在IAT和INT,存在双桥结构,这里不再赘述,关于导入表请参考:导入表
先看一下延迟导入表的结构体。
typedef struct ImgDelayDescr {
DWORD grAttrs; // attributes
RVA rvaDLLName; // RVA to dll name
RVA rvaHmod; // RVA of module handle
RVA rvaIAT; // RVA of the IAT
RVA rvaINT; // RVA of the INT
RVA rvaBoundIAT; // RVA of the optional bound IAT
RVA rvaUnloadIAT; // RVA of optional copy of original IAT
DWORD dwTimeStamp; // 0 if not bound,
// O.W. date/time stamp of DLL bound to (Old BIND)
} ImgDelayDescr, * PImgDelayDescr;
typedef const ImgDelayDescr * PCImgDelayDescr;
grAttrs:用来区分版本,1是新版本,0是旧版本,旧版本中后续的rvaxxxxxx域使用的都是指针,而新版本中都用RVA,我们只讨论新版本。
rvaDLLName:一个RVA,指向导入DLL的名字。
rvaHmod:一个RVA,指向导入DLL的模块基地址,这个基地址在DLL真正被导入前是NULL,导入后才是实际的基地址。
rvaIAT:一个RVA,表示导入函数表,实际上指向IAT,在DLL加载前,IAT里存放的是一小段代码的地址,加载后才是真正的导入函数地址。
rvaINT:一个RVA,指向导入函数的名字表。
rvaUnloadIAT:延迟导入函数卸载表。
dwTimeStamp:延迟导入DLL的时间戳。
我们根据这张图来了解一下延迟导入表的机制。
当执行某一个延迟导入的函数时,找到对应的IAT之后,其中的内容其实很简单,就是将自身地址放入eax中,然后跳转。
.text:75C7A363 __imp_load__InternetConnectA@32: ; InternetConnectA(x,x,x,x,x,x,x,x)
.text:75C7A363 mov eax, offset __imp__InternetConnectA@32
.text:75C7A368 jmp __tailMerge_WININET
跳转到如下的位置,然后执行完毕之后IAT表中的值就变成了正确的函数地址,下次再调用就可以直接调用。
__tailMerge_WININET proc near
.text:75C6BEF0 push ecx
.text:75C6BEF1 push edx
.text:75C6BEF2 push eax
.text:75C6BEF3 push offset __DELAY_IMPORT_DESCRIPTOR_WININET
.text:75C6BEF8 call __delayLoadHelper
.text:75C6BEFD pop edx
.text:75C6BEFE pop ecx
.text:75C6BEFF jmp eax
.text:75C6BEFF __tailMerge_WININET endp
这个__delayLoadHelper只有两个参数,而没有当前函数的名称,那么它是如何完成的呢。
实际上eax中存的是IAT表中该函数的地址(address1),然后我们有__DELAY_IMPORT_DESCRIPTOR_WININET这个结构中有一个rvaIAT其中存的是IAT表的基址(address0),我们通过(address1-address0)/4 就可以得到该函数在INT表中偏移,从而获得函数的名称。