PE文件解析-导入表

调用dll文件函数的原理

程序在调用dll文件函数的时候,并不是把函数编译到当前程序中,

而是把函数的地址保存到了当前文件中

在文件当中,对应的函数地址部分存放的是函数名称

一个进程空间的exe dll文件如何被加载到内存

1 LoadLibraryA显示加载DLL文件,OS把exe调用到内存中后根据exe需要调用的dll文件,再把dll调用进内存中。

HMOULE 等于的是dll文件的IMAGEBASE,也就是首地址

2 GetProcessAddr(HMOUDLE,fun1); 就拿到dll中对应的函数地址

exe文件调用的动态链接库在内存和在硬盘中的区别

函数地址的不同,加载到内存中是一个具体的函数地址,而在硬盘中是一个函数名称或者序号,通过函数名称加载得到函数地址。

导入表

导入表和导出表所属位置相同,都是可选PE头的数据目录表里面的,但是是第二个元素

img

 

 

但是这个东西其实是一个数组,因为导入表和输出表不一样,有导入几个dll文件,就有几个导入表。在结束的时候有一个跟该结构体一样大小的为0的结构体作为判断。

img

 

 

 

导入表结构体

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)
​
    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

  

 

结构体重点

OriginalFirstThunk

是导入表结构体的第一个字段,是一个共用体,该字段的第二个字段的意思是指向一个导入名称表(INT Import Name Table)

img

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

  

这个结构体只有四个字节,在不同的情况下表示是意义不一样。

DWORD TimeDateStamp

名字看着是一个时间戳,但是如果该字段为0,FirstThunk也就是IAT和INT执行的是一个表。

当为-1的时候,IAT的表存放的是真实的地址。

DWORD Name

指向dll文件名称的RVA

FirstThunk 导入地址表(IAT)的

导入名称表叫 INT(Import Name Table),导入地址表叫IAT,两者对应结构体一样,只是用法和名称不同。

img

 

区分导入函数如何导入

导入函数可以通过序号导入和函数名称导入,通过区分_IMAGE_THUNK_DATA32结构体中的Ordinal来判断,如果二进制位的最高位是1就按照序号导入,如果不是1就是按照函数名称导入。

如果按照序号导入,则_IMAGE_THUNK_DATA32的低31位则为序号值。

如果按照函数名称导入则_IMAGE_THUNK_DATA32结构体AddressOfData字段指向一个结构体IMAGE_IMPORT_BY_NAME:

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;   //导入函数序号,该值可以为0
    CHAR   Name[1];//存放导入函数的名称
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

  

  

 

 

 

INT和IAT的关系以及作用:

在PE没有被加载到内存时,IAT和INT指向的是同一个内存地址:

img

在PE文件被加载到内存中后,IAT表被替换为函数的入口地址:

img

代码解析导入表

首先拿到可选PE头的数据目录表的第二个元素的内容。然后依据该元素指向的内容来遍历每一个导入表,再根据导入表里面的字段来遍历

void GetImportTable()
{
    //拿到可选PE头的数据目录表的第二个元素的内容
    IMAGE_DATA_DIRECTORY directory = pOptionalHeader->DataDirectory[1];
    //拿到第一个导入表
    PIMAGE_IMPORT_DESCRIPTOR ImportTable = PIMAGE_IMPORT_DESCRIPTOR(RvaToFoa(directory.VirtualAddress) + FileBuff);
​
    //循环遍历每一个导入表
    while (ImportTable->Name)
    {
        //输出该导入表的文件的名字
        char* pName = RvaToFoa(ImportTable->Name) + FileBuff;
        std::cout << pName << std::endl;
        //输出导入表的函数名称表
        PIMAGE_THUNK_DATA INT = PIMAGE_THUNK_DATA(RvaToFoa(ImportTable->OriginalFirstThunk) + FileBuff);
        //导入表地址
        PIMAGE_THUNK_DATA IAT = PIMAGE_THUNK_DATA(RvaToFoa(ImportTable->FirstThunk) + FileBuff);
        PIMAGE_IMPORT_BY_NAME temp = { 0 };
        while (INT->u1.AddressOfData)//当遍历到的是最后一个是时候是会为0,所以随便遍历一个就好
        {
            if (INT->u1.Ordinal & 0x80000000)//利用二进制首位第一个是否为1来判断是不是按照序号导入
            {
                printf("按序号导入时函数序号为%d\n", INT->u1.Ordinal & 0x7FFFFFFF);
            }
            else  //如果 不为1表示按照名称导入
            {
                temp = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(INT->u1.AddressOfData) + FileBuff);
                if (temp->Hint == NULL)
                    printf("函数名称为:%s,该函数没有序号\n", temp->Name);
                printf("函数序号和名称:%x,%s\n", temp->Hint, temp->Name);
​
            }
            INT++;//INT在INT数组中下移
        }
        ImportTable++;
    }
}