最近听别人讲的我晕晕乎乎的,于是上网上百度下,感觉这篇还不错。  链接:http://www.blogfshare.com/pe-export.html

一、导入表简介

在编程中常常用到“导入函数”(Import functions),导入函数就是被程序调用但其执行代码又不在程序中的函数,这些函数的代码位于一个或者多个DLL中,在调用者程序中只保留一些函数信息,包括函数名及其驻留的DLL名等。

于磁盘上的PE 文件来说,它无法得知这些输入函数在内存中的地址,只有当PE 文件被装入内存后,Windows 加载器才将相关DLL 装入,并将调用输入函数的指令和函数实际所处的地址联系起来。这就是“动态链接”的概念。动态链接是通过PE 文件中定义的“导入表”来完成的,导入表中保存的正是函数名和其驻留的DLL 名等。

1.调用导入函数的指令

程序被执行的时候是怎样使用导入函数的呢?我们来对一个简单的弹出一个MessageBox的程序反汇编一把,看看调用导入函数的指令都是什么样子的.

灰常简单的一个小程序,如图双击程序只显示一个对话窗口,然后就结束~试验用小程序,我们尽量的将内部的结构删减,调试起来才方便些。我们这次体验的目的就是想靠所学的知识,试图来找到MessageBox 在内存中的地址。
注:MessageBox 是来自于USER32.DLL 动态链接库里的一个函数,我们通过对PE 文件的静态反编译分析来观察hello.exe 这个试验品是如何定位和调用MessageBox 这个在USER32.dll的函数的。
(MessageBox 有两个版本,一个是MessageBoxA 还有一个是MessageBoxW 分别代表ASCII码形式和UNICODE~)

需要反汇编的两句源码如下:

invoke  MessageBox,NULL,offset szText,offset szCaption,MB_OK

invoke  ExitProcess,NULL

我们直接对其反汇编:

反汇编后,对MessageBox和ExitProcess函数的调用变成了对0040101A和00401020地址的调用,但是这两个地址显然是位于程序自身模块而不是DLL模块中,实际上,这是由编译器在程序所有代码的后面自动加上的Jmp dword ptr[xxxxxxxx]类型的指令,这个指令时一个间接寻址的跳转指令,xxxxxxxx地址中存放的才是真正的导入函数的地址。在这个例子中,00402000地址处存放的就是ExitProcess函数的地址。

那在没有装载到内存前,PE文件中的00402000地址处的内容又是什么呢?我们来分析一下它的节表。

由于建议装入地址是00400000h,所以00402000地址实际上处于RVA为2000h的地方,再看看各个节的虚拟地址,可以发现2000h开始的地方位于.rdata节内,而这个节的Raw_偏移为600h,也就是00402000h的内容实际上对应于PE文件中偏移600h处的数据。

我们再来看看文件0600h处的内容是什么:

查看的结果是0002076h,这显然不是内存中的ExitProcess函数的地址。不过,我们将它作为RVA看会怎么样?RVA地址00002076h也处于.rdata节内,减去节的其实地址00002000h后得到这个RVA相对于节首的偏移是76h,也就是对应文件0676h开始的地方,接下来会惊奇地发现,0676h再过去两个字节的内容正是函数名字符串“ExitProcess”!

是不是感觉有点不对?

如果我告诉你,当PE文件被装载的时候,Windows装载器会根据xxxxxxxx处的RVA得到函数名,再根据函数名在内存中找到函数地址,并且用函数地址将xxxxxxx处的内容替换成真正的函数地址,那么所有的疑惑就迎刃而解了。接下来看看如何获取导入表的位置。

2.获取导入表的位置

导入表的位置和大小可以从PE文件中IMAGE_OPTIONAL_HEADER32结构的数据目录字段中获取。从IMAGE_OPTIONAL_DIRECTORY结构的VirtualAddress字段得到的是导入表的RVA值,如果在内存中查找导入表,那么将RVA值加上PE文件装入的基址就是实际的地址,如果在PE文件中查找导入表,那么需要使用上一篇中讲的将RVA转换成文件偏移的方法进行转换。

二、导入表的结构

1.PE文件中的导入表

导入表由一系列的IMAGE_IMPORT_DESCRIPTOR结构组成,结构的数量取决于程序要使用的DLL文件的数量,每一个结构对应一个DLL文件,在所有这些结构的最后,由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构作为结束。

IMAGE_IMPORT_DESCRIPTOR结构的定义:

IMAGE_IMPORT_DESCRIPTOR STRUCT 
union 
    Characteristics              DWORD   ? 
    OriginalFirstThunk        DWORD   ? 
ends 
TimeDateStamp                     DWORD   ? 
ForwarderChain                     DWORD   ? 
Name1                                    DWORD   ? 
FirstThunk                            DWORD   ?
IMAGE_IMPORT_DESCRIPTOR ENDS

①Name1

它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称,如:KERNEL32.DLL)。

②OriginalFirstThunk和FirstThunk

现在可以看成是相同的(现在),它们都指向一个包含一系列IMAGE_THUNK_DATA结构的数组,数组中的每个IMAGE_THUNK_DATA结构定义了一个导入函数的信息,数组最后以一个内容为0的IMAGE_THUNK_DATA结构作为结束。

一个IMAGE_THUNK_DATA结构实际上就是一个双字,之所以把它定义成结构,是因为它在不同时刻有不同的含义:

IMAGE_THUNK_DATA STRUC
union u1
ForwarderString       DWORD  ?        ; 指向一个转向者字符串的RVA
Function                      DWORD  ?        ; 被输入的函数的内存地址
Ordinal                       DWORD  ?        ; 被输入的API 的序数值
AddressOfData         DWORD  ?        ; 指向 IMAGE_IMPORT_BY_NAME
ends
IMAGE_THUNK_DATA ENDS

当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。(读者可以用预定义值IMAGE_ORDINAL_FLAG32或80000000h来对最高位进行测试)
当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一IMAGE_IMPORT_BY_NAME 结构。

IMAGE_IMPORT_BY_NAME STRUCT
Hint        WORD    ? 
Name1      BYTE      ?
IMAGE_IMPORT_BY_NAME ENDS

结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0,Name1字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。

我们来看一个例子:

2.内存中的导入表

为什么需要两个一样的IMAGE_THUNK_DATA 数组呢?

当PE文件被装入内存的时候,其中一个数组的值将被改作他用,正如上面分析的,Windows装载器会将指令Jmp dword ptr[xxxxxxxx]指定的xxxxxxxx处的RVA替换成真正的函数地址,其实xxxxxxx地址正是FirstThunk字段指向的那个数组的一员。

实际上,当PE文件被装入内存后,内存中的映象就被Windows装载器修正成了下图的样子,其中由FirstThunk字段指向的那个数组中的每个双字都被替换成了真正的函数入口地址,之所以在PE文件中使用两份IMAGE_THUNK_DATA 数组的拷贝并修改其中的一份,是为了最后还可以留下一份拷贝用来反过来查询地址所对应的导入函数名。

3.导入地址表(IAT)

暂把上面FirstThunk指向的真正导入函数地址数组称为导入地址数组。在PE文件中,所有DLL对应的导入地址数组是被排列在一起的,全部这些数组的组合也被称为导入地址表(Import Address Table),导入表中第一个IMAGE_IMPORT_DESCRIPTOR结构的FirstThunk字段指向的就是IAT的起始地址。也可以通过数据目录表的第13项找到IAT数据块的位置和大小。

《初识PE》导入表的更多相关文章

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

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

  2. 逆向-PE导入表

    导入表 动态链接库需要导入表 结构 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for ...

  3. PE导入表分析

    A.dll 导入 B.dll 导出函数 A.dll 表内容 这个结构指向的B导出函数的地址 Hook这个位置 等同于 Hook B.dll导出函数

  4. PE文件结构详解(五)延迟导入表

    PE文件结构详解(四)PE导入表讲 了一般的PE导入表,这次我们来看一下另外一种导入表:延迟导入(Delay Import).看名字就知道,这种导入机制导入其他DLL的时机比较“迟”,为什么要迟呢?因 ...

  5. 初识PE文件结构

    前言 目前网络上有关PE文件结构说明的文章太多了,自己的这篇文章只是单纯的记录自己对PE文件结构的学习.理解和总结. 基础概念 PE(Portable Executable:可移植的执行体)是Win3 ...

  6. 手动添加导入表修改EXE功能

    目标: 改动PE导入表,手工给HelloWorld增加一个功能,就是启动的时候写入一条开机启动项,C:\cmd0000000000000000000000000000.exe 实现方法: 直接在注册相 ...

  7. 利用模块加载回调函数修改PE导入表实现注入

    最近整理PE文件相关代码的时候,想到如果能在PE刚刚读进内存的时候再去修改内存PE镜像,那不是比直接对PE文件进行操作隐秘多了么? PE文件在运行时会根据导入表来进行dll库的"动态链接&q ...

  8. 小甲鱼PE详解之输入表(导入表)详解(PE详解07)

    捷径并不是把弯路改直了,而是帮你把岔道堵上! 走得弯路跟成长的速度是成正比的!不要害怕走上弯路,弯路会让你懂得更多,最终还是会在终点交汇! 岔路会将你引入万劫不复的深渊,并越走越深…… 在开始讲解输入 ...

  9. 利用PE数据目录的导入表获取函数名及其地址

    PE文件是以64字节的DOS文件头开始的(IMAGE_DOS_HEADER),接着是一段小DOS程序,然后是248字节的 NT文件头(IMAGE_NT_HEADERS),NT的文件头位置由IMAGE_ ...

随机推荐

  1. 关于angularjs依赖注入的整理

    初学angularjs阶段,刚刚看到菜鸟教程的angularjs依赖注入.现在整理一下: 1.含义:一个或更多的依赖(可以理解为模块关系依赖)或服务(分为内建服务[例如$http,$tiomeout等 ...

  2. java 枚举类型和数据二进制等问题思考

    .以下代码的输出结果是什么? int X=100; int Y=200; System.out.println("X+Y="+X+Y); System.out.println(X+ ...

  3. JavaBean的属性变量名前两个字母大小写问题

    Java属性命名规范! 一般情况下.Java的属性变量名都已小写字母开头,如:userName,showMessage等,但也存在着特殊情况,考虑到一些特定的有意思的英文缩略词如(USA,XML等), ...

  4. linux下安装php的oracle拓展

    最近要用Php远程连接第三方的oracle数据库,安装oracle拓展搞了好久,终于弄出来了,现在分享出来: 1,注意安装的客户端版本要和服务端的版本一致,不然会有异常 2,安装之前先要下载三个软件: ...

  5. win10下安装Django

    Django的核心(1.4+)可以运行在从2.5到2.7之间的任何Python版本. 我的电脑是操作系统是window10 ,内存是4G. 1.下载django 官网地址:https://www.dj ...

  6. CodeForces 702E Analysis of Pathes in Functional Graph

    倍增预处理. 先看一下这张图的结构,因为出度都是$1$,所以路径是唯一的,又因为每个点都有出度,所以必然有环,也就是一直可以走下去. 接下来我们需要记录一些值便于询问: 设$t[i][j]$表示从$i ...

  7. HDU 5821 Ball

    记录一下每个位置最终到达的位置.然后每次操作排序. #pragma comment(linker, "/STACK:1024000000,1024000000") #include ...

  8. 编写高质量iOS代码的52个有效方法2-1

    一.变量的定义位置(用{}声明示例变量或者用@property属性声明实例变量) 1.用{}声明示例变量: 此方法生命的实例变量,编译器在编译时,会自动计算其偏移量(表示该变量距离存放对象的内存区域的 ...

  9. 【Sort】多种排序

    这篇文章包含了插入排序,希尔排序,堆排序,归并排序和快速排序,是前几篇文章的集合. 一共包括三个文件 sort.h sort.cpp main.cpp 1.main.cpp #include < ...

  10. GLSL 纹理贴图

    #include <ork/render/FrameBuffer.h> #include <ork/scenegraph/SceneManager.h> #include &l ...