65.9K
CodeProject 正在变化。 阅读更多。
Home

加载 EXE 作为 DLL:任务可行

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (39投票s)

2015年11月1日

CPOL

4分钟阅读

viewsIcon

96284

downloadIcon

2645

将 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 : 首次发布

© . All rights reserved.