PE文件之导出表
代码重用机制提供了重用代码的动态链接库,它会向调用者说明库里的哪些函数是可以被别人使用的,这些用来说明的信息便组成了导出表。EXE文件中很少有导出表,大部分的DLL文件中都有导出表。
导出表的存在可以让程序的开发者很容易清楚PE中到底有多少可以使用的函数,但如果没有函数使用说明,开发者也只能通过名称、反汇编代码或者运行结果对函数的调用方式、函数的功能等进行猜测。
Windows装载器在进行PE装载时,会将导入表中登记的所有DLL一并装入,然后根据DLL的导出表中对导入函数的描述修正导入表的IAT值。
综上所述,导出表的作用有两个:
DLL示例
下面给出我们自己编写的DLL源代码:
#define HOOKDLL_H __declspec(dllexport)
#include <iostream>
#include "HookDll.h"
#pragma comment(lib, "kernel32.lib")
#pragma comment(lib, "user32.lib")
HHOOK hHook;
HINSTANCE hInstance;
BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, LPVOID lpvReserved)
{
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
std::cout << hinstDll << std::endl;
hInstance = hinstDll;
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
default:
break;
}
return TRUE;
}
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode < 0)
return CallNextHookEx(hHook, nCode, wParam, lParam);
switch (wParam)
{
case WM_LBUTTONDOWN:
MessageBox(NULL, TEXT("HookProc: WM_LBUTTONDOWN"), TEXT("HookProc"), MB_OK);
break;
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
extern "C" HOOKDLL_H BOOL InstallHook()
{
HWND hWnd = FindWindow(NULL, TEXT("HookTest"));
if (hWnd == NULL)
{
std::cout << "Can't find the window" << std::endl;
return FALSE;
}
DWORD dwThreadId = GetWindowThreadProcessId(hWnd, NULL);
std::cout << dwThreadId << std::endl;
hHook = SetWindowsHookEx(WH_MOUSE, HookProc, hInstance, dwThreadId);
if (hHook == NULL)
{
std::cout << "can't set hook" << std::endl;
return FALSE;
}
return TRUE;
}
extern "C" HOOKDLL_H BOOL UninstallHook()
{
if (hHook)
{
UnhookWindowsHookEx(hHook);
hHook = NULL;
}
return TRUE;
}
我们编译并链接生成可执行文件:HookDll.dll
导出表数据结构
导出表数据为数据目录中注册的数据类型之一,其描述信息处于数据目录的第1个目录项中。

第一项是一个FOA,指向了导出表描述符。下面两张图片分别展示了导出表描述符的hex和数据组织结构:


导入表的IMAGE_IMPORT_DESCRIPTOR个数与调用的动态链接库个数相等,而导出表的IMAGE_EXPORT_DIRECTORY只有一个。
以下是导出表描述符各字段的详细解释:
| 0x00 | Characteristics | 4字节 | 保留字段,通常设置为0。在早期的Windows版本中可能有特定含义,但现在不再使用。 |
| 0x04 | TimeDateStamp | 4字节 | 导出表创建的时间戳,表示自1970年1月1日以来的秒数。通常由链接器生成,但也可以为0。反调试工具可能修改此值。 |
| 0x08 | MajorVersion | 2字节 | 主版本号,通常为0。这是一个可选的版本信息字段,大多数编译器将其设置为0。 |
| 0x0A | MinorVersion | 2字节 | 次版本号,通常为0。同主版本号一样,通常为0,除非明确指定版本信息。 |
| 0x0C | Name | 4字节 | 模块名称的RVA(相对虚拟地址)。指向一个以null结尾的ASCII字符串,包含本模块的名称。这是其他模块通过GetModuleHandle或LoadLibrary加载模块时使用的名称。 |
| 0x10 | Base | 4字节 | 导出函数的起始序号。导出表中的函数序号是相对值,实际序号=Base+索引。例如,如果Base=1,则第一个导出函数的序号为1。这个值通常为1,但可以是其他值。 |
| 0x14 | NumberOfFunctions | 4字节 | 导出函数的数量。即EAT(Export Address Table,导出地址表)中的条目数。这个值决定了有多少函数可以导出。 |
| 0x18 | NumberOfNames | 4字节 | 按名称导出的函数数量。即ENT(Export Name Table,导出名称表)中的条目数。注意:只有通过名称导出的函数才会计入此数,通过序号导出的函数不包括在内。 |
| 0x1C | AddressOfFunctions | 4字节 | 导出地址表的RVA。指向一个DWORD数组,数组的每个元素是导出函数的RVA。数组的大小由NumberOfFunctions指定。如果RVA指向模块内部,则为函数地址;如果指向导出表内部,则可能是一个转发器(forwarder)。 |
| 0x20 | AddressOfNames | 4字节 | 导出名称表的RVA。指向一个DWORD数组,数组的每个元素是一个函数名称字符串的RVA。数组的大小由NumberOfNames指定。这些名称按字母顺序排序,以支持二分查找。 |
| 0x24 | AddressOfNameOrdinals | 4字节 | 导出序号表的RVA。指向一个WORD数组,数组的每个元素是对应函数在EAT中的索引。数组的大小同样由NumberOfNames指定。这个表与AddressOfNames一一对应,用于建立函数名称到EAT索引的映射。 |



