PE文件结构详解(四)PE导入表讲 了一般的PE导入表,这次我们来看一下另外一种导入表:延迟导入(Delay Import)。看名字就知道,这种导入机制导入其他DLL的时机比较“迟”,为什么要迟呢?因为有些导入函数可能使用的频率比较低,或者在某些特定的场 合才会用到,而有些函数可能要在程序运行一段时间后才会用到,这些函数可以等到他实际使用的时候再去加载对应的DLL,而没必要再程序一装载就初始化好。

这个机制听起来很诱人,因为他可以加快启动速度,我们应该如何利用这项机制呢?VC有一个选项,可以让我们很方便的使用到这项特性,如下图所示:

在这一项后面填写需要延迟导入的DLL名称,连接器就会自动帮我们将这些DLL的导入变为延迟导入。

现在我们知道如何使用延迟导入了,那这个看上去很厉害的机制是如何实现的呢?接下来我们来探索一番。在IMAGE_DATA_DIRECTORY
中,有一项为IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT,这一项便延迟导入
表,IMAGE_DATA_DIRECTORY.VirtualAddress就指向延迟导入表的起始地址。既然是表,肯定又是一个数组,每一项都是一个
ImgDelayDescr结构体,和导入表一样,每一项都代表一个导入的DLL,来看看定义:

  1. typedef struct ImgDelayDescr {
  2. DWORD           grAttrs;        // attributes
  3. RVA             rvaDLLName;     // RVA to dll name
  4. RVA             rvaHmod;        // RVA of module handle
  5. RVA             rvaIAT;         // RVA of the IAT
  6. RVA             rvaINT;         // RVA of the INT
  7. RVA             rvaBoundIAT;    // RVA of the optional bound IAT
  8. RVA             rvaUnloadIAT;   // RVA of optional copy of original IAT
  9. DWORD           dwTimeStamp;    // 0 if not bound,
  10. // O.W. date/time stamp of DLL bound to (Old BIND)
  11. } ImgDelayDescr, * PImgDelayDescr;
  12. 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里,默认保存的是一段代码的地址,当程序第一次调用到这个延迟导入函数时,流程会走到那段代码,这段代码用来干什么呢?请看一个真实的延迟导入函数的例子:

  1. .text:75C7A363 __imp_load__InternetConnectA@32:        ; InternetConnectA(x,x,x,x,x,x,x,x)
  2. .text:75C7A363                 mov     eax, offset __imp__InternetConnectA@32
  3. .text:75C7A368                 jmp     __tailMerge_WININET

这段代码其实只有两行汇编,第一行把导入函数IAT项的地址放到eax中,然后用一个jmp跳转走,那么他跳转到哪里了呢?我们继续跟踪:

  1. __tailMerge_WININET proc near
  2. .text:75C6BEF0                 push    ecx
  3. .text:75C6BEF1                 push    edx
  4. .text:75C6BEF2                 push    eax
  5. .text:75C6BEF3                 push    offset __DELAY_IMPORT_DESCRIPTOR_WININET
  6. .text:75C6BEF8                 call    __delayLoadHelper
  7. .text:75C6BEFD                 pop     edx
  8. .text:75C6BEFE                 pop     ecx
  9. .text:75C6BEFF                 jmp     eax
  10. .text:75C6BEFF __tailMerge_WININET endp


中最重要的是push了一个__DELAY_IMPORT_DESCRIPTOR_WININET,这个就是上文中看到的ImgDelayDescr结
构,他的DLL名字是wininet.dll。之后,CALL了一个__delayLoadHelper,在这个函数里,执行了加载DLL,查找导出函
数,填充导入表等一系列操作,函数结束时IAT中已经是真正的导入函数的地址,这个函数同时返回了导入函数的地址,因此之后的eax里保存的就是函数地
址,最后的jmp
eax就跳转到了真实的导入函数中。

这个过程很完美,也很灵巧,但是如果仔细观察就会发现什么地方有点不对劲,你发现了吗?__delayLoadHelper的参数中只有IAT项的偏移和整个模块的延迟导入描述__DELAY_IMPORT_DESCRIPTOR_WININET,但是参数中并没有要导入函数的名字。也许你说,名字在__DELAY_IMPORT_DESCRIPTOR_WININET
的名字表中,是的,那里确实有名字,但是别忘了,那是个表,里面存的是所有要从该模块导入的函数名字,而不是“当前”这个被调用函数的函数名。或许你觉得
参数中应该有个索引号,用来表示名字列表中的第几项是即将被导入的那个函数的名字,不幸的是我们也没有看到参数中有这样的信息存在,那Windows执行
到这里是如何得到名字的呢?MS在这里使用了一个巧妙的办法:__DELAY_IMPORT_DESCRIPTOR_WININET
中有一项是rvaIAT,前面提到了,这里实际上就是指向了IAT,而且是该模块第一个导入函数的IAT的偏移,现在我们有两个偏移,即将导入的函数
IAT项的偏移(记作RVA1)和要导入模块第一个函数IAT项的偏移(记作RVA0),(RVA1-RVA0)/4
=
导入函数IAT项在rvaIAT中的下标,rvaINT中的名字顺序与rvaIAT中的顺序是相同的,所以下标也相同,这样就能获取到导入函数的名字了。
有了模块名和函数名,用GetProcAddress就可以获取到导入函数的地址了。

上述流程用一张图来总结一下:

最后还有两点要提醒大家:

延迟导入的加载只发生在函数第一次被调用的时候,之后IAT就填充为正确函数地址,不会再走__delayLoadHelper了。

延迟导入一次只会导入一个函数,而不是一次导入整个模块的所有函数。

PE文件结构详解(五)延迟导入表的更多相关文章

  1. PE文件结构详解(四)PE导入表

    PE文件结构详解(二)可执行文件头的最后展示了一个数组,PE文件结构详解(三)PE导出表中解释了其中第一项的格式,本篇文章来揭示这个数组中的第二项:IMAGE_DIRECTORY_ENTRY_IMPO ...

  2. PE文件结构详解(六)重定位

    前面两篇 PE文件结构详解(四)PE导入表 和 PE文件结构详解(五)延迟导入表 介绍了PE文件中比较常用的两种导入方式,不知道大家有没有注意到,在调用导入函数时系统生成的代码是像下面这样的: 在这里 ...

  3. PE文件结构详解(二)可执行文件头

    在PE文件结构详解(一)基本概念里,解释了一些PE文件的一些基本概念,从这篇开始,将详细讲解PE文件中的重要结构. 了解一个文件的格式,最应该首先了解的就是这个文件的文件头的含义,因为几乎所有的文件格 ...

  4. PE文件结构详解(三)PE导出表

    上篇文章 PE文件结构详解(二)可执行文件头 的结尾出现了一个大数组,这个数组中的每一项都是一个特定的结构,通过函数获取数组中的项可以用RtlImageDirectoryEntryToData函数,D ...

  5. PE文件结构详解(一)基本概念

    PE(Portable Execute) 文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等,事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是任 何扩展名.那 ...

  6. PE文件结构详解

    (注:最左边是文件头的偏移量.) IMAGE_DOS_HEADER STRUCT { +0h WORD e_magic // Magic DOS signature MZ(4Dh 5Ah) DOS可执 ...

  7. PE文件结构详解(三)

    0x01 前言 上一篇讲到了数据目录表的结构和怎找到到数据目录表(DataDirectory[16]),这篇我们我来讲讲数据目录表后面的另一个结构——区块表. 0x01 区块 区块就是PE载入器将PE ...

  8. PE文件格式详解,第三讲,可选头文件格式,以及节表

    PE文件格式详解,第三讲,可选头文件格式,以及节表 作者:IBinary出处:http://www.cnblogs.com/iBinary/版权所有,欢迎保留原文链接进行转载:) 一丶可选头结构以及作 ...

  9. 【转】pe结构详解

    (一)基本概念 PE(Portable Execute)文件是Windows下可执行文件的总称,常见的有DLL,EXE,OCX,SYS等, 事实上,一个文件是否是PE文件与其扩展名无关,PE文件可以是 ...

随机推荐

  1. VS2013 不能打开DTCMS项目 的解决办法

    <system.webServer> <validation validateIntegratedModeConfiguration="false"/> & ...

  2. UI3_UIbarButtonItem

    // // AppDelegate.m // UI3_UIbarButtonItem // // Created by zhangxueming on 15/7/6. // Copyright (c) ...

  3. mongodb gdal 矢量数据格式驱动

    写了个mongodb的gdal driver,放在了github上,如果你需要,欢迎加入mongogis group. 直接的效果是使得QGIS, GeoServer, MapServer, ArcG ...

  4. 基于OCILIB的oracle数据库操作总结及自动生成Model和Dao的工具

    基于OCILIB的oracle数据库操作总结 1.       类图 2.       基类BaseOCIDao的设计与实现 BaseOCIDao.h头文件 #pragma once /* ----- ...

  5. java基础-注解Annotation原理和用法

    在很多java代码中都可以看到诸如@Override.@Deprecated.@SuppressWarnings这样的字符,这些就是注解Annotation.注解最早在jdk5中被引入,现在已经成为j ...

  6. 数据库连接字符串大全 资料引用:http://www.knowsky.com/339545.html

    转自:http://www.connectionstrings.com/ • SQL Server • ODBC ◦ Standard Security: "Driver={SQL Serv ...

  7. 【Sharing】开发与研发

    [声明]此文为转载,只为收藏. 按:这几天我一直在写这篇东西,本来是胸有成竹,没想到后来越写越发现自己在这个题目下有太多话想说,而以我现在的能力又不能很好地概括总结,以至于越写越长,文章结构也变得混乱 ...

  8. SequoiaDB 与 Hive 集成

    SequoiaDB与Hadoop部署 SequoiaDB与Hadoop在物理上部署方案如下图所示,部署建议如下: l  SequoiaDB与Hadoop部署在相同的物理设备上,以减少Hadoop与Se ...

  9. laravel--has方法--查看关联关系

    has()方法可以用来查询是否有关联关系的一个东西,一般其他的has方法 就是判断这个里面有没有值 $packageOrders = Company::has('packages')->get( ...

  10. 常用的php数组排序函数

    分享几个php数组排序函数,每个函数出去sort是排序的意思前缀字母的含义分别代表: a 索引 k 数组键 r 逆向 u 用户自定义 顺序排序函数 sort — 对数组排序  ksort — 对数组按 ...