在Windows编程中,通常会使用LoadLibrary函数来加载动态链接库(DLL),并使用GetProcAddress来获取DLL中函数的地址。但是,是否尝试过使用LoadLibrary来加载一个可执行文件(EXE)并调用其函数呢?本文将带了解如何实现这一技术,并探讨其背后的原理和潜在的风险。
首先,需要准备一个可执行文件,使其能够像DLL一样在任意基址加载。这可以通过链接器选项/FIXED:NO实现,同时为了提高安全性,可以使用/DYNAMICBASE选项(默认情况下是开启的)。如果EXE文件使用/FIXED:YES链接,那么所有的重定位信息将被剥离,EXE只能加载到其首选的基址,除非通过/BASE选项设置,否则默认是0x400000。
接下来,需要准备导出函数,以便从另一个EXE中调用。这与DLL的方式类似:
extern "C" void __stdcall some_func() {
// ...
}
#ifdef _WIN64
#pragma comment(linker, "/EXPORT:some_func=some_func")
#else
#pragma comment(linker, "/EXPORT:some_func=_some_func@0")
#endif
在加载可执行文件时,不能使用LoadLibraryEx函数并指定LOAD_LIBRARY_AS_DATAFILE或LOAD_LIBRARY_AS_IMAGE_RESOURCE。这样做不会导出EXE中的函数,并且GetProcAddress调用将失败。
调用LoadLibrary后,得到一个有效的HINSTANCE句柄。但是,使用LoadLibrary加载.EXE文件时,有两件事情不会发生:
需要更新可执行文件的导入表。以下函数展示了如何做到这一点,这里为了简单起见省略了错误检查(在项目文件中,该函数是完全实现的):
void ParseIAT(HINSTANCE h) {
// Find the IAT size
DWORD ulsize = 0;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(h, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulsize);
if (!pImportDesc) return;
// Loop names
for (; pImportDesc->Name; pImportDesc++) {
PSTR pszModName = (PSTR)((PBYTE)h + pImportDesc->Name);
if (!pszModName) break;
HINSTANCE hImportDLL = LoadLibraryA(pszModName);
if (!hImportDLL) {
// ... (error)
}
// Get caller's import address table (IAT) for the callee's functions
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)h + pImportDesc->FirstThunk);
// Replace current function address with new function address
for (; pThunk->u1.Function; pThunk++) {
FARPROC pfnNew = 0;
size_t rva = 0;
// ... (rest of the code)
}
}
}
众所周知,可执行文件的入口点不是WinMain,而是WinMainCRTStartup()。这个函数初始化CRT(假设它链接了CRT),设置异常处理程序,加载argc和argv,并调用WinMain。当WinMain返回时,WinMainCRTStartup调用exit()。
因此,需要从EXE中导出一个函数,该函数调用WinMainCRTStartup:
extern "C" void WinMainCRTStartup();
extern "C" void __stdcall InitCRT() {
WinMainCRTStartup();
}
这个调用的问题是WinMain将被调用。所以可以设置一个全局标志,如果设置了,WinMain将什么都不做:
extern "C" void WinMainCRTStartup();
bool DontDoAnything = false;
extern "C" void __stdcall InitCRT() {
DontDoAnything = true;
WinMainCRTStartup();
}
int __stdcall WinMain(...) {
if (DontDoAnything) return 0;
// ...
}
但还有另一个问题。当WinMain返回时,WinMainCRTStartup将调用exit(),不希望这样。因此不希望WinMain返回:
int __stdcall WinMain(...) {
if (DontDoAnything) {
for (;;) {
Sleep(60000);
}
}
// ...
}
但这样做将永远阻塞初始化 - 因此必须将其放入某个线程:
std::thread t([]() {
InitCRT();
});
t.detach();
但也想知道CRT何时完成初始化,所以最终的解决方案是使用一个事件:
HANDLE hEv = CreateEvent(0, 0, 0, 0);
void (__stdcall *InitCRT)(HANDLE) = (void (__stdcall*)(HANDLE))GetProcAddress(hL, "InitCRT");
if (!InitCRT) return 0;
std::thread t([&](HANDLE h) {
InitCRT(h);
}, hEv);
t.detach();
WaitForSingleObject(hEv, INFINITE);
在完成上述步骤后,直接调用可执行文件的函数应该可以按预期工作。在HotPatching文章中使用了这种技术。
幸运的是,LINK.EXE为DLLEXE.EXE生成了一个.lib文件,因此可以使用它将EXE链接到EXE,就像它是另一个DLL一样:
#pragma comment(lib,"..\\dllexe\\dllexe.lib")
extern "C" void __stdcall e0(HANDLE);
extern "C" void __stdcall e1();
仍然需要修补IAT并调用CRT初始化,但不再需要GetProcAddress()所需的函数。喜欢DUMPBIN.EXE的这个入口列表:
dllexe.exe
14017B578 Import Address Table
14017BC18 Import Name Table
0 time date stamp
0 Index of first forwarder reference
0 e0
1 e1