0%

PE结构-导出表(3)

导出表

导出表的作用

一个可执行程序是由多个PE文件构成的

利用EverEdit.exe为例,查看运行他所需的所有模块

使用od载入程序,利用e查看载入的模块(dll与exe程序,PE文件载入内存后可称作模块)

image-20211015112208833

我们可以看出,该程序除了载入自身exe程序外,还在外部动态链接了大量dll文件,这些dll为程序提供了运行所需的一些函数

1
<!--more-->

就比如MessageBoxA的弹窗函数就是由User32.dll模块提供

以上的每个模块都发挥着其作用,使得程序得以正常运行

一个程序引用哪些模块是由其导入表决定的

与导入表相对应的便是导出表,导出表则是决定当前PE文件能给其他PE文件提供的函数

拿User32.dll举例,它的导出表结构中,一定包含着MessageBoxA这个函数,否则它不可能提供这个函数给程序。


导入表与导出表概念总结:

  • 导出表:设置本PE文件可以将什么函数提供给其他PE文件
  • 导入表:该PE文件可以使用哪些PE文件。

个人理解,PE文件中的导入表像是C/C++中的#include 包含另一个文件来使用其中的函数,导出表便是其头文件,可使用的函数便是头文件中所声明的函数那般。

何为导出表?

导出表便是记录该PE文件提供给其他PE文件的函数的一种结构

定位导出表

定位导出表的原理

导出表的定位可以利用PE文件_IMAGE_OPTIONAL_HEADER32中的DataDirectory结构进行定位。

DataDirecoty是一个长为16的数组,每个数组成员对应一个表

1
2
3
成员										数据宽度

DataDirectory[16] IMAGE_DATA_DIRECTORY[16]=128字节

数据目录中共有16个表

根据表的结构可知,导出表的下标为0

即DataDirectory[0]表示导出表

image-20211014165215769

根据c语言中,该成员在扩展PE头中的定义

1
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

可以看到数组成员的结构为IMAGE_DATA_DIRECTORY

IMAGE_DATA_DIRECTORY

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORYAddressOfFunctions

这样的结构于扩展PE头内有15个

IMAGE_DATA_DIRECTORY成员 数据宽度 说明
VirtualAddress DWORD(4字节) 表的起始位置(RVA)
Size DWORD(4字节) 表的大小

VirtualAddress 意为一个相对虚拟地址(RVA)

Size: 表的大小

根据VirtualAddress 与 Size 可求出该表的范围。

同时,IMAGE_DATA_DIRECTORY仅仅记录表的位置和大小,没有描述表的具体结构

流程

1.找到IMAGE_OPTIONAL_HEADER32的最后一个成员DataDirectory

2.获取DataDirectory[0]

3.根据DataDirectory[0].virtualAddress得到导出表的RVA

4.将导出表的RVA转换为FOA,利用16进制编辑器定位导出表

RVA = 区块.VirtualAddress + 差值

1
2
3
4
5
6
7
8
9
10
11
### RVA 转 FOA 计算方式

若RVA不在PE头内:

差值 = RVA - 区块.VirtualAddress

FOA = 区块.PointerToRawData + 差值

若在PE头内:

RVA = FOA

分析实例

以lyl610abc师傅于文中公布的dll文件为例,进行调试

导出函数的声明定义

1
2
3
4
5
EXPORTS
Add @12
Sub @15 NONAME
Multiply @17
Divide @10

具体导出函数定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int _stdcall Add(int x, int y)
{
return x+y;
}

int _stdcall Sub(int x, int y)
{
return x-y;
}

int _stdcall Multiply(int x, int y) {
return x * y;
}

int _stdcall Divide(int x, int y) {
return x / y;
}

找到DataDirectory

由于DataDirectory数据目录本身在PE头的扩展头部分,因此我们需要先获取到PE头的起始位,再根据IMAGE_NT_HEADERS结构的PE文件头标志大小+标准PE头大小达到扩展PE头:

image-20211014191938589

演示DLL文件为PE32文件,因此PE格式的标准头文件为 IMAGE_NT_HEADERS:

扩展PE头首地址:0xF8+0x4+0x14(20) = 0x110

image-20211014192704811

同时扩展PE头大小为224 字节,数据目录(DATADirectory大小) = _IMAGE_DATA_DIRECTORY大小*16 = 8 *16 = 128

因此224 - 128 = 96个字节

因此扩展PE头起始地址+ 96字节后为DataDirectory结构的起始地址

DataDirectory起始地址 = 扩展头地址+ 0x60(96) = 0x110 + 0x60 = 0x110

image-20211014195234440

由此拿到DataDirectory,顺序排放DataDirectory[0] ~ DataDirectory[15] 8字节一张表

DataDirectory[0] 对应着导出表

IMAGE_DATA_DIRECTORY成员 说明
VirtualAddress 0x00018FB0 表的起始位置(RVA)
Size 0x00000190 表的大小

得到导出表的RVA

根据结构拿到导出表RVA值为: 0x018FB0

导出表的size为: 0x0190

RVA转换FOA

但是IMAGE_DATA_DIRECTORY中的VirtualAddress 是RVA它在内存中才有的用, 在磁盘文件下需要将其转换成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 +0x18FB0,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\\mydll.dll", 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 +0x18FB0,dos,(_IMAGE_NT_HEADERS*)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;
}

关键代码

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

由于之前的函数是VA转FOA,这边我们拿到的是RVA,因此想要正确利用,需要用到RVA + ImageBase得到VA

image-20211014201121563

自此拿到FOA为:0x79B0

导出表的结构

介绍

可以定位到导出表后下一步便是要了解导出表的结构,才能解读导出表的信息

C语言中的结构体(Winnt.h可以找到),本结构在C语言中长40字节,导出表范围:导出表首地址(FOA)+导出表长度-1 (减1是由于导出表首地址也占一个字节)

image-20211014203245039

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

结构体分析

成员 数据宽度 说明
Characteristics DWORD(4字节) 标志,未用
TimeDateStamp DWORD(4字节) 时间戳
MajorVersion WORD(2字节) 未用
MinorVersion WORD(2字节) 未用
Name DWORD(4字节) 指向该导出表的文件名字符串
Base DWORD(4字节) 导出函数起始序号
NumberOfFunctions DWORD(4字节) 所有导出函数的个数
NumberOfNames DWORD(4字节) 以函数名字导出的函数个数
AddressOfFunctions DWORD(4字节) 导出函数地址表RVA
AddressOfNames DWORD(4字节) 导出函数名称表RVA
AddressOfNameOrdinals DWORD(4字节) 导出函数序号表RVA

解释几个比较重要的字段

Name

该字段指示的地址指向了一个以”\0”结尾的字符串,字符串记录了导出表所在文件的最初文件名,因此就算后期重命名此PE文件,也可在name处找到最初文件名

Base:

该字段指出了导出函数序号的起始值。dll中第一个导出函数的序号并非从0开始,导出函数的编号等于从AddressOfFunction开始的序号加上这个值。大概意思是这样

image-20211014205036689

如图所示,Func1的函数编号为nBase + 0 = 200h, Func2的函数编号为 nbase + 1 = 201h

NumberOfFunctions

该字段定义了文件中导出函数的总个数

NumberOfNames

在导出表中,有些函数是定义名字的,有些是没有定义名字的。该字段记录了所有定义名字函数的个数。如果这个值是0,则表示所有的函数都没有定义名字。NumbersOfNames一定小于等于NumbersOfFuctions

AddressOfNames

该值为一个指针。该指针指向的位置是一连串的DWORD值,这些值均指向了对应的定义了函数名的函数的字符串地址。这一连串的DWORD值的个数为NumberOfNames

AddressOfFunctions

该指针指向了全部导出函数的入口地址的起始。从入口地址开始为DWORD数组,数组的个数由NumbersOfFuctions决定

导出函数的每一个地址按函数的编号顺序依次往后排开。在内存中,可以通过函数编号来定位某个函数的地址

AddressOfNameOrdinals

该值也是一个指针,与AddressOfNames是一一对应关系

不同的是,AddressOfNames指向的是字符串的指针数组,而AddressOfNameOrdinals则指向了该函数在AddressOfFunctions中的索引值


注意:索引值数据类型为WORD,而非DWORD。该值与函数编号是两个不同的概念,两者的关系为:

索引值 = 编号 - Base

根据结构分析导出表

根据之前拿到的导出表FOA + 40 - 1 ,拿到导出表范围,自0x79B0 至0x79D7

image-20211015093614192

成员 说明
Characteristics 0x00000000 标志,未用
TimeDateStamp 0xFFFFFFFF 时间戳
MajorVersion 0x0000 未用
MinorVersion 0x0000 未用
Name 0x0001900A 指向该导出表文件名字符串
Base 0x0000000A 导出函数的起始序号
NumberOfFunctions 0x00000008 所有导出函数的个数
NumberOfNames 0x00000003 以函数名字导出的函数个数
AddressOfFunctions 0x00018FD8 导出函数地址表RVA
AddressOfNames 0x00018FF8 导出函数名称表RVA
AddressOfNameOrdinals 0x00019004 导出函数序号表RVA

Name

存储的值为指针,该指针为RVA(0x1900A),同样需要转换成FOA

1
VaToFoa32(nt->OptionalHeader.ImageBase+0x1900A,dos,nt,sectionArr); //0x1900A 为name的rva

运行可得

image-20211015110233872

利用16进制编辑器得到0x7A0A,拿到Name,PE文件的文件名字符串,以00结尾,MyDll.dll

image-20211015110551475

Base

导出函数起始序号的RVA 为 0xA , 对应十进制10

回顾之前导出函数的定义

1
2
3
4
5
EXPORTS
Add @12
Sub @15 NONAME
Multiply @17
Divide @10

可以看出,此处的base=最小的序号=min{12,15,17,10}=10

NumberOfFunctions

所有导出函数的个数为8

为什么前面声明的导出函数只有4个,但这里显示的却会有八个?

此处的NumberOfFunctions = 最大的序号减去最小的序号 +1 =17-10+1=8

NumberOfNames

以函数名字导出的函数个数为3,和定义声明中有名称的导出函数数量一致

AddressOfFunctions

存储的值为指针,该指针为RVA(0x18FD8),同样需要转换为FOA

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

利用之前的VA转FOA程序修改VaToFoa32代码处数值

运行程序可得AddressOfFunctions处文件偏移为0x79D8,跳转到该地址

根据AddressOfFunctions (DWORD)为数组且数组个数由NumbersOfFuctions决定,本dll文件NumberOfFunctions为8

因此8x4=32(10进制字节数)

拿到每个函数的RVA.

image-20211015112733536

image-20211015113610137

记录下每个导出函数的地址并转换为RVA为FOA得到

Oridinals(索引) 序号(Base+ORIDINALS) 导出函数地址(RVA) 导出函数地址(FOA)
0 10+0 = 10 0x00011320 0x720
1 10+1=11 0x00000000
2 12 0x00011302 0x702
3 13 0x00000000
4 14 0x00000000
5 15 0x000111EF 0x5EF
6 16 0x00000000
7 17 0x000111A4 0x5A4

可以发现,只有4个函数存在有效地址,跟前方Mydll.dll定义的序号以及数量一致

AddressOfNames

内部存储着导出函数名称表的RVA

存储的值为指针,该指针为RVA,同样需要转为FOA

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

image-20211015115246528

16进制编辑器直接跳转过去

导出函数名称表 = 导出函数名称表单个大小(DWORD) * NumberOfNames值 = 4 * 3 = 12(十进制)

image-20211015120351876

拿到所有导出函数名称的地址

1
2
3
0x00019014
0x00019018
0x0001901F

利用程序将RVA转为OA

1
2
3
VaToFoa32(nt->OptionalHeader.ImageBase+0x19014,dos,nt,sectionArr);
VaToFoa32(nt->OptionalHeader.ImageBase+0x19018,dos,nt,sectionArr);
VaToFoa32(nt->OptionalHeader.ImageBase+0x1901F,dos,nt,sectionArr);

按照顺序依次拿到

1
2
3
foa 7a14
foa 7a18
foa 7a1f

即得到有名称函数的名称地址为:

顺序索引 RVA FOA
1 0x19014 0x7A14
2 0x19018 0x7A18
3 0x1901F 0x7A1F

利用FOA与16进制编辑器拿到各自的函数名字符串

image-20211015120841885

顺序索引 RVA FOA 导出函数名称
1 0x19014 0x7A14 Add
2 0x19018 0x7A18 Divide
3 0x1901F 0x7A1F Multiply

AddressOfNameOrdinals

存储的值为指针,指针为RVA,同样需要转为FOA在PE文件中读取

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

获取foa为 7A04

跳转至0x7A04位置

由NumberOfFunctions可知共有3个有名函数

因此Ordinals列表为 4x3=12

image-20211015121856554

拿到有名称的Ordinals

Ordinals的为Word型,共2个字节

顺序索引 Oridinals 序号(Oridinals+Base)
1 0x0002 12
2 0x0000 10
3 0x0007 17

根据有名称函数的Oridinals结合前面得到的AddressOfFunctions和AdressOfNames,就可以得到函数的名称、函数的地址的关系

顺序索引 Oridinals 导出函数地址(RVA) 导出函数地址(FOA) 函数名称
1 0x0002 0x00011302 0x702 Add
2 0x0000 0x00011320 0x720 Divide
3 0x0007 0x000111A4 0x5A4 Multiply

至此导出表分析完毕

由导出表获得导出函数

从前面的分析中可以得知查询导出表有两个办法

  • 根据导出表函数名称获得导出函数地址
  • 根据导出表函数序号获得导出函数地址

函数名称获取导出函数地址

  1. 根据导出表的函数名称去AddressOfNames指向的每个名称字符串查询是否有匹配的字符串
  2. 找到匹配的字符串后,根据找到的顺序索引去AddressOfNameOrdinals中找到对应的Ordinals
  3. 根据前面找到的Ordinals到AddressOfFunctions中获得函数地址

图示

image-20211015125252798

函数序号取得导出函数

  1. 根据函数序号-导出表.Base获得导出函数的Ordinal
  2. 根据前面拿到的Ordinals到AddressOfFunctions中获得函数地址

图示

image-20211015125443438

也可利用工具得出导出表的RVA,后利用RVA转FOA可获得其文件偏移量

image-20211015125801883

image-20211015130012615

总结

  • 导出表中还包含了三张小表:导出函数地址表、导出函数名称表
  • 导出表中存储了指向这三张表的指针,而不是直接存储表内的内容
  • 无论是根据函数名称还是函数序号来获取导出函数,都需要用到Ordinals,用Ordinals到导出函数地址表中获取地址
  • 导出表的Base取决于编写DLL时导出定义的最小序号
  • 导出表的NumberOfFuctions取决于编写DLL时导出定义的序号最大差值+1
  • 导出名称表和导出函数序号表只对有名称的导出函数有效

学习复现自52pojie论坛内的lyl640abc 师傅的pe笔记系列