第四章 导入表

导入表是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. Django 使用 pycharm 创建新的app(可以理解为模块)

    创建工程的时候,注意选择Existing interpreter 选择对应的 python 解释器,电脑如果安装有多个版本的 Python 的话,注意python版本的问题, 以上即是创建的项目目录, ...

  2. Python编程中可能经常用到的函数

    1.os.walk() 一般用法为 import os ph=r'D:\temp\build' for root,dirs,files in os.walk(ph): print(root,dirs, ...

  3. Srping源码之XMLBeanFactory

    ​ 本文是针对Srping的XMLBeanFactory来进行解析xml并将解析后的信息使用GenericBeanDefinition作为载体进行注册,xmlBeanFactory已经在Spring ...

  4. P1996_约瑟夫问题(JAVA语言)_可能是最简单的解法了!

    思路:使用队列模拟. 判断是否为出圈的数.如果不是,把数加入队列尾部:如果是,输出并删除. 题目背景 约瑟夫是一个无聊的人!!! 题目描述 n个人(n<=100)围成一圈,从第一个人开始报数,数 ...

  5. 攻防世界 reverse 进阶 notsequence

    notsequence  RCTF-2015 关键就是两个check函数 1 signed int __cdecl check1_80486CD(int a1[]) 2 { 3 signed int ...

  6. HOOK实现游戏无敌-直接修改客户端-2-使用VS来处理

    HOOK实现游戏无敌-直接修改客户端-2-使用VS来处理 大概流程 1 首先找到游戏进程,打开进程 2 申请一段内存空间来保存我们的硬编码(virtualAllocEx) 3 找到攻击函数,修改函数的 ...

  7. Istio 网络弹性 实践 之 故障注入 和 调用重试

    网络弹性介绍 网络弹性也称为运维弹性,是指网络在遇到灾难事件时快速恢复和继续运行的能力.灾难事件的范畴很广泛,比如长时间停电.网络设备故障.恶意入侵等. 重试(attempts) Istio 重试机制 ...

  8. 用RUST写流媒体服务器实战——rtmp chunk 深入解析

    用RUST写流媒体服务器实战--rtmp chunk 深入解析 最近几个月断更了,把精力放在了新的开源项目上,一个用rust写的流媒体服务xiu. 实现过程中踩了不少坑,今天说下rtmp中的chunk ...

  9. vs2019新建数据库后插入中文变问号

    在使用VS创建了数据库后如果直接给字符类型插入中文内容的话查询结果插入的中文会以"?"的格式展现. 原因是因为默认创建的数据库的排序类型为拉丁文不支持中文. 所以需要讲这个排序的字 ...

  10. Dynamics CRM产生公共签名,避免每次插件换环境重新输入签名密钥账号密码

    在Dynamcs CRM项目维护交接过程中,我们经常会使用其他合作者的插件代码.但是每次拿到别人代码编译的时候插件密钥都要重新输入密钥的账号密码.而且如果密钥都是的话比较麻烦.所以这里就针对这个问题做 ...