0%

PE结构-导入表(2)

首先,PE文件中的数据被载入内存后根据不同页面属性被划分成很多区块(节),并有区块表(节表)的数据来描述这些区块,这里我们需要注意一点:一个区块中的数据仅仅知识由于属性相同而放一起,并不一定是同一种用途的内容。例如输入表、输出表等就有可能和只读常量一起被放在同一个区块中。因为他们的属性都是可读不可写的。

其次,由于不同用途的数据有可能被放入同一个区块中,因此仅仅依靠区块表是无法确定和定位的。因此还需要通过PE文件头中的IMAGE_OPTIONAL_READER32结构的数据目录表来指出他们的位置,我们可以由数据目录表来定位他们的位置,我们可以由数据目录表来定位的数据包括输入表、输出表、资源、重定位表和TLS等15种数据。

导入表

导入表的作用

当程序运行时,需要多个PE文件共同组成

PE文件提供哪些功能给其他PE文件是导出表的作用

PE文件需要依赖的模块以及依赖模块中的哪些函数是导出表的作用

什么是导出表

导出表是用于记录该PE文件还需要依赖的模块以及依赖这些模块中的那些函数的一种结构

如何定位导入表

定位导入表的原理

根据之前所学可知,导入、导出等表的起始位置和大小都存放在了IMAGE_OPTIONAL_HEADERS结构的DataDirectory数组当中。而导入表对应的下标为1

宏定义 含义
IMAGE_DIRECTORY_ENTRY_IMPORT 1 导入表

定位导入表流程

  1. 找到扩展PE头IMAGE_OPTIONAL_HEADERS的最后一个成员DataDirectory[1]
  2. 根据DataDirectory[1].VirtualAddress 得到导入表的RVA
  3. 将导入表的RVA转为FOA,在文件中定位到导入表

根据流程定位导入表

分析demo

使用everEdit.exe

找到DataDirectory[1]

image-20211015152622826

根据索引,我们找到DataDirectory[1]即第二个表导出表的数据目录

iMAGE_data_directory成员 说明
VirtualAddress 0x001CF47C 导出表的RVA地址
Size 0x00000140 导出表的大小

得到导出表的RVA

导出表的RVA即0x001CF47C

修改VA转FOA程序代码,求出导出表FOA

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
#include <math.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664

//VA转FOA 32位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa32(UINT va, _IMAGE_DOS_HEADER *dos,_IMAGE_NT_HEADERS* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew+sizeof(_IMAGE_NT_HEADERS);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;

if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
UINT offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
UINT foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}

}

}

//VA转FOA 64位
//第一个参数为要转换的在内存中的地址:VA
//第二个参数为指向dos头的指针
//第三个参数为指向nt头的指针
//第四个参数为存储指向节指针的数组
UINT VaToFoa64(UINT va, _IMAGE_DOS_HEADER* dos, _IMAGE_NT_HEADERS64* nt, _IMAGE_SECTION_HEADER** sectionArr) {
//得到RVA的值:RVA = VA - ImageBase
UINT rva = va - nt->OptionalHeader.ImageBase;
//输出rva
printf("rva:%X\n", rva);
//找到PE文件头后的地址 = PE文件头首地址+PE文件头大小
UINT PeEnd = (UINT)dos->e_lfanew + sizeof(_IMAGE_NT_HEADERS64);
//输出PeEnd
printf("PeEnd:%X\n", PeEnd);
//判断rva是否位于PE文件头中
if (rva < PeEnd) {
//如果rva位于PE文件头中,则foa==rva,直接返回rva即可
printf("foa:%X\n", rva);
return rva;
}
else {
//如果rva在PE文件头外
//判断rva属于哪个节
int i;
for (i = 0; i < nt->FileHeader.NumberOfSections; i++) {
//计算内存对齐后节的大小
UINT SizeInMemory = ceil((double)max((UINT)sectionArr[i]->Misc.VirtualSize ,(UINT)sectionArr[i]->SizeOfRawData ) / (double)nt->OptionalHeader.SectionAlignment)* nt->OptionalHeader.SectionAlignment;

if (rva >= sectionArr[i]->VirtualAddress && rva < (sectionArr[i]->VirtualAddress + SizeInMemory)) {
//找到所属的节
//输出内存对齐后的节的大小
printf("SizeInMemory:%X\n", SizeInMemory);
VaToFoa32(nt->OptionalHeader.ImageBase +0x1CF47C,dos,(_IMAGE_NT_HEADERS*)nt,sectionArr);
break;
}
}
if (i >= nt->FileHeader.NumberOfSections) {
//未找到
printf("没有找到匹配的节\n");
return -1;
}
else {
//计算差值= RVA - 节.VirtualAddress
int offset = rva - sectionArr[i]->VirtualAddress;
//FOA = 节.PointerToRawData + 差值
int foa = sectionArr[i]->PointerToRawData + offset;
printf("foa:%X\n", foa);
return foa;
}

}

}
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\86156\\Desktop\\everEdit.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while(cnt< nt->FileHeader.NumberOfSections){
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER)*cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

VaToFoa32(nt->OptionalHeader.ImageBase +0x11320,dos,nt,sectionArr);

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
break;
}

default:
{
printf("error!\n");
break;
}

}
getchar();
return 0;
}

此代码本是将VA转为FOA值,但由于VA = ImageBase + RVA,我们可以修改增加一句关键代码使其RVA转FOA

1
VaToFoa32(nt->OptionalHeader.ImageBase +0x1CF47C,dos,nt,sectionArr);

image-20211015153401402

拿到导出表文件偏移FOA:0x10720

完成定位

导入表结构

导入表个数

与导出表不同,导入表通常需要包含多个模块,而不像导出表只需要提供PE文件需要提供的导出函数即可

因此,导出表只有一个,但导入表可能会有多个

当程序运行时,需要依赖几个模块,就有对应几个导出表

导出表的结构体

在C语言中,导出表的结构如下(在winnt.h中有定义)

image-20211015154327762

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
成员 数据宽度 说明
Characteristics DWORD 标志为0表示结束,没有导入描述符了
OriginalFirstThunk DWORD RVA指向IMAGE_THUNK_DATA结构数组 (桥1)
TimeDateStamp DWORD 时间戳
ForwarderChain DWORD 链表的前一个结构
Name DWORD RVA,指向DLL名字,该名字以‘\0’为结尾
FirstThunk DWORD RVA指向IMAGE_THUNK_DATA结构数组 (桥2)

Characteristics

标志 为0表示结束 没有导入描述符了

IMAGE_THUNK_DATA

在介绍OriginalFirstThunk之前,要先了解一下OriginalFirstThunk和FirstThunk所指向的结构数组

image-20211015164516486

指向的数组中每一项为一个结构,此结构名称是IMAGE_THUNK_DATA

数组最后以一个内容全为0的IMAGE_THUNK_DATA作为结束

IMAGE_THUNK_DATA实际上只是一个DWORD,但在不同的时刻却拥有不同的解释

IMAGE_THUNK_DATA有两种解释

  • DWORD最高位为0,那么该数值是一个RVA,指向_IMAGE__IMPORT_BY_NAME结构,表明函数是字符串类型的函数名导入的
  • DWORD最高位为1,那么该数值的低31位就是函数的导出函数的序号

_IMAGE_IMPORT_BY_NAME结构:

1
2
3
4
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

该结构即为:”编号—名称”(Hint/Name)描述部分

  • Hint:导出函数地址表的索引编号,可能为空且不一定准确,由编译器决定,一般不使用该值
  • Name:这个是一个以”\0”结尾的字符串,表示函数名

此时可发现,IMAGE_THUNK_DATA最终提供的数据只有两个:

  • DWORD最高位为0时:需要导入函数的名称(Hint不一定准确,所以不使用)
  • DWORD最高位为1时:需要导入的函数在导出表中的序号

此处对应了导出表笔记中的由导出表获得导出函数所需的两种方法

  1. 根据函数名称获取导出函数地址
  2. 根据函数序号获取导出函数地址

OriginalFirstThunk

因为它是指向另外数据结构的通路,因此简称为桥1。该字段指向一个包含了一系列结构的数组:IMAGE_THUNK_DATA

桥1所指向的地址列表被定义为:INT(Import Name Table) 导入名称表

导入表的双桥结构

桥1 与 桥 2 最终的目的地都是一致的,都指向了引入函数的“编号-名称”(Hint/Name)描述部分

桥1到IMAGE_THUNK_DATA的过程中,经过了:INT(Import Name Table) 导入名称表

而桥2到IMAGE_THUNK_DATA的过程中,经过了: IAT(Import Address Table)导入地址表

PE文件加载前

image-20211015193909496

image-20211015194546419

PE文件载入后

image-20211015194834185

image-20211015194759949

加载前后对比

  • 在PE文件加载前: 桥1指向INT和桥2指向的IAT的数据是相同的,但是其存储的位置是不同的
  • 在PE文件加载后:桥1指向的INT不变,但桥2指向的IAT的数值变成了函数相应的RVA地址

另:函数相应的RVA地址是根据IAT中的函数名称或者导入表中的序号获得的。

根据结构分析导入表

回到先前得到的导入表的FOA,在16进制编辑器中跳转至FOA:0x1CDA7C位置