动态链接库(DLL)的导出表分析

导出表(Export Table)是PE(Portable Executable)格式中一个非常重要的部分,主要用于记录DLL文件中的导出信息,系统通过它可以找到DLL中的函数、资源等,完成动态链接的过程。

在Windows操作系统中,动态链接库(DLL)文件并不是像静态链接库那样直接嵌入到程序中,而是通过导出表提供的入口点与调用程序进行动态连接。系统会根据导出表中的信息,知道如何加载函数和资源。

扩展名为.exe 的PE 文件中一般不存在导出表,而大部分的.dll 文件中都包含导出表。但这并不是绝对的。例如纯粹用作资源的.dll 文件就不需要导出函数,另外有些特殊功能的.exe 文件也会存在导出函数。

导出表(Export Table)中的主要成分是一个表格,内含函数名称、输出序数等。序数是指定DLL 中某个函数的16位数字,在所指向的DLL 文件中是独一无二的。在此我们不提倡仅仅通过序数来索引函数的方法,这样会给DLL 文件的维护带来问题。例如当DLL 文件一旦升级或修改就可能导致调用改DLL 的程序无法加载到需要的函数。

导出表的结构

导出表通常是一个包含多个信息的表格,记录着DLL导出的函数、资源等信息,主要包括:

  • 函数名
  • 函数的序号
  • 函数的入口地址等

导出表的基本结构是一个结构体,通常定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
0x00 DWORD Characteristics; // 未使用
0x04 DWORD TimeDateStamp; // 时间戳
0x08 WORD MajorVersion; // 未使用
0x0a WORD MinorVersion; // 未使用
0x0c DWORD Name; // 指向导出表文件名字的字符串
0x10 DWORD Base; // 导出函数的起始序号
0x14 DWORD NumberOfFunctions; // 导出函数的个数
0x18 DWORD NumberOfNames; // 通过函数名导出的函数个数
0x1c DWORD AddressOfFunctions; // 导出函数地址表的RVA(相对虚拟地址)
0x20 DWORD AddressOfNames; // 导出函数名称表的RVA
0x24 DWORD AddressOfNameOrdinals; // 导出函数序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

RVA 和 FOA 的区别

  • **RVA (Relative Virtual Address)**:指相对虚拟地址,它是模块在内存中加载后的地址相对于基址的偏移量。举个例子:如果PE文件加载到虚拟地址 0x400000,而某个数据项的地址是 0x401000,则RVA为 0x1000
  • **FOA (File Offset Address)**:指文件的偏移地址,它是某个位置距离文件头的偏移量。FOA与内存无关,只与文件本身的结构有关。

注意:在PE文件的解析中,经常会遇到需要将RVA转换为FOA的情况。

导出表的组成

导出表主要分为三张表:

  1. 导出函数地址表(AddressOfFunctions)
  2. 导出函数名称表(AddressOfNames)
  3. 导出函数序号表(AddressOfNameOrdinals)

这三张表的存在有其必要性,主要是因为:

  • 函数可以通过序号导出,也可以通过函数名导出。如果将这两种方式放在一张表中,会导致索引混乱和不易管理。
  • 函数序号表的作用是:每个函数在导出时都会指定一个唯一的序号,序号是16位的数字,并且是唯一的。这样可以通过序号直接索引函数,而不必依赖函数名。
  • 函数名称表则是通过字符串来定位函数。
  • 函数序号表通过 Base 加上一个偏移值来映射到具体的函数序号。

image-20241229233716192

导出函数的查找方式

导出函数的查找有两种方式:

  1. 通过函数名查找

    • 首先,查找 AddressOfNames 表中存储的函数名字符串。
    • 如果匹配到某个函数名,接着查找 AddressOfNameOrdinals,通过名称找到对应的序号。
    • 最后,通过序号从 AddressOfFunctions 表中找到对应的函数地址。

    image-20241229233932675

  2. 通过序号查找

    • 直接给定一个序号,先减去 Base 得到实际的函数序号。
    • 然后在 AddressOfFunctions 表中通过这个序号直接查找对应的函数地址。

    image-20241229234009942

表格的具体内容

  • AddressOfFunctions:该表是一个包含导出函数地址的表,每个函数的地址通过RVA表示。通过该表可以获得函数的入口点。
  • AddressOfNames:该表包含函数名称的字符串地址。如果函数是通过名称导出的,查找这个表可以得到对应的函数名称。
  • AddressOfNameOrdinals:该表包含每个函数的序号,每个序号对应一个函数,可以通过序号访问到相应的函数。

关键字段说明

  1. AddressOfFunctions
    • 说明:存储的是导出函数地址表的 RVA,直接指向函数的入口地址。
    • 操作:直接使用即可,不需要进一步索引。
  2. AddressOfNames
    • 说明:存储的是导出函数名称表的 RVA,表中存放的是函数名称的字符串地址(也是RVA),需要进一步索引到具体的名称字符串。
    • 操作:先根据 AddressOfNames 找到 RVA,再通过基址加上这个偏移值,最终获取到实际的字符串地址。
  3. AddressOfNameOrdinals
    • 说明:存储的是导出函数序号表的 RVA,每个值为一个序号,序号是函数在导出函数地址表中的索引。
    • 操作:直接使用即可,不需要再索引到别的地方。

关键提示

  • AddressOfFunctionsAddressOfNameOrdinals 的值指向的数据可以直接使用,不需要再进行索引。
  • AddressOfNames 指向的值需要进一步解析,最终定位到实际的字符串地址。

例子:查找导出函数

步骤总结:

  1. 通过函数名导出:
    • 使用 AddressOfNames 获取名称字符串地址。
    • 比较字符串,找到目标函数的索引值。
    • 使用索引值从 AddressOfNameOrdinals 获取函数序号。
    • 最后通过序号从 AddressOfFunctions 找到目标函数地址。
  2. 通过序号导出:
    • 函数序号减去 Base,得到索引值。
    • 直接在 AddressOfFunctions 中通过索引值找到函数地址。

重要结论:

  • 直接使用 AddressOfFunctionsAddressOfNameOrdinals
  • AddressOfNames 需要再索引到字符串地址后处理。

总结

  • 导出表使得DLL能够动态链接并提供函数访问。通过导出表,系统可以使用函数名称或序号访问DLL中的函数。
  • RVA与FOA的转换是PE文件解析的一个重要概念,必须特别注意。
  • 在动态链接过程中,导出表的三个部分相互协作,保证了程序能够灵活、准确地调用DLL中的函数。

Work:打印导出表

示例代码

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
229
230
231
#include <iostream>
#include <windows.h>

LPVOID ReadPE(
IN LPCSTR lpszName
)
{
FILE* file = nullptr;
fopen_s(&file, lpszName, "rb");
if (!file)
{
printf("打开文件失败!\n");
return nullptr;
}

fseek(file, 0, SEEK_END);
size_t size = ftell(file);
fseek(file, 0, SEEK_SET);

LPVOID fileBuff = malloc(size);
if (!fileBuff)
{
printf("申请内存空间失败!\n");
fclose(file);
return nullptr;
}
fread_s(fileBuff, size, 1, size, file);

WORD mz = *((PWORD)fileBuff);
if (mz != 0x5a4d)
{
printf("该文件不是pe可执行程序!\n");
fclose(file);
free(fileBuff);
return nullptr;
}

return fileBuff;
}

void AnlyzePE(
IN LPVOID pe,
OUT PIMAGE_DOS_HEADER& dos,
OUT PIMAGE_FILE_HEADER& file,
OUT PIMAGE_OPTIONAL_HEADER32& optional,
OUT PIMAGE_SECTION_HEADER*& section
)
{
dos = (PIMAGE_DOS_HEADER)pe;
file = (PIMAGE_FILE_HEADER)((PCHAR)pe + dos->e_lfanew + 4);
optional = (PIMAGE_OPTIONAL_HEADER32)((PCHAR)pe + dos->e_lfanew + 4 + 20);
section = (PIMAGE_SECTION_HEADER*)malloc(file->NumberOfSections * sizeof(IMAGE_SECTION_HEADER));
if (section != nullptr)
{
for (int i = 0; i < file->NumberOfSections; i++)
{
*(section + i) = (PIMAGE_SECTION_HEADER)((PCHAR)pe + dos->e_lfanew + 4 + 20 + file->SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER)));
}
}
}

DWORD RvaToFoa(
IN LPVOID pe,
IN UINT_PTR rva
)
{
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);
DWORD foa = -1;

for (int i = 0; i < file->NumberOfSections; i++)
{
UINT_PTR begin = (*section + i)->VirtualAddress;
UINT_PTR end = (*section + i)->VirtualAddress + (*section + i)->SizeOfRawData;
if (begin <= rva && rva <= end)
{
foa = rva - begin + (*section + i)->PointerToRawData;
break;
}
}
free(section);
return foa;
}

void PrintExport(
IN LPVOID fileBuff
)
{
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(fileBuff, dos, file, optional, section);

DWORD offset = RvaToFoa(fileBuff, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)fileBuff + offset);
printf(">>>> 导出表 <<<<\n");
printf("Characteristics =%x\n", exportTable->Characteristics);
printf("TimeDateStamp =%x\n", exportTable->TimeDateStamp);
printf("MajorVersion =%x\n", exportTable->MajorVersion);
printf("MinorVersion =%x\n", exportTable->MinorVersion);
printf("Name =%x\n", exportTable->Name);
printf("Base =%x\n", exportTable->Base);
printf("NumberOfFunctions =%x\n", exportTable->NumberOfFunctions);
printf("NumberOfNames =%x\n", exportTable->NumberOfNames);
printf("AddressOfFunctions =%x\n", exportTable->AddressOfFunctions);
printf("AddressOfNames =%x\n", exportTable->AddressOfNames);
printf("AddressOfNameOrdinals =%x\n", exportTable->AddressOfNameOrdinals);

DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfFunctions));
printf(">>>> Functions <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
printf("%d = %x\n", i, *(*(function)+i));
}

WORD(*ordinal)[1];
ordinal = (WORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfNameOrdinals));
printf(">>>> Ordinals <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
printf("%d = %x\n", i, *(*(ordinal)+i));
}

DWORD(*name)[1];
name = (DWORD(*)[1])((PCHAR)fileBuff + RvaToFoa(fileBuff, exportTable->AddressOfNames));
printf(">>>> Names <<<<\n");
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
printf("%d = %s\n", i, (PCHAR)fileBuff + RvaToFoa(fileBuff, *(*(name)+i)));
}
free(section);
return;
}

bool M_strcmp(
IN char* s1,
IN char* s2
)
{
int length = strlen(s1);
if (length != strlen(s2))
{
return false;
}
else
{
for (int i = 0; i < length; i++)
{
if (s1[i] != s2[i])
{
return false;
}
}
}
return true;
}


LPVOID GetFunctionAddrByName(
IN LPVOID pe,
IN LPCSTR funcName
)
{
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);

DWORD offset = RvaToFoa(pe, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)pe + offset);

DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfFunctions));
WORD(*ordinal)[1];
ordinal = (WORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfNameOrdinals));
DWORD(*name)[1];
name = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfNames));
for (int i = 0; i < exportTable->NumberOfFunctions; i++)
{
LPCSTR tempName = (PCHAR)pe + RvaToFoa(pe, *(*(name)+i));
if (M_strcmp((char*)tempName, (char*)funcName))
{
DWORD funcIndex = *(*(ordinal)+i);
free(section);
return (LPVOID) * (*(function)+funcIndex);
}
}
free(section);
return nullptr;
}

LPVOID GetFunctionAddrByOrdinal(
IN LPVOID pe,
IN DWORD exportNumber
)
{
PIMAGE_DOS_HEADER dos = nullptr;
PIMAGE_FILE_HEADER file = nullptr;
PIMAGE_OPTIONAL_HEADER32 optional = nullptr;
PIMAGE_SECTION_HEADER* section = nullptr;
AnlyzePE(pe, dos, file, optional, section);

DWORD offset = RvaToFoa(pe, optional->DataDirectory[0].VirtualAddress);
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((PCHAR)pe + offset);

DWORD(*function)[1];
function = (DWORD(*)[1])((PCHAR)pe + RvaToFoa(pe, exportTable->AddressOfFunctions));
free(section);
return (LPVOID) * (*(function)+(exportNumber - exportTable->Base));
}

int main()
{
LPVOID fileBuff = ReadPE(R"(D:\source\repos\dllmain\Debug\MyDll.dll)");
if (fileBuff) {
PrintExport(fileBuff);
LPVOID addAddress = GetFunctionAddrByName(fileBuff, "add");
LPVOID maxAddress = GetFunctionAddrByOrdinal(fileBuff, 5);
printf("null\n");
}

free(fileBuff);
system("pause");
return 0;
}

运行结果

image-20241229235358296