逆向工程——PE(二)

 在(一)中我们主要讨论了什么是PE以及PE头的组成结构,由此我们继续深入学习PE头的核心内容——IAT(IMPORT  Address  Table)和EAT(EXPORT Address Table)

  在进行Windows程序编程时,我们会使用到windows的各种API,那么在C语言里有类似的include语句,而在实现方面,操作系统是并不认识include语句的,其次,Windows使用了较为庞大的库函数来支持环境,因此运行多个进程,每个程序运行时都包含相同的库,会造成严重的内存浪费。这个时候,操作系统就使用一种手段,称为动态链接库(Dynamic Link Library,简称DLL),DLL的功能很强大,它是用来连接程序与库之间的联系,像桥梁的关系。在PE头(程序的PE头)里有一个IAT表,里面记录了程序正在使用哪些库中的哪些函数,而在DLL的PE头中则存在一张EAT表,它可以准确地求得从相应库中导出函数的起始地址。

  使用DLL的好处主要有以下三点:

    1. 不需要把整个库加载进内存,需要什么用什么就行了。
    2. 进行内存映射以后,DLL文件可以被多个进程共享,不会造成资源的浪费。
    3. 更新库函数,只需要修改DLL里对应的地址即可。
  • IAT

      IAT的结构体为IMAGE_IMPORT_DESCRIPTOR,是PE头中可选项IMAGE_OPTIONAL_HEADER32里的DataDirectory[1],起始地址为IMAGE_OPTIONAL_HEADER32.DataDirectory[1].VirtualAddress(是RVA值,图中为1F4AC),Size为230。

 1 typedef struct _IMAGE_IMPORT_DESCRIPTOR {
 2     union {
 3         DWORD   Characteristics;            // 0 for terminating null import descriptor
 4         DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
 5     };
 6     DWORD   TimeDateStamp;                  // 0 if not bound,
 7                                             // -1 if bound, and real date\time stamp
 8                                             //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
 9                                             // O.W. date/time stamp of DLL bound to (Old BIND)
10 
11     DWORD   ForwarderChain;                 // -1 if no forwarders
12     DWORD   Name;
13     DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
14 } IMAGE_IMPORT_DESCRIPTOR;
15 typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
16 
17 typedef struct _IMAGE_IMPORT_BY_NAME {
18     WORD    Hint;
19     BYTE    Name[1];
20 } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

  执行一个文件通常需要导入多个库,导入多少库就有多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体组成数组,结构体数组最后以NULL结束。

       由上图的RVA地址(1F4AC)----->文件偏移地址为1C0AC(1F4AC - 所在节区的VirtualAddress(1F000) + PointerToRawData(1BC00) ),下图灰色选中区域即为第一个结构体。

  Address     values          tips
1
01C0AC 0001F6DC OriginalFirstThunk(INT) 2 01C0B0 00000000 TimeDateStamp 3 01C0B4 00000000 ForwarderChain 4 01C0B8 0001FC7E Name 5 01C0BC 0001F000 FirstThunk(IAT)
    • Name

      Name是一个字符串指针,指向导入函数所属的库文件名称。(RVA 1FC7E  --->  FOA 1C87E)

    • OriginalFirstThunk(INT)

      INT的各个元素的值为IMAGE_IMPORT_BY_NAME结构体指针,是由IMAGE_IMPORT_BY_NAME数组构成的表,每4个字节存放着对应的函数名称和Hint。

01C2DC        0001FB84        IMAGE_IMPORT_BY_NAME
...... ...... ...... ......

      我们查看IMAGE_IMPORT_BY_NAME(数组中第一个元素)(RVA  1FB84  --->  FOA  1C784)。下图就是存放函数名字的空间。前2个字节为Hint编号,也称为Ordinal,是库中函数的编号,后面一长串是函数的名字。

01C784        0215               Hint
01C786        4F70656E50......        Name
    • FirstThunk(IAT)

        IAT数组中存放的每一个元素中的值是函数的地址(RVA  1F000  --->  FOA  1BC00)。有时IAT也拥有和INT一样的值,如下图。(1FB84转换为FOA与IAT的值一样)

      IAT的输入(装载)顺序:

      1. 读取IMAGE_IMPORT_DESCRIPTOR结构体中的name成员,获取库名称字符串。
      2. 装载相对应的库(LoadLibrary("库名称字符串"))
      3. 读取IMAGE_IMPORT_DESCRIPTOR的OriginalFirstThunk成员,获取INT入口地址。
      4. 再逐一读取INT中数组的值,获取相对应IMAGE_IMPORT_BY_NAME地址(RVA地址)。
      5. 使用IMAGE_IMPORT_BY_NAME的Hint或者是Name成员,获取相应函数的起始地址。eg: GetProcAddress("GetCurrentThreadld")
      6. 读取IMAGE_IMPORT_DESCRIPTOR的FirstThunk(IAT)成员,获取IAT的地址。
      7. 最后将第5步求得的函数地址输入相应的IAT数组中。
      8. 重复以上4—7步骤,直到最后遇到NULL(NULL为结束)时结束。
  • EAT

    也有细心的读者发现了,在notepad.exe文件里的IMAGE_OPTIONAL_HEADER32.DataDirectory[0]的地址和大小都为0,也就是说,可执行文件只需要“保管好”自己的IAT表就可以了,而DLL文件则“保管好”自己的EAT值。以下是Kernel32.dll文件中的EAT值。

1 000168   00091020   RVA of EXPORT DIRECTORY 
2 00016C   0000D850   size of EXPORT DIRECTORY 
    • IMAGE_EXPORT_DIRECTORY结构体
 1 typedef struct _IMAGE_EXPORT_DIRECTORY {
 2     DWORD   Characteristics;
 3     DWORD   TimeDateStamp;
 4     WORD    MajorVersion;
 5     WORD    MinorVersion;
 6     DWORD   Name;
 7     DWORD   Base;
 8     DWORD   NumberOfFunctions;
 9     DWORD   NumberOfNames;
10     DWORD   AddressOfFunctions;     // RVA from base of image
11     DWORD   AddressOfNames;         // RVA from base of image
12     DWORD   AddressOfNameOrdinals;  // RVA from base of image
13 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
NumberOfFunctions             实际Export函数的个数
NumberOfNames                 Export函数中具名的函数个数
AddressOfFunctions            Export函数地址数组(数组元素个数 = NumberOfFunctions)
AddressOfNames                函数名称地址数组(数组元素个数 = NumberOfNames)
AddressOfNameOrdinals         Ordinal地址数组(数组元素个数 = NumberOfNames)

       由上图可知,结构体位置在73020(RVA 91020  ---> FOA 73020)

 1 073020        00000000        Characteristics
 2 073024        AE0A74BF        TimeDateStamp
 3 073028        0000            MajorVersion
 4 07302A        0000            MinorVersion
 5 07302C        00094E96        Name
 6 073030        00000001        Base
 7 073034        0000063B        NumerOfFuctions
 8 073038        0000063B        NumberOfNames
 9 07303C        00091048        AddressOfFunctions
10 073040        00092934        AddressOfNames
11 073044        00094220        AddressOfNameOrdinals
    • 函数名称数组(AddressOfNames) RVA 92934 ---> FOA 74934,以下图即为数组。每4个字节代表函数名称的RVA地址。

我们以94F02为例,(RVA  94F02  --->  FOA  76F02),以下深色选中区域即为函数名称。

我们再查找Ordinal数组。(RVA  94220  --->  FOA  76220),每2个字节代表索引号(Ordinal)。94F02处的函数名的索引号为0003。

最后我们再根据索引号,查出此函数的实际函数地址。通过AddressOfFunctions成员(RVA  91048  --->  FOA  73048 )的入口地址,再加上索引号产生的偏移量来确定函数地址。


                     AddressOfFunctions[Ordinal] = RVA  (ordinal = 3 , RVA = 94F1A)

                Kernel32.dll的ImageBase = 6B800000,因此,此函数的实际地址(VA)为6B894F1A(ImageBase + RVA)。

      以上演示的过程的原理其实就是模拟了GetProcAdress()API函数的操作原理:

        1. 利用AddressOfNames成员转到“函数名称数组”。
        2. “函数名称数组”中存储着字符串地址。通过比较函数(strcmp)字符串,来查找函数名称。找到以后产生一个索引值(name_index)
        3. 利用AddressOfNameOrdinals成员,转到ordinal数组。
        4. 在ordinal数组中通过name_index查找与之相对应的ordinal值:AddressOfNameOrdinals[index] = ordinal 
        5. 再利用AddressOfFunctions成员转到“函数地址数组”(EAT)
        6. 最后在“函数地址数组”中将求得的ordinal值用作数组索引,获得指定数组的起始地址。

  

猜你喜欢

转载自www.cnblogs.com/re-and-er/p/PE2.html