PE格式详解(八)

来源:互联网 发布:简易平面广告制作软件 编辑:程序博客网 时间:2024/05/18 19:21
 

         上次讲了输出区段,还是比较简单的,但输入区段内容就稍稍多了点,保持耐心阿,这是本系列关于PE格式部分的最后一讲啦!

         输入区段(Import Section),包含了所有从DLL中引用的函数的信息。与输出区段类似,这些信息是由几个数据结构描述的,其中最重要的是输入目录表(Import Directory)与输入地址表(Import Address Table),在一些应用程序中,还会有Bound_ImportDelay_ImportDelay_Import不那么重要,就忽略了,Bound_Import后面介绍。

         PE加载器的任务就是把DLL映射到进程的地址空间,并且找到每个函数的地址。

         DLL中函数的地址并不是总是固定不变的,而随着DLL版本更新变化,为了解决这个问题,PE引入了输入地址表(IAT),这样地址只要在DLLIAT里更新一次,其他地方(尤其客户方)由于是间接访问DLL函数代码,就不用发生改变。IAT其实就是一个指针表,每一个指针指向一个函数的地址。

         我们先来看一下输入目录表的结构:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

    _ANONYMOUS_UNION union {

        DWORD Characteristics;

        DWORD OriginalFirstThunk;

    } DUMMYUNIONNAME;

    DWORD TimeDateStamp;

    DWORD ForwarderChain;

    DWORD Name;

    DWORD FirstThunk;

} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

    一个输入目录表项对应一个DLL,如果我们的EXE引用了100DLL,就会有100IMAGE_IMPORT_DESCRIPTOR结构。PE文件中没有字段可以指明输入目录表一共有多少个项,但默认最后一个项的字段是全0

    如同输出目录表一样,你可以在data directory1个元素中得到输入表的地址与大小,我们testPE.exe的输入表起始虚拟地址是00 01 80 00,大小3C,单个输入表项大小是10字节,所以表示testPE.exe引用了3DLL

    下面逐个讲下每一个成员:

    Characteristics。原先是存放一系列标志用的,现在已经废弃。

    OriginalFirstThunk。由于是共用体,所以这个匿名共用体仅表示OriginalFirstThunk,用来指向IMAGE_THUNK_DATA结构数组,稍后会描述。

    TimeDateStamp。除了应用程序被绑定(下详),值为-1,否则总是0

    ForwarderChain。用于旧式风格的绑定,现在已经废弃。

    Name。指向ASCII字符串,是DLL的名字。

    FirstThunk也指向一个IMAGE_THUNK_DATA结构数组,其实OriginalFirstThunk那个数组的拷贝,当函数被绑定输入(Bound Import)后,它就被替换成函数的实际地址,这也就是为什么前面要有一个OriginalFirstThunk(原FirstThunk)的原因。

    自然,下一步我们就要关心下IMAGE_THUNK_DATA到底是个怎样的东西。

typedef struct _IMAGE_THUNK_DATA32 {

    union {

        DWORD ForwarderString;

        DWORD Function;

        DWORD Ordinal;

        DWORD AddressOfData;

    } u1;

} IMAGE_THUNK_DATA32,*PIMAGE_THUNK_DATA32;

    可以看到这个结构里只有一个是共用体成员,说明u1仅表示四个DWORD成员之一。

    ForwarderString没有任何意义,无视。

    在文件中,它要么表示函数的序号(Ordinal),且一般从8开始,要么表示IMAGE_IMPORT_BY_NAME(下详)结构的指针(AddressOfData)。当被载入到内存中后,它会被替换成输入函数的实际地址(Function),就是说由FirstThunk指向的IMAGE_THUNK_DATA32数组实际上就变成了输入地址表(IAT)

typedef struct _IMAGE_IMPORT_BY_NAME {

    WORD Hint;

    BYTE Name[1];

} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;

    Hint。就是该函数在DLL中输出地址表(EAT)中的索引号,目的是方便PE加载器查阅目标DLLEAT,所以名字就叫“暗示”(Hint)。此值可选,一些linker设置它为0

    Name。这个字段比较奇怪,尽管从C定义中可以看到,结构里实际只包含一个字节而已,但是Name这样的定义,却预示着这是一个字符串,也就是从字符串第二个字节开始就出现在了结构外面,直到遇到/0结束符。它既没有采用DWORD类型来指向一个字符串的首地址,也没有开辟固定的长度来存储字符串,这是比较不符合常识的地方。

    好,稍稍总结一下。一个DLL对应一个IMAGE_IMPORT_DESCRIPTOR,然后创建两个一模一样的IMAGE_THUNK_DATA32数组,第一个由OriginalFirstThunk指向,第二个由FirstThunk指向。由于一个函数对应一个IMAGE_THUNK_DATA32,所以该DLL有几个函数,两个IMAGE_THUNK_DATA32数组就都有几个元素,刚开始都分别指向同一个IMAGE_IMPORT_BY_NAME结构,且两个IMAGE_THUNK_DATA32结构数组都以一个null DWORD表示结束。

    我们常称FirstThunk指向的IMAGE_THUNK_DATA32结构数组为输入地址表(IAT),而把OriginalFirstThunk指向的数组称为输入名称表(Import Name Table)或输入查询表(Import Lookup Table)

    生成两个IMAGE_THUNK_DATA32结构数组的目的是比较显然的。因为FirstThunk指向的那个,在被加载时,PE加载器会以实际函数地址替代其中的元素,即FirstThunk指向的数组中的元素不再保存IMAGE_IMPORT_BY_NAME结构的地址,而是转而保存函数地址。但IMAGE_IMPORT_BY_NAME结构数组本身并没有被删除(并且始终只有一个该数组!),仍然由OriginalFirstThunk指向。这样万一要查找DLL的函数名称等信息,就可以去OriginalFIrstThunk指向的数组找到。

    看到这,你会发现输入表,或者说输入区段并不简单等同于输出地址表(IAT),查看Data Directory,你会发现第12个元素就是IAT,而并没有EAT。当然其实PE加载器并没有把它当作IAT指针,而只是把存储IAT的虚拟内存页面标记为可读可写而已,因为IAT是被载入到只读区域的,在载入时,先临时把页面设置为可读写,等输入表初始化完成后,再改回原先的受保护属性。

在调用DLL函数时,还涉及到一个性能优化问题。假设你要调用的函数所在的Thunk Data 的地址是00405030h,当前程序运行到0040100Ch

那么比较高效的方式是:

    0040 100C CALL DWORD PTR [00405030]

    比较弱智的方式的:

    0040 100C CALL [00402200]

   

    0040 2200 JMP DWORD PTR [00405030]

(注:这里我解释下加了DWORD PTR的区别,表示00405030指向的内存区块是DWORD类型,并且会“返回”区块的代表的值,这其实是一种间接寻址的方式)

    产生后一种的原因是,编译器本身是无法区别模块内的普通函数调用和位于别的模块的外部函数调用,因而产生同一类型代码 CALL [XXXXXXXX]

    对于普通函数,XXXXXXXX就是函数的执行地址,而对于DLL函数,XXXXXXXX处实际保存的是一个指针,这个指针指向的地方才是真正的函数执行地址。但CALL指令里的XXXXXXXX只能是实际代码的地址,因此用在DLL函数上就会出错,于是链接器就想到了这么个笨拙的办法,用一段中间跳转代码来替换,造成了空间和时间资源的双重浪费。

    解决办法是,通过加上_declspec(dllimport)修饰符,告诉编译器这个函数是在DLL中,那么编译器就会直接产生CALL DWORD PTR [XXXXXXXX]之类的代码。不加这个修饰符,你的程序就会充满着特定的JMP语句而变得臃肿。

    第(七)讲提到过,一些函数可能只能通过EAT索引号来查找,就是那些出现在EAT但不出现在ENTEOT中的函数。大家可能也注意到了,IMAGE_THUNK_DATA32中的Ordinal字段是DWORD类型的,但Ordinal属性本身只需要一个WORD即可,于是M$这样做,定义了一个常量IMAGE_ORDINAL_FLAG32 = 80000000h,如果一个函数的索引号是1234h,且是那种只能查EAT表调用的函数,那么他的Ordinal字段值就是80001234h,这样PE加载器就会很容易识别出来。

    绑定输入(Bound Imports)。当PE文件被加载到内存中时,加载器会先检查输入表然后把需要的DLL载入到地址空间中去。接着它会遍历FirstThunk指向的数组,并用输入函数的实际地址去替换每一个元素的内容,这个步骤可能会花一部分时间。但如果程序员(或者链接器)可以完全得知函数的地址,就可以直接把数组中的元素替换为地址,节省相当多的时间。这种方法就称为绑定(Binding)

    M$采用一个bind.exe的程序(VS2008的在Windows SDK 6.0A里面),可以把IMAGE_THUNK_DATA32结构的内容都静态替换成地址。这样在载入DLL时,加载器会先检查这些地址是否正确合法,比如DLL版本是否符合(很早前就提过了DLL版本更新导致IAT的产生),如不符合或者DLL需要被重定位,加载器就会去遍历OriginalFirstThunk指向的数组(也就是INT),去计算新的地址。所以尽管INTEXE是可选的,但是没有INT,就无法进行绑定。

    最后要提到的,也是最开始提到的,一个或许不是很重要的数据结构:绑定输入目录表(The Bound Import Directory)。它包含了可以让加载器判断绑定的地址是否合法的信息。描述它的数据结构是IMAGE_BOUND_IMPORT_DESCRIPTOR,目录表就是这种结构的数组,每一项都对应一个被绑定过的DLL。先看下这个结构:

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {

    DWORD TimeDateStamp;

    WORD OffsetModuleName;

    WORD NumberOfModuleForwarderRefs;

} IMAGE_BOUND_IMPORT_DESCRIPTOR,*PIMAGE_BOUND_IMPORT_DESCRIPTOR;

    TimeDateStamp。这个成员必须和要引用的DLL的文件头信息相吻合,否则就会加载器去手动计算新IAT,这种情况一般发生在DLL版本不同时或者DLL映像被重定位时。

    OffsetModuleName。包含了以第一个IMAGE_BOUND_IMPORT_DESCRIPTOR为基址,DLL名称字符串(ASCII且以null结束)的偏移(非RVA)。

    NumberOfModuleForwarderRefs。是紧接着本结构后的另一个IMAGE_BOUND_FORWARDER_REF结构数组的元素个数。

typedef struct _IMAGE_BOUND_FORWARDER_REF {

    DWORD TimeDateStamp;

    WORD OffsetModuleName;

    WORD Reserved;

} IMAGE_BOUND_FORWARDER_REF,*PIMAGE_BOUND_FORWARDER_REF;

    这个结构与IMAGE_BOUND_IMPORT_DESCRIPTOR好像一样嘛。。。噢就最后一个字是保留的。这个结构数组干什么用的?你一定已经注意到ModuleForwarder这个词了,还记得我们在输出表中讲到的,函数的输出转送么?就是一个函数自己不实现而是把调用请求转发给另一个DLL中的函数。这里的IMAGE_BOUND_FORWARDER_REF结构就是用来记录接受转发的另一个DLL的校验信息,如果这个DLL还有输出转送,那么在该DLL中也有IMAGE_BOUND_FORWARDER_REF结构描述第三个DLL的校验信息。

    本文完。

原创粉丝点击