PE文件结构(十)

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; // 指向 导入名称表INT(ImportNameTable)的RVA
} DUMMYUNIONNAME;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // 指向 DLL名称的地址 RVA
DWORD FirstThunk; // 指向 导入地址表IAT(ImportAddressTable)的RVA
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

其中OrginalFirstThunkFirstThunk各自再指向两张表,分别是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;//指向IMAGE_IMPORT_BY_NAME
} 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]; //函数名称,以0结尾
} 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;
//如果是查看32位的PE文件 这里改为PIMAGE_OPTIONAL_HEADER6432
PIMAGE_OPTIONAL_HEADER64 OptionalHeader = NULL;
PIMAGE_SECTION_HEADER SectionHeader = NULL;


DosHeader = (PIMAGE_DOS_HEADER)Buffer;
FILEHeader = (PIMAGE_FILE_HEADER)(Buffer + DosHeader->e_lfanew + 4);
//如果是查看32位的PE文件 这里改为PIMAGE_OPTIONAL_HEADER6432
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");
//如果是查看32位的PE文件 这里改为DWORD*
long long* INT_Address = (long long*)(Buffer + RVA_TO_FOA(Import_Table->OriginalFirstThunk,Buffer));

for(int j = 1; *INT_Address ;j++){
//最高位是1 则是序号导出 ////如果是查看32位的PE文件 这里改为0x80000000
if(((*INT_Address) & 0x8000000000000000)){ // 这里改为0x7fffffff
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");
//如果是查看32位的PE文件 这里改为DWORD*
long long* IAT_Address = (long long*)(Buffer + RVA_TO_FOA(Import_Table->FirstThunk,Buffer));
for(int j = 1; *IAT_Address ;j++){
//最高位是1 则是序号导出 这里改为0x80000000
if((*IAT_Address) & 0x8000000000000000){ // 这里改为0x7fffffff
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中。


PE文件结构(十)
http://example.com/2023/07/15/PE_9/
Author
yring
Posted on
July 15, 2023
Licensed under