Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / Win32

Load EXE as DLL: Mission Possible

Rate me:
Please Sign up or sign in to vote.
4.96/5 (39 votes)
6 Dec 2015CPOL4 min read 89.1K   2.5K   73   16
Load an EXE file as a DLL in another EXE and execute the functions

Introduction

You have been warned not to load an executable with LoadLibrary(), you have probably tried to do so and your application crashed. Therefore you thought that it is not possible.

Actually, it is quite possible. Le't see how.

Disclaimer

This is somewhat against what Microsoft has to say about it. Actually, they never say "don't load it". They just say "don't use LoadLibrary() to run an executable, use CreateProcess() instead". Who said anything about running the EXE? Therefore, what I will present here is compatible enough, at least for the mighty reader. But  don't use this stuff in production code unless you know very well what you are doing. You have been warned.

Preparing the Executable

The first required thing to do is to mark the executable as relocatable with the ability to load at any base address (as any DLL). This is done with the /FIXED:NO, and you can improve security by using /DYNAMICBASE (which is on by default) as well. EXE files might be linked with /FIXED:YES in which all relocation information from the executable is stripped and the EXE can only be load in it's prefered base address, which, unless set by the /BASE option is 0x400000. 

The next thing to prepare is the exported functions we will call from another EXE, this is of course similar to the DLL way:

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

 

LoadLibrarying the Executable

Do not load the executable with LoadLibraryEx() specifying LOAD_LIBRARY_AS_DATAFILE or LOAD_LIBRARY_AS_IMAGE_RESOURCE. Doing so will not export the exported functions from the EXE and GetProcAddress() to it will fail.

After a call to LoadLibrary(), we get a valid HINSTANCE handle. However, when loading an .EXE file with LoadLibrary() , two important things are NOT happening:

  • The CRT is not initialized, including any global variables and,
  • The Import Address Table is not correctly configured, which means that all calls to imported functions will crash.

 

Updating the Import Table

 

We then first have to update the Import Table for the executable. The following function demonstrates the way, with error-checkind removed from simplicity (in the project file, the function is fully implemented):

 

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

This function loops through the entire IAT, replacing invalid references to the imported functions with the correct references got from our OWN IAT table (which are simply taken with LoadLibrary() and GetProcAddress()).

 

Initializing the CRT

As you know, the entry point of the executable is not WinMain but WinMainCRTStartup(). This function initializes the CRT (assuming that it was linked against the CRT), sets up exception handlers, loads argc and argv and calls WinMain. When WinMain returns, WinMainCRTStartup calls exit().

Therefore you have to export a function from your EXE that calls WinMainCRTStartup:

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

The problem with this call is that your WinMain will be called. So you might put a global flag that allows WinMain to do nothing if it is set:

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

But you have another problem. When WinMain returns, WinMainCRTStartup will call exit(), and you don't want that. Therefore you don't want WinMain to return:

int __stdcall WinMain(...)
    {
    if (DontDoAnything)
        {
        for(;;)
            {    
            Sleep(60000);
            }    
        }
    // ...
    }

    
But doing this will block for ever your initialization - therefore you must put it in some thread:

std::thread t([] ()
        {
        InitCRT();
        }
    );
t.detach();

But you also want to know when the CRT has finished it's initialization, so the final solution would be to use an event:

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

And then your other code would be:

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

What is the catch in all this? All your globals are initialized within the context of another thread, and not the main one. If initializing global stuff must be in the main thread, then you can have your WinMain call again your own callback and never return; and then return execution from that callback.    

You also run the risk of calling WinMainCRTStartup() twice (the first was from your own executable) in the same address space. Are there any side effects? Who knows.

    

Calling the EXE functions

After that, calling directly the executable functions should work as expected.  I use that technique in my HotPatching article.

 

 

 

Linking to the EXE without LoadLibrary/GetProcAddress

Fortunately, LINK.EXE generates a .lib file for our DLLEXE.EXE and therefore, we can use it to link to the EXE from our EXE as if it was another DLL:

#pragma comment(lib,"..\\dllexe\\dllexe.lib")
extern "C"    void __stdcall e0(HANDLE);
extern "C"    void __stdcall e1();

We still have to patch the IAT and call the CRT init, but we don't anymore need to GetProcAddress() the functions we need. I like this entry listing from 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

Code

The code contains 2 EXE projects in which the one loads the other. Have fun with it.

History

01 - 11 - 2015 : First Release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Greece Greece
I'm working in C++, PHP , Java, Windows, iOS, Android and Web (HTML/Javascript/CSS).

I 've a PhD in Digital Signal Processing and Artificial Intelligence and I specialize in Pro Audio and AI applications.

My home page: https://www.turbo-play.com

Comments and Discussions

 
QuestionWhy is it so complicated? Pin
Kay-Uwe Schreiner13-Mar-24 15:02
Kay-Uwe Schreiner13-Mar-24 15:02 
Questionunnecessary code in this example Pin
Mike Kosak12-Jul-23 7:30
Mike Kosak12-Jul-23 7:30 
QuestionCalling main entry point? Pin
Danielku158-Mar-18 20:47
Danielku158-Mar-18 20:47 
AnswerRe: Calling main entry point? Pin
Michael Chourdakis26-Nov-18 8:58
mvaMichael Chourdakis26-Nov-18 8:58 
PraiseNice Pin
neo85x9-Nov-17 20:39
neo85x9-Nov-17 20:39 
Praisevery nice Pin
BillW3314-Dec-15 8:34
professionalBillW3314-Dec-15 8:34 
Bugrun error Pin
HateCoding7-Dec-15 16:34
HateCoding7-Dec-15 16:34 
GeneralRe: run error Pin
Harm-Jan8-Dec-15 4:10
professionalHarm-Jan8-Dec-15 4:10 
Questionone question about static variable or global variable use by dllexe Pin
fyten198517-Nov-15 0:32
fyten198517-Nov-15 0:32 
QuestionTwo questions Pin
Marius Bancila12-Nov-15 11:01
professionalMarius Bancila12-Nov-15 11:01 
AnswerRe: Two questions Pin
Michael Chourdakis12-Nov-15 11:34
mvaMichael Chourdakis12-Nov-15 11:34 
AnswerRe: Two questions Pin
ivan.varga7-Dec-15 2:02
ivan.varga7-Dec-15 2:02 
Questiongreat job Pin
Member 118151624-Nov-15 15:59
Member 118151624-Nov-15 15:59 
QuestionMy vote of 5 Pin
Michael Haephrati1-Nov-15 3:48
professionalMichael Haephrati1-Nov-15 3:48 
QuestionNice option Pin
Sergey Kizyan1-Nov-15 0:49
professionalSergey Kizyan1-Nov-15 0:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.