动态加载可执行文件的技术探讨

在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文件时,有两件事情不会发生:

  • CRT(C运行时)没有初始化,包括任何全局变量。
  • 导入地址表(Import Address Table)没有正确配置,这意味着所有对导入函数的调用都将崩溃。

更新导入表

需要更新可执行文件的导入表。以下函数展示了如何做到这一点,这里为了简单起见省略了错误检查(在项目文件中,该函数是完全实现的):

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) } } }

初始化CRT

众所周知,可执行文件的入口点不是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);

调用EXE函数

在完成上述步骤后,直接调用可执行文件的函数应该可以按预期工作。在HotPatching文章中使用了这种技术。

不使用LoadLibrary/GetProcAddress链接到EXE

幸运的是,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
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485