第四章 导入表

导入表是PE数据组织中的一个很重要的组成部分,它是为实现代码重用而设置的。通过分析导入表数据,可以获得诸如OE文件的指令中调用了多少外来函数,以及这些外来函数都存在于哪些动态链接库里等信息。Windows加载器在运行PE时会将导入表中声明的动态链接库一并加载到进程的地址空间,并修正指令代码中调用的函数地址。在数据目录中一共有四种类型的数据与导入表数据有关:
导入表、导入函数地址表、绑定导入表、延迟加载导入表。

4.1何为导入表

当程序调用了动态链接库的相关函数,在进行编译和链接的时候,编译程序和链接程序就会将调用的相关信息写入最终生成的PE文件中,以告诉操作系统这些函数的执行指令字节码从哪里能够获取。这些信息就是导入表所要描述的内容。

4.2导入函数

程序开发者在基于汇编语言的源程序中,通过invoke指令调用用户自定义的函数,或者从其他动态链接库中导入的函数。

4.2.1  invoke指令分解

在汇编语言中,程序一旦被编译,编译器会对invoke指令进行适当分解。分解后的指令中将会包含指向导入函数的地址的操作数。当PE文件被装载到内存中时,该操作数就会变成导入函数所在虚拟地址真实的VA。

使用OD打开HelloWorld.exe程序,查看汇编后的字节码以及相关调用如下:

书上的OD结果:

自己本地C++写的一个的OD结果:

VS2012 反汇编结果:

以书上的为解释例子:

将原代码中两个导入函数MessageBoxA和ExitProcess的调用语句解释成字节码分别为:

从指令的反汇编代码中可以看出,以第一个调用为例,对invoke指令的分解操作包含以下三步:

1.压栈。即先将要调用的所有参数push到栈中。(反向顺序压栈)。

2.段内调用。即通过指令call调用一个段内地址,既call 00401018。

3.无条件转移。call指令操作数0x00401018处的值是:FF25 28204000,该字节码反汇编,得到一个无条件跳转指令,跳转到了位置0x00402008处。

(从位置00402008处获取的值是导入函数MessageBoxA在内存中的VA。)

4.2.2  导入函数地址

导入函数是从动态链接库引入的函数,所以,导入函数地址位于被加载的进程地址空间的相应的动态链接库模块内。系统在执行用户程序对导入函数的调用语句时,会跳转到该地址处执行导入函数代码。

使用OD打开HelloWorld.exe,选择地址0x0040101E所在行,在其上单机鼠标右键,选择“数据窗口中跟随”|“内存地址”。OD(3)区就会显示内存从00402000开始的数据:

我的程序得到的是这个:

如上图所示,加粗部分既为导入表数据(大小为3Ch字节)。到目前为止。感觉两个jmp指令中的操作数0x00402008和0x402000都不在该导入表(黑体部分)的范围内,API函数调用好像与导入表无关。其实不是,jmp指令中的操作数虽然不在导入表范围内,但导入表的数据结构中有一个字段是指向这个操作数所在位置的。从跳转指令的操作所指向的位置0x00402008获取的值为77D507EAh。

该值是MessageBoxA这个导入函数在进程HelloWorld.exe中的VA。

现在来对比一下磁盘文件和内存映象的导入函数的地址数据,看看是否存在差别。

4.2.3  导入函数宿主

指令要运行,就必须将指令字节码调入到内存中。既然程序中调用了动态链接库的有关函数,那么程序进程地址空间也一定会有这些函数的指令代码。也就是说,操作系统会在加载时根据导入表的描述将调用的函数指令字节码复制到进程地址空间中。

事实上,操作系统总是会将该函数所处的动态链接库全部复制到地址空间,这些动态链接库便是导入函数的指令宿主。如果一个动态链接库在一个进程中加载过,且在其他进程中也引用了该链接库的函数,操作系统不会再次加载这个动态链接库,而是通过页面调用机制使两个进程同时访问一个动态链接库。也就是说,为了节约内存资源,操作系统只保证有一份代码存在于物理内存中,大家看到的在每个进程中加载的不同地址的相同动态链接库,其实只是在页面存取机制下的一个映射而已。

之后作者证明了这个,做了一些测试和数据说明。这里就直接省略了。直接把结论整理下好了:

编译程序在编译汇编语言源文件时,会把程序中的invoke语句分解成三部分:将参数压栈、call指令、jmp指令部分

call的操作是jmp指令所在的地址;而jmp指令的操作数则是该导入函数在导入表的地址。在程序中所有的导入函数可以排列在一起,组成IAT,动过这样的分解操作配合导入表实现对外部函数的调用。

4.3  PE中的导入表

导入表是数据目录中注册的数据类型之一,其描述信息位于数据目录第2个目录项中。IAT也是数据目录中注册的数据类型之一,其描述信息位于数据目录的第13个目录项中。使用OEDump小工具获取helloworld.exe的数据目录内容如下:

加黑部分为数据目录表中的导入表项,加框部分为导入函数地址表项。

其中下划线部分为导入表数据,共60个字节。方框部分为IAT数据,共16个字节。

4.3.2  导入表描述符IMAGE_IMPORT_DESCRIPTOR

导入表数据的起始是一组导入表描述符结构。没组20个字节,实例中60个字节的导入表数据被分成三个组。前两组均代表两个动态链接库,最后一组为全0结构,表示导入表描述已经结束。可以通过导入表起始地址和这个空结构计算出导入表中引用的动态链接库的个数。

其实,windows在查找导入表的时候并不一定要求最后一组的20个字节都为0,只要其中的字段Name1是0就已经满足结束条件了。导入表的每一组都是一个结构,成为导入表描述符IMAGE_IMPORT_DESCRIPTOR,该结构的具体定义如下:

54.IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk

+0000h,双字。因为它是指向另外数据结构的通路,因此简称为桥1.该字段指向一个包含一系列结构的数组。

指向的数组中的每个结构定义了一个导入函数的信息,最后以一个全0的结构作为结束。指向的数组中每一项为一个结构,此结构的名称是IMAGE_THUNK_DATA。该结构实际上只是一个双字,但在不同的时刻却拥有不同的解释。该字段有两种解释:

双字最高位为0,表示导入符号是一个数值,该数值是一个RVA。

双字最高位为1,表示导入符号是一个名称。

55.IMAGE_IMPORT_DESCRIPTOR.TimeDateStamp

+0004h,双字。时间戳,一般不用,多数为0。如果该导入表被绑定,那么绑定后的这个时间戳就是被设置为对应dll文件的时间戳。操作系统在加载时,可以通过这个时间戳来判断绑定的信息是否过时。

56.IMAGE_IMPORT_DESCRIPTOR.ForwarderChain

+0008h,双字。这个字段的含义和名称并不一致,这里的Name1是一个RVA,它指向该结构所对应的DLL文件的名称,而这个名称是以”\0”结尾的Ansi字符串。

58.IMAGE_IMPORT_DESCRIPTOR.FirstThunk

+0010h,双字。与OriginalFirstThunk相同,它指向的连接表定义了对Name1这个动态链接库引入的所有导入函数,简称桥2。


4.3.3  导入表的双桥结构

桥1和桥2最终指向了一个目的地,都指向了引入函数的“编号-名称”(Hint/Name)描述部分。而从桥2到目的地的过程中,还经理了另外一个很重要的结构IAT。

下图为引入了ExitProcess等3个函数的kernel32.dll的导入表描述符结构示意图。

以下是对helloworld.exe中的导入表数据的详细解释:

>>54 20 00 00

桥1,最高位为0,这是一个RVA,表明函数是以字符串类型的函数名导入的。先将RVA转换为FOA,值为0x00000654,从文件的该位置开始读取双字,知道去除的双字为“0”结束。每一个双字都是结构IMAGE_THUNK_DATA。该结构的详细定义如下:

因为这个动态链接库只调用了一个函数,所以,数组里只有两个元素。这组数中每个都是一个RVA,不过这个RVA却指向了另外一个结构IMAGE_IMPORT_BY_NAME。这个结构大小不确定,是桥1的最终目的地。结构的第一个为字,紧跟着是函数的名字。

从文件偏移0x0000065C开始的数据是(碰到“0”既结束):

9D 01 4D 65 73 73 61 67 65 42 6F 78 41 00

这些值组成的数据结构就是IMAGE_IMPORT_BY_NAME,详细描述如下:

59.IMAGE_IMPORT_BY_NAME.Hint

+0000h,双字。函数的编号,在DLL中对每个函数都进行了编号,访问函数时可以通过名称访问,也可以通过编号访问。

60.IMAGE_IMPORT_BY_NAME.Name1

+0004h,大小不确定。函数名字字符串的具体内容,以“\0”作为字符串结束标志。

其中019dh标识该函数在user32.dll中的编号,后面紧跟着函数名MessageBoxA。

在文件中尽管通过桥2和桥1指向的数据值相同,但其实存储的位置却是不同的。桥1指向的INT与桥2指向的IAT内容完全一样,但INT和IAT却存储在文件的不同位置。

每一个结构IMAGE_IMPORT_DESCRIPTOR都对应一个唯一的动态链接库文件,以及引用了该动态链接库的多个函数,每个函数的最终“值-名称”描述均可沿着桥1或者桥2找到,这种导入表结构被称为双桥结构。

双桥结构的导入表在文件中存在两份内容完全一样的地址列表。一般情况下,桥2指向的地址列表被定义为IAT,而桥1指向的地址列表则被定义为INT(Import
Name Table)。有的连接程序职位导入表存储一个桥,如Borland公司的Tlink只保留桥2,这样的导入表我们称之为单桥结构的导入表。

4.3.4  导入函数地址表

PE文件中所有导入函数jmp指令操作数的集合,组合成另外一个数据结构,这个结构就是导入函数地址表(Import Address Table,IAT)。该地址表示数据目录的第13个数据目录项。

导入表函数地址是一个双字的数组,每个双字代表的是一个导入函数的VA,该地址成称为导入函数地址(IA)。用户程序通过无条件跳转指令跳转到VA指定处,便可以运行引入函数的指令。由于IAT中定义了不止一个连接库的函数,为了区分这些不同链接库引入的函数,规定所有引入函数按照库分类:相同链接库的函数地址排列在一起,最后以一个双字的0结束。IAT结构可以用下图表示:

前面说过,导入表和IAT是有紧密联系的,通过桥2即可定位到IAT。在内存中,桥1可以让你找到调用的函数名称或函数的索引编号,桥2却可以帮助你找到该函数指令代码在内存空间的地址。导入表与IAT的关系如下:

当PE被加载进虚拟地址空间以后,IAT的内容会被操作系统更改为函数的VA。这个修改最终导致通向“值-名称”描述的桥2发生断裂,如下图:

当桥2发生断裂后,如果没有桥1作为参照(因为桥1和桥2维护了两个一一对应的函数RVA),我们就无法重新找到该地址到底是调用了那个函数。这就是为什么在导入表数据结构中存在两个桥的原因,也是为什么单桥导入表结构中无法实施绑定的原因。

4.3.5  多函数导入表

当程序加载到内存以后,导入表部分发生变化的值正是IMAGE_IMPORT_DESCRIPTOR结构中的FirstThunk字段指向的函数指针表内容。这些内容已经不是指向函数名的指针了,而是指向了虚拟内存中该函数的可执行代码的地址!所以其含义也由原来的函数指针更改为函数的入口地址。现在看来,所有的这些值最终都指向了同一片连续的区域,从而形成了我们常说的IAT。

Windows PE 第四章 导入表的更多相关文章

  1. Windows PE 第八章 延迟加载导入表

    延迟加载导入表 延迟加载导入表是PE中引入的专门用来描述与动态链接库延迟加载相关的数据,因为这些数据所引起的作用和结构与导入表数据基本一致,所以称为延迟加载导入表. 延迟加载导入表和导入表是相互分离的 ...

  2. Windows PE第6章 栈与重定位表

    第六章 栈与重定位表 本章主要介绍栈和代码重定位.站和重定位表两者并没有必然的联系,但都和代码有关.栈描述的是代码运行过程中,操作系统为调度程序之间相互调用关系,或临时存放操作数而设置的一种数据结构. ...

  3. Windows Pe 第三章 PE头文件(中)

    这一章的上半部分大体介绍了下PE文件头,下半部分是详细介绍里面的内容,这一章一定要多读几遍,好好记记基础概念和知识,方便之后的学习. 简单回忆一下: 3.4  PE文件头部解析 3.4.1 DOS M ...

  4. Windows Pe 第三章 PE头文件(上)

    第三章  PE头文件 本章是全书重点,所以要好好理解,概念比较多,但是非常重要. PE头文件记录了PE文件中所有的数据的组织方式,它类似于一本书的目录,通过目录我们可以快速定位到某个具体的章节:通过P ...

  5. Oracle11g在Windows和Linux下imp导入表,exp导出表,sqluldr2导出表,sqlldr导入表

    Windows(Win10) 打开cmd 首先输入sqlplus,依次输入用户名.口令 C:\Users\hasee>sqlplus SQL*Plus: Release Production o ...

  6. Windows Pe 第三章 PE头文件(下)

    3.5  数据结构字段详解 3.5.1  PE头IMAGE_NT_HEADER的字段 1.IMAGE_NT_HEADER.Signature +0000h,双字.PE文件标识,被定义为00004550 ...

  7. Windows PE 第十三章 PE补丁技术

    PE补丁技术 这章很多东西之前都见过,也单独总结过,比如动态补丁里说的远程代码注入,还有hijack什么的.之前整理过的这里就不细说了,大体说下思路.这里总结一些之前没总结过的东西. 资料中把补丁分为 ...

  8. 第四章 Web表单

    4.1 跨站请求伪造保护 安装flask-wtf app = Flask(__name__) app.config['SECRET_KEY'] = 'hard to guess string' 密钥不 ...

  9. Windows Pe 第三章 PE头文件-EX-相关编程-1(PE头内容获取)

    获取pE头相关的内容,就是类似如下内容 原理:比较简单,直接读取PE到内存,然后直接强转就行了. #include <windows.h> #include <stdio.h> ...

随机推荐

  1. FreeBSD 乃至开源界中的孔乙己 再论苦难哲学之一

    在许多狂热的FreeBSD 粉丝里,他们甚至不允许别人把FreeBSD写作freebsd,要和你强调,F和BSD都是大写的.还说这是什么尊重之类的东西.大抵和孔乙己的茴香豆的茴的有四种写法一样吧:&q ...

  2. Maven配置ali镜像

    Maven目录,Conf文件夹下settings.xml 找到mirrors节点 添加配置 <mirror> <id>alimaven</id> <mirro ...

  3. 简单ping确定网络故障

    1.ping localhost (127.0.0.1) 目的:确定TCP/IP有无问题 2.ping本机地址 用来检测网卡驱动有无问题 如何获取本机地址? win+r cmd 输入ipconfig/ ...

  4. 关于java的访问修饰符权限

    作用域    public protected default private  同一个类   yes     yes      yes      yes 同一个包   yes     yes    ...

  5. 事件 on

    $(选择器).on(事件名称,事件的处理函数) 事件名称:js事件去掉on的部分,例如js中onclick,这里就是click 例如:<input type="button" ...

  6. P1060_开心的金明(JAVA语言)

    思路 0/1背包问题 模板 //暴力出奇迹 题目描述 金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间.更让他高兴的是,妈妈昨天对他说:"你的房间需要购买哪些 ...

  7. ch2_8_1求解n阶螺旋矩阵问题

    思路:循环输出,注意边界控制 import java.util.Scanner; public class ch2_8_1求解n阶螺旋矩阵问题 { public static void main(St ...

  8. 一文彻底搞懂JS前端5大模块化规范及其区别

    码文不易,转载请带上本文链接,感谢~ https://www.cnblogs.com/echoyya/p/14577243.html 目录 码文不易,转载请带上本文链接,感谢~ https://www ...

  9. Redis 超详细自动管理Cluster集群工具上手 redis-trib.rb (多图,手把手)

    安装介绍 ​ redis-trib.rb是一款由Redis官方提供的集群管理工具,能够大量减少集群搭建的时间. ​ 除此之外,还能够简化集群的检查.槽迁徙.负载均衡等常见的运维操作,但是使用前必须要安 ...

  10. vue-router 监控全局路由,在路由中改变vuex中的状态值