介绍

你可能已经被警告过,不要用LoadLibrary()加载可执行文件,你可能尝试这么做过,然后程序就崩溃了,所以你可能会认为这是不可能的。

但实际上这是可行的,本文就将介绍具体的方法。

声明

这好像跟微软说的有点不一样。实际上,微软没说不要加载,他们只是说“不要用LoadLibrary()加载可执行文件,应该用CreateProcess() ”。不过除非你很清楚你在做什么,否则不要把这用在产品代码中,我已经警告过你了| :)

准备可执行文件

首先要做的是把可执行文件标记为可重定位的文件,能够从任何的基地址(任何DLL)加载。你可以用/FIXED:NO来实现,如果想要提高安全性,还可以使用/DYNAMICBASE(默认就是开启的)。EXE文件可能设置了/FIXED:YES,那样的话exe就只能在它的首选基地址加载了,如果没有用/BASE设置过的话这个地址就是0×400000。

下一步的准备工作就是要我们需要从另外的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

使用LoadLibrary加载可执行文件

在使用LoadLibraryEx()加载可执行文件时候,不要指定LOAD_LIBRARY_AS_DATAFILE或者LOAD_LIBRARY_AS_IMAGE_RESOURCE,如果这么做的话,exe中的导出的函数就不能成功导出,而执行GetProcAddress()时就会失败。

调用LoadLibrary()后,我们就可以得到一个有效的HINSTANCE handle。但是当我们用LoadLibrary()加载exe文件时,以下两件关键的事没有发生:

1. CRT运行库没有初始化,包括所有全局变量
2. 导入地址表没有正确配置,这就意味着所有对导入函数的调用就会导致崩溃

更新导入表

首先我们得要更新可执行文件的导入表。下面的程序片段展示了其过程:

 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,建立异常处理器,加载argc和argv,并且调用WinMain。当WinMain返回时,WinMainCRTStartup则会调用exit()。

因此你得要从你的exe中导出调用WinMainCRTStartup的函数:

extern "C" void WinMainCRTStartup();
extern "C" void __stdcall InitCRT()
    {
    WinMainCRTStartup();
    }

问题是,这样的话你的WinMain会被调用。所以你得要放一个global flag。

extern "C" void WinMainCRTStartup();
bool DontDoAnything = false;
extern "C" void __stdcall InitCRT()
    {
    DontDoAnything = true;
    WinMainCRTStartup();
    }
   
int __stdcall WinMain(...)
    {
    if (DontDoAnything)
        return 0;
    // ...
    }

现在又有另外的问题了,当WinMain return的时候,WinMainCRTStartup会调用exit(),但你并不希望那样。因此,你不希望WinMain return:

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

不用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()了:

dllexe.exe
           14017B578 Import Address Table
           14017BC18 Import Name Table
                   0 time date stamp
                   0 Index of first forwarder reference
 
                         0 e0
                         1 e1

代码下载:http://pan.baidu.com/s/1pJpIgBL 密码: 6xx3

*参考来源:codeproject,译/Sphinx,文章有修改,转载请注明来自Freebuf黑客与极客(FreeBuf.COM)

源链接

Hacking more

...