我知道我可以阅读PE 规范以编写执行此操作的代码。但是,由于我手头没有太多时间,我希望你们中的一些人可能已经准备好发送这样的代码示例。
重要提示: 32位和64位有什么区别吗?
感谢您的时间!
PE 文件导出
与导入函数相反的是导出函数以供 EXE 或其他 DLL 使用。PE 文件将有关其导出函数的信息存储在 .edata 部分中。通常,Microsoft 链接器生成的 PE EXE 文件不会导出任何内容,因此它们没有 .edata 部分。Borland 的 TLINK32 总是从 EXE 中导出至少一个符号。大多数 DLL 都会导出函数并具有 .edata 部分。.edata 部分(又名导出表)的主要组成部分是函数名称、入口点地址和导出序数值的表。在 NE 文件中,导出表的等价物是条目表、常驻名称表和非常驻名称表。这些表存储为 NE 标头的一部分,而不是存储在不同的段或资源中。
.edata 部分的开头是一个 IMAGE_EXPORT_DIRECTORY 结构(参见表 10)。该结构后面紧跟着结构中字段指向的数据。
表 10. IMAGE_EXPORT_DIRECTORY格式
DWORD Characteristics
该字段似乎未使用,并且始终设置为 0。
DWORD TimeDateStamp
指示此文件何时创建的时间/日期戳。
WORD MajorVersion
WORD MinorVersion
这些字段似乎未使用并设置为 0。
DWORD Name
具有此 DLL 名称的 ASCIIZ 字符串的 RVA。
DWORD Base
导出函数的起始序号。例如,如果文件导出序号值为 10、11 和 12 的函数,则此字段包含 10。要获取导出的函数序号,您需要将此值添加到 AddressOfNameOrdinals 数组的适当元素中。
DWORD NumberOfFunctions
AddressOfFunctions 数组中的元素数。这个值也是这个模块导出的函数的数量。理论上,该值可能与 NumberOfNames 字段(下一个)不同,但实际上它们始终相同。
DWORD NumberOfNames
AddressOfNames 数组中的元素数。该值似乎总是与 NumberOfFunctions 字段相同,导出函数的数量也是如此。
PDWORD *AddressOfFunctions
该字段是一个 RVA,指向一个函数地址数组。函数地址是此模块中每个导出函数的入口点 (RVA)。
PDWORD *AddressOfNames
该字段是一个 RVA,指向一个字符串指针数组。字符串是此模块中导出函数的名称。
PWORD *AddressOfNameOrdinals
该字段是一个 RVA,指向一个 WORD 数组。WORD 是该模块中所有导出函数的导出序号。但是,不要忘记添加在 Base 字段中指定的起始序号。
导出表的布局有些奇怪(参见图 4 和表 10)。正如我前面提到的,导出函数的要求是名称、地址和导出序号。您会认为 PE 格式的设计者会将所有这三个项目放入一个结构中,然后拥有这些结构的数组。相反,导出条目的每个组件都是数组中的一个元素。其中有三个数组(AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals),它们都相互平行。要查找有关第四个函数的所有信息,您需要查找每个数组中的第四个元素。
图 4. 导出表布局
表 11. EXE 文件的典型导出表
Name: KERNEL32.dll
Characteristics: 00000000
TimeDateStamp: 2C4857D3
Version: 0.00
Ordinal base: 00000001
# of functions: 0000021F
# of Names: 0000021F
Entry Pt Ordn Name
00005090 1 AddAtomA
00005100 2 AddAtomW
00025540 3 AddConsoleAliasA
00025500 4 AddConsoleAliasW
00026AC0 5 AllocConsole
00001000 6 BackupRead
00001E90 7 BackupSeek
00002100 8 BackupWrite
0002520C 9 BaseAttachCompleteThunk
00024C50 10 BasepDebugDump
// Rest of table omitted...
顺便说一句,如果您从 Windows NT 系统 DLL(例如,KERNEL32.DLL 和 USER32.DLL)中转储导出,您会注意到在许多情况下,有两个函数在结尾处仅相差一个字符名称,例如 CreateWindowExA 和 CreateWindowExW。这就是透明地实现 UNICODE 支持的方式。以 A 结尾的函数是 ASCII(或 ANSI)兼容函数,而以 W 结尾的是函数的 UNICODE 版本。在您的代码中,您没有明确指定要调用的函数。相反,通过预处理器#ifdefs 在 WINDOWS.H 中选择适当的函数。这段来自 Windows NT WINDOWS.H 的摘录显示了它是如何工作的示例:
#ifdef UNICODE
#define DefWindowProc DefWindowProcW
#else
#define DefWindowProc DefWindowProcA
#endif // !UNICODE
//
// Export Format
//
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;
编辑:在 PE 格式的导出表中,函数的地址只是 64 位地址的 RVA。