加载 EXE 作为 DLL:任务可行






4.96/5 (39投票s)
将 EXE 文件加载为另一个 EXE 中的 DLL 并执行函数
引言
您已被警告不要使用 LoadLibrary() 加载可执行文件,您可能已经尝试过这样做,并且您的应用程序崩溃了。因此,您认为这是不可能的。
实际上,这是完全可能的。让我们来看看如何做到。
免责声明
这在某种程度上违背了微软的说法。事实上,他们从未说过“不要加载它”。他们只是说“不要使用 LoadLibrary() 来运行可执行文件,而是使用 CreateProcess()”。谁说过要运行 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 未初始化,包括任何全局变量,并且
- 导入地址表(IAT)未正确配置,这意味着所有对导入函数的调用都会崩溃。
更新导入表
然后我们必须先更新可执行文件的导入表。下面的函数演示了方法,为简洁起见省略了错误检查(在项目文件中,该函数已完整实现):
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; #ifdef _WIN64 if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64) #else if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32) #endif { // Ordinal #ifdef _WIN64 size_t ord = IMAGE_ORDINAL64(pThunk->u1.Ordinal); #else size_t ord = IMAGE_ORDINAL32(pThunk->u1.Ordinal); #endif PROC* ppfn = (PROC*)&pThunk->u1.Function; if (!ppfn) { // ... (error) } rva = (size_t)pThunk; char fe[100] = {0}; sprintf_s(fe,100,"#%u",ord); pfnNew = GetProcAddress(hImportDLL,(LPCSTR)ord); if (!pfnNew) { // ... (error) } } else { // Get the address of the function address PROC* ppfn = (PROC*)&pThunk->u1.Function; if (!ppfn) { // ... (error) } rva = (size_t)pThunk; PSTR fName = (PSTR)h; fName += pThunk->u1.Function; fName += 2; if (!fName) break; pfnNew = GetProcAddress(hImportDLL,fName); if (!pfnNew) { // ... (error) } } // Patch it now... auto hp = GetCurrentProcess(); if (!WriteProcessMemory(hp,(LPVOID*)rva,&pfnNew,sizeof(pfnNew),NULL) && (ERROR_NOACCESS == GetLastError())) { DWORD dwOldProtect; if (VirtualProtect((LPVOID)rva,sizeof(pfnNew),PAGE_WRITECOPY,&dwOldProtect)) { if (!WriteProcessMemory(GetCurrentProcess(),(LPVOID*)rva,&pfnNew,sizeof(pfnNew),NULL)) { // ... (error) } if (!VirtualProtect((LPVOID)rva,sizeof(pfnNew),dwOldProtect,&dwOldProtect)) { // ... (error) } } } } } }
此函数遍历整个 IAT,将指向导入函数的无效引用替换为从我们自己的 IAT 表中获取的正确引用(这些引用是通过 LoadLibrary() 和 GetProcAddress() 获取的)。
初始化 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);
然后您的其他代码将是
extern "C" void WinMainCRTStartup(); HANDLE hEV = 0; extern "C" void __stdcall InitCRT(HANDLE hE) { hEV = hE; WinMainCRTStartup(); } int __stdcall WinMain(...) { if (hEV) { SetEvent(hEV); for(;;) { Sleep(60000); } } }
这一切的症结是什么?您的所有全局变量都在另一个线程的上下文中初始化,而不是主线程。如果全局内容的初始化必须在主线程中进行,那么您的 WinMain 可以再次调用您的回调函数并且永不返回;然后从该回调函数返回执行。
您还面临着在同一地址空间中两次调用 WinMainCRTStartup() 的风险(第一次是从您自己的可执行文件中)。是否有任何副作用?谁知道。
调用 EXE 函数
之后,直接调用可执行文件函数应该可以按预期工作。我在这篇 HotPatching 文章 中使用了这种技术。
在没有 LoadLibrary/GetProcAddress 的情况下链接到 EXE
幸运的是,LINK.EXE 为我们的 DLLEXE.EXE 生成了一个 .lib 文件,因此,我们可以像使用另一个 DLL 一样,使用它从我们的 EXE 链接到该 EXE。
#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
代码
代码包含 2 个 EXE 项目,其中一个加载另一个。玩得开心。
历史
01 - 11 - 2015 : 首次发布