Last updated on October 7, 2023 pm
IAT表、INT表、导入表
回顾
导出表是告诉其他PE文件可以如何找到自己提供的函数,它分为名字导出、序号导出,又各自对应几张表,分别是函数地址表AddressOfFunctions、函数序号表AddressOfNameOrdinals、函数名称表AddressOfNames。
重定位表里面则是记录所有需要修改地址的地址,也就是告诉操作系统哪里有地址需要修改。当一个PE文件无法按照预计ImageBase装载时发挥作用
导入表
一个PE文件有导出表,自然就有导入表。前者是告诉别人如何用,后者就相当于告诉别人自己要用什么。它可以通过OptionalHeader数据目录项的第二项找到
同理这个VirtualAddress也是一个RVA,需要转化为FOA后才能在文件中找到真正的导入表结构
1 2 3 4 5 6 7 8 9 10 11
| typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
|
其中OrginalFirstThunk
和FirstThunk
各自再指向两张表,分别是INT表和IAT表,name
则是要用到的dll的名字。在进一步解析IAT表和INT表之前,先做个实验。
随便使用调试器打开一个PE文件,在里面找一找一些使用了系统调用的函数
比如这里,实际上这条指令就是mov rax ds:[4083b0]
,在内存中转去4083B0这个地址,看看是什么
也就是说4083B0这个地址存放的是7FFFE86C3190,借助于调试器很方便的知道这个地址就是MessageBoxA的函数地址,这条指令也就等价于把MessageBoxa的地址给rax,然后再调用此函数。
而我们在文件中去看看4083B0这个地址存放的是什么呢(这里要减去ImageBase得到83B0,同时RVA转换FOA,才能在文件中找到,此地址在文件中对应的地址为37B0
37B0这个地址存放的则是一个86F6,很显然与运行时存放的值不一样。继续深究86F6又是什么,首先这是一个RVA,将其转换为FOA后,得到3AF6
当在文件中转去3AF6时,就会意料中地发现居然就是MessageBoxA这个函数名字,虽然前面还带了E9 01两字节不知道什么东西。
也就是说在内存中4083B0存放的是MessageBoxA的地址,而文件中则对应存放的是MessageBoxA的名字的RVA。换句话说存放的东西不一样,但两者之间肯定有一定的联系。实际上,3AF6处就是IAT表中的一项。
目前可以明确的是IAT表运行前运行后确实会发生变化,这其实就跟PE文件加载过程有关了。
在PE文件加载之前,INT表和IAT表指向同一块地方,换句话说INT表和IAT表在运行前是完全一样的但是独立的两张表,在多数情况下它们处于不同的地址处,但是存放相同的值。在PE文件加载之后,操作系统就会根据INT表去找那些函数地址,比如MesaageBoxA的地址,然后填入IAT表中。
这里不妨细说一下exe文件加载过程
- 首先操作系统给exe文件分配4GB的虚拟空间,然后把exe本身的代码加载到ImageBase处,通常来说exe是首先加载的,往往都能预计加载到ImageBase处,因此不需要使用重定位表修改。
- 其次,操作系统往4GB空间中加载各种DLL文件,由于DLL文件往往会有冲突,不能按照预计ImageBase装载,因此需要使用DLL本身提供的重定位表修复自己的地址
- 待所有DLL都加载并修复完后,操作系统根据exe的INT表去找函数地址,找到一个地址就填入IAT表中,遍历完INT表也就遍历完IAT表,由此完成对IAT表的修改(这个找函数地址的过程,其实就是调用GetPrcoAddress这个函数,我们在此之前实现了差不多的)
- 此时INT表存储了函数名称或者序号,IAT表存储了对应的函数地址,想要使用DLL一个函数也就简单了。
也就是说文件加载前后INT表不变,文件加载前IAT表和INT表相同,文件加载后IAT表发生变化
那么INT表是如何存储函数名称或者序号呢?
先来看看INT表表项结构
1 2 3 4 5 6 7 8
| typedef struct _IMAGE_THUNK_DATA32 { union { PBYTE ForwarderString; PDWORD Function; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; } u1; } IMAGE_THUNK_DATA32;
|
注意这是个联合体,也就是说这个结构宽度是4个字节,虽然看起来这个联合体存放了很多东西。但又最简单的方法判断,如果这4个字节最高位是1,那么剩下31位就代表函数序号;如果最高位不为1,那么剩下31为就代表一个RVA指向另一个结构IMAGE_IMPORT_BY_NAME
,即if(INT & 0x80000000 == 0x80000000) :序号导出 else RVA = IMAGE_IMPORT_BY_NAME
这个IMAGE_IMPORT_BY_NAME结构也简单
1 2 3 4
| typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME
|
第二个成员就指向名字的第一个字节。
IAT表和INT表在加载前是完全一样的,其结构也是一样的,它们的成员都会指向相同的序号或者IMAGE_IMPORT_BY_NAME结构
代码遍历
NOTE:我使用的PE文件是64位的,在处理上会略有所不同。会在代码中标出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
| #include <windows.h> #include <stdio.h>
int Size(FILE* fp){ int size = 0; fseek(fp,0,SEEK_END); size = ftell(fp); fseek(fp,0,SEEK_SET); return size; }
DWORD RVA_TO_FOA(DWORD RVA,char* Buffer){ PIMAGE_DOS_HEADER DosHeader = NULL; PIMAGE_NT_HEADERS NTHeader = NULL; PIMAGE_FILE_HEADER FileHeader = NULL; PIMAGE_OPTIONAL_HEADER OptionalHeader = NULL; PIMAGE_SECTION_HEADER SectionHeader = NULL;
DosHeader = (PIMAGE_DOS_HEADER)Buffer; FileHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4); OptionalHeader = (PIMAGE_OPTIONAL_HEADER)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER); SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FileHeader->SizeOfOptionalHeader); if (RVA < OptionalHeader->SizeOfHeaders) { return RVA; }
for(int i=0;i<FileHeader->NumberOfSections-1;i++){ if(RVA >= SectionHeader->VirtualAddress && RVA < (SectionHeader+1)->VirtualAddress){ DWORD offset = RVA - SectionHeader->VirtualAddress; return SectionHeader->PointerToRawData + offset; } else{ SectionHeader +=1; } }
DWORD offset = RVA - SectionHeader->VirtualAddress; return SectionHeader->PointerToRawData + offset; }
void Import_INFO(char* Buffer){ PIMAGE_DOS_HEADER DosHeader = NULL; PIMAGE_NT_HEADERS NTHeader = NULL; PIMAGE_FILE_HEADER FILEHeader = NULL; PIMAGE_OPTIONAL_HEADER64 OptionalHeader = NULL; PIMAGE_SECTION_HEADER SectionHeader = NULL;
DosHeader = (PIMAGE_DOS_HEADER)Buffer; FILEHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4); OptionalHeader = (PIMAGE_OPTIONAL_HEADER64)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER); SectionHeader = (PIMAGE_SECTION_HEADER)(Buffer + DosHeader->e_lfanew + 4 + IMAGE_SIZEOF_FILE_HEADER + FILEHeader->SizeOfOptionalHeader);
PIMAGE_IMPORT_BY_NAME ImportByName=NULL; PIMAGE_IMPORT_DESCRIPTOR Import_Table = (PIMAGE_IMPORT_DESCRIPTOR)(Buffer + RVA_TO_FOA(OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress,Buffer)); char ZERO[sizeof(IMAGE_IMPORT_DESCRIPTOR)] = { 0 };
for(int i = 1; memcmp(ZERO,Import_Table,sizeof(IMAGE_IMPORT_DESCRIPTOR));i++){ printf("----------第 %d 个导入表----------\n",i); printf("Time Date Stamp:%X\n",Import_Table->TimeDateStamp); printf("Dll name:%s\n",(Buffer + RVA_TO_FOA(Import_Table->Name,Buffer))); printf("INT RVA:%X\n",Import_Table->OriginalFirstThunk); printf("IAT RVA:%X\n",Import_Table->FirstThunk);
printf("******INT TABLE******\n"); long long* INT_Address = (long long*)(Buffer + RVA_TO_FOA(Import_Table->OriginalFirstThunk,Buffer));
for(int j = 1; *INT_Address ;j++){ if(((*INT_Address) & 0x8000000000000000)){ printf("第%d个函数序号为:%d\n",j,(*INT_Address) & 0x7fffffffffffffff); } else{ ImportByName = (PIMAGE_IMPORT_BY_NAME)(Buffer + RVA_TO_FOA(*(INT_Address),Buffer)); printf("第%d个函数名字为:%s\n",j,ImportByName->Name); printf("INT_ADDRESS:%X \n",*INT_Address); } INT_Address++; }
printf("******IAT TABLE******\n"); long long* IAT_Address = (long long*)(Buffer + RVA_TO_FOA(Import_Table->FirstThunk,Buffer)); for(int j = 1; *IAT_Address ;j++){ if((*IAT_Address) & 0x8000000000000000){ printf("第%d个函数序号为:%d\n",j,(*IAT_Address) & 0x7fffffffffffffff); } else{ ImportByName = (PIMAGE_IMPORT_BY_NAME)(Buffer + RVA_TO_FOA(*IAT_Address,Buffer)); printf("第%d个函数名字为:%s\n",j,ImportByName->Name); printf("IAT_ADDRESS:%X \n",*IAT_Address); } IAT_Address++; }
Import_Table++; printf("---------------------------------\n"); } }
int main(){ char* buffer; char* new_buffer;
FILE* fp1 = NULL; errno_t err_1 = fopen_s(&fp1, "C:\\Users\\yongrin\\Desktop\\Func.dll", "rb");
int size = Size(fp1); buffer = (char*)malloc(size); fread(buffer,size,1,fp1);
Import_INFO(buffer);
return 0; }
|
总结
PE加载前,INT表和IAT表存放了相同的值指向同一处地方,或为函数序号、或为函数名称。
PE加载后,操作系统根据INT表找到各个DLL中每个函数的具体地址,将其填入IAT中。