调用dll文件函数的原理
程序在调用dll文件函数的时候,并不是把函数编译到当前程序中,
而是把函数的地址保存到了当前文件中
在文件当中,对应的函数地址部分存放的是函数名称
一个进程空间的exe dll文件如何被加载到内存
1 LoadLibraryA显示加载DLL文件,OS把exe调用到内存中后根据exe需要调用的dll文件,再把dll调用进内存中。
HMOULE 等于的是dll文件的IMAGEBASE,也就是首地址
2 GetProcessAddr(HMOUDLE,fun1); 就拿到dll中对应的函数地址
exe文件调用的动态链接库在内存和在硬盘中的区别
函数地址的不同,加载到内存中是一个具体的函数地址,而在硬盘中是一个函数名称或者序号,通过函数名称加载得到函数地址。
导入表
导入表和导出表所属位置相同,都是可选PE头的数据目录表里面的,但是是第二个元素
但是这个东西其实是一个数组,因为导入表和输出表不一样,有导入几个dll文件,就有几个导入表。在结束的时候有一个跟该结构体一样大小的为0的结构体作为判断。
导入表结构体
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)
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,两者对应结构体一样,只是用法和名称不同。
区分导入函数如何导入
导入函数可以通过序号导入和函数名称导入,通过区分_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指向的是同一个内存地址:
在PE文件被加载到内存中后,IAT表被替换为函数的入口地址:
代码解析导入表
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++; } }