• Windows PE 第四章 导入表


    第四章 导入表

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

    4.1何为导入表

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

    4.2导入函数

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

    4.2.1  invoke指令分解

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

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

    书上的OD结果:

     

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

     

    VS2012 反汇编结果:

     

    以书上的为解释例子:

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

     

    从指令的反汇编代码中可以看出,以第一个调用为例,对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指令中的操作数0x004020080x402000都不在该导入表(黑体部分)的范围内,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,只要其中的字段Name10就已经满足结束条件了。导入表的每一组都是一个结构,成为导入表描述符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文件的名称,而这个名称是以””结尾的Ansi字符串。

    58.IMAGE_IMPORT_DESCRIPTOR.FirstThunk

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


    4.3.3  导入表的双桥结构

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

    下图为引入了ExitProcess3个函数的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,大小不确定。函数名字字符串的具体内容,以“”作为字符串结束标志。

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

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

    每一个结构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

  • 相关阅读:
    各种页的意义
    ecstore Fatal error: Class 'base_request' not found
    viewer.js 视图预览demo
    div在另一个div居中对齐
    文件权限解释rwx
    TPshop各个目录模块介绍
    tpshop linux安装下注意事项
    navicate 远程无法链接linux上mysql数据库问题
    关于破解邮箱的一点心得
    linux开启新端口
  • 原文地址:https://www.cnblogs.com/csnd/p/12062232.html
Copyright © 2020-2023  润新知