Running managed executables inside an unmanaged executable in C++

rab00t 1 Reputation point
2020-12-31T12:07:33.387+00:00

For the past few days, I've been trying to run a .NET executable inside an unmanaged executable made in C++.

I made a simple MessageBox program to test if my code worked, and indeed the application ran fine:

using System;
using System.Windows.Forms;

namespace Test2Lol
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            MessageBox.Show("I am alive!", "Hello!", 0, MessageBoxIcon.Information);
        }
    }
}

The application works without throwing any exceptions, here's the output log of the unmanaged application running the managed one. The latter's shellcode is inside the unmanaged application.

[main][116] Phase 3: Loading data...
[MDLL_OpenProcess][75] Process opened sucessfully.
[MDLL_VirtualAlloc][40] Sucessfully allocated memory.
[DLL_RunImageDotNet][662] Loading .NET application: BaseAddress 0x04980000, Size: 6656.
[DLL_RunImageDotNet][671] Done 6655 cycles.
[DLL_RunImageDotNet][688] [+] ICLRMetaHost: CLRCreateInstance(...) succeeded
[DLL_RunImageDotNet][701] [+] ICLRMetaHostPolicy: CLRCreateInstance(...) succeeded
[DLL_RunImageDotNet][714] [+] ICLRDebugging: CLRCreateInstance(...) succeeded
[DLL_RunImageDotNet][729] [+] pMetaHost->GetRuntime(...) succeeded
[DLL_RunImageDotNet][744] [+] pRuntimeInfo->IsLoadable(...) succeeded
[DLL_RunImageDotNet][759] [+] pRuntimeInfo->GetInterface(...) succeeded
[DLL_RunImageDotNet][772] [+] pRuntimeHost->Start() succeeded
[DLL_RunImageDotNet][785] [+] pRuntimeHost->GetDefaultDomain(...) succeeded
[DLL_RunImageDotNet][800] [+] pAppDomainThunk->QueryInterface(...) succeeded
[DLL_RunImageDotNet][823] [+] SafeArrayAccessData(...) succeeded
[DLL_RunImageDotNet][836] [+] SafeArrayUnaccessData(...) succeeded
[DLL_RunImageDotNet][838] pSafeArray: 00ABD488, cDims: 1, fFeatures: 128, cbElements: 1, cLocks: 0, pvData: 00AF8C88, cElements 6656, lLBound 0.
[DLL_RunImageDotNet][849] [+] pDefaultAppDomain->Load_3(...) succeeded
[DLL_RunImageDotNet][862] [+] pAssembly->get_EntryPoint(...) succeeded

eNBpnAy.png

So, where is the problem you guys might ask? Well, while it works for that very simple messagebox program, it does not work whenever I attempt to load a different application, crashing at Load_3() with the following output log:

[main][116] Phase 3: Loading data...
[MDLL_OpenProcess][75] Process opened sucessfully.
[MDLL_VirtualAlloc][40] Sucessfully allocated memory.
[DLL_RunImageDotNet][662] Loading .NET application: BaseAddress 0x05000000, Size: 514048.
[DLL_RunImageDotNet][671] Done 514047 cycles.
[DLL_RunImageDotNet][688] [+] ICLRMetaHost: CLRCreateInstance(...) succeeded
[DLL_RunImageDotNet][701] [+] ICLRMetaHostPolicy: CLRCreateInstance(...) succeeded
[DLL_RunImageDotNet][714] [+] ICLRDebugging: CLRCreateInstance(...) succeeded
[DLL_RunImageDotNet][729] [+] pMetaHost->GetRuntime(...) succeeded
[DLL_RunImageDotNet][744] [+] pRuntimeInfo->IsLoadable(...) succeeded
[DLL_RunImageDotNet][759] [+] pRuntimeInfo->GetInterface(...) succeeded
[DLL_RunImageDotNet][772] [+] pRuntimeHost->Start() succeeded
[DLL_RunImageDotNet][785] [+] pRuntimeHost->GetDefaultDomain(...) succeeded
[DLL_RunImageDotNet][800] [+] pAppDomainThunk->QueryInterface(...) succeeded
[DLL_RunImageDotNet][823] [+] SafeArrayAccessData(...) succeeded
[DLL_RunImageDotNet][836] [+] SafeArrayUnaccessData(...) succeeded
[DLL_RunImageDotNet][838] pSafeArray: 0137E298, cDims: 1, fFeatures: 128, cbElements: 1, cLocks: 0, pvData: 0137E9D0, cElements 514048, lLBound 0.
code: 0xE06D7363 (0x00000001)
addr: 75D74662

As you guys might notice (and will notice after reading the code below): execution halts at Load_3() throwing an error with code 0xE06D7363 (0x0000001) (E_UNKNOWN?), after I loaded the executable shellcode inside the SafeArray. I also tried with a smaller .NET application (46KB in size) and still no luck. I'm quite lost on why is this happening, by the way here is the code that executes the .NET shellcode. Load_3() call is at line 189.

/* Allocating space for the shellcode in memory */

    BYTE* AssemblyBuffer = (BYTE*)MDLL_VirtualAlloc(Syscalls, 0, 0, Size, FULL_COMMIT, PAGE_EXECUTE_READWRITE);

    BYTE* FilePtr = BaseAddress;
    BYTE* BufferPtr = AssemblyBuffer;

    int cy = 0;

    DLL_DEBUG("Loading .NET application: BaseAddress 0x%08X, Size: %d.\n", BaseAddress, Size);

    /* Load the shellcode in the allocated memory space */
    for (int i = 0; i < Size; i++)
    {
        BufferPtr[i] = *FilePtr;
        FilePtr++;
        cy = i;
    }

    DLL_DEBUG("Done %d cycles.\n", cy);

    ICLRMetaHost* pMetaHost = NULL;

    HRESULT hr;

    /* Get ICLRMetaHost instance */

    hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&pMetaHost);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] ICLRMetaHost: CLRCreateInstance(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] ICLRMetaHost: CLRCreateInstance(...) succeeded\n");

    ICLRMetaHostPolicy* pMetaHostPolicy = NULL;

    hr = CLRCreateInstance(CLSID_CLRMetaHostPolicy, IID_ICLRMetaHostPolicy, (VOID**)&pMetaHostPolicy);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] ICLRMetaHostPolicy: CLRCreateInstance(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] ICLRMetaHostPolicy: CLRCreateInstance(...) succeeded\n");

    ICLRDebugging* pCLRDebugging = NULL;

    hr = CLRCreateInstance(CLSID_CLRDebugging, IID_ICLRDebugging, (VOID**)&pCLRDebugging);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] ICLRDebugging: CLRCreateInstance(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] ICLRDebugging: CLRCreateInstance(...) succeeded\n");

    ICLRRuntimeInfo* pRuntimeInfo = NULL;

    /* Get ICLRRuntimeInfo instance */

    hr = pMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&pRuntimeInfo);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pMetaHost->GetRuntime(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pMetaHost->GetRuntime(...) succeeded\n");

    BOOL bLoadable;

    /* Check if the specified runtime can be loaded */

    hr = pRuntimeInfo->IsLoadable(&bLoadable);

    if(FAILED(hr) || !bLoadable)
    {
        DLL_DEBUG("[!] pRuntimeInfo->IsLoadable(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pRuntimeInfo->IsLoadable(...) succeeded\n");

    ICorRuntimeHost* pRuntimeHost = NULL;

    /* Get ICorRuntimeHost instance */

    hr = pRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&pRuntimeHost);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pRuntimeInfo->GetInterface(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pRuntimeInfo->GetInterface(...) succeeded\n");

    /* Start the CLR */

    hr = pRuntimeHost->Start();

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pRuntimeHost->Start() failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pRuntimeHost->Start() succeeded\n");

    IUnknownPtr pAppDomainThunk = NULL;

    hr = pRuntimeHost->GetDefaultDomain(&pAppDomainThunk);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pRuntimeHost->GetDefaultDomain(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pRuntimeHost->GetDefaultDomain(...) succeeded\n");

    _AppDomainPtr pDefaultAppDomain = NULL;

    /* Equivalent of System.AppDomain.CurrentDomain in C# */

    hr = pAppDomainThunk->QueryInterface(__uuidof(_AppDomain), (VOID**) &pDefaultAppDomain);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pAppDomainThunk->QueryInterface(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pAppDomainThunk->QueryInterface(...) succeeded\n");

    _AssemblyPtr pAssembly = NULL;

    SAFEARRAYBOUND rgsabound[1];

    rgsabound[0].cElements = Size;

    rgsabound[0].lLbound   = 0;

    SAFEARRAY* pSafeArray  = SafeArrayCreate(VT_UI1, 1, rgsabound);

    void* pvData = NULL;

    hr = SafeArrayAccessData(pSafeArray, &pvData);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] SafeArrayAccessData(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] SafeArrayAccessData(...) succeeded\n");

    memcpy(pvData, AssemblyBuffer, Size);

    hr = SafeArrayUnaccessData(pSafeArray);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] SafeArrayUnaccessData(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] SafeArrayUnaccessData(...) succeeded\n");

    DLL_DEBUG("pSafeArray: %p, cDims: %hu, fFeatures: %hu, cbElements: %lu, cLocks: %lu, pvData: %p, cElements %lu, lLBound %ld.\n", pSafeArray, pSafeArray->cDims, pSafeArray->fFeatures, pSafeArray->cbElements, pSafeArray->cLocks, pSafeArray->pvData, pSafeArray->rgsabound->cElements, pSafeArray->rgsabound->lLbound);

    hr = pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pDefaultAppDomain->Load_3(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pDefaultAppDomain->Load_3(...) succeeded\n");

    _MethodInfoPtr pMethodInfo = NULL;

    hr = pAssembly->get_EntryPoint(&pMethodInfo);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pAssembly->get_EntryPoint(...) failed\n");

        return 0;
    }

    DLL_DEBUG("[+] pAssembly->get_EntryPoint(...) succeeded\n");

    VARIANT retVal;
    ZeroMemory(&retVal, sizeof(VARIANT));

    VARIANT obj;
    ZeroMemory(&obj, sizeof(VARIANT));
    obj.vt = VT_NULL;

    SAFEARRAY *psaStaticMethodArgs = SafeArrayCreateVector(VT_VARIANT, 0, 0);

    hr = pMethodInfo->Invoke_3(obj, psaStaticMethodArgs, &retVal);

    if(FAILED(hr))
    {
        DLL_DEBUG("[!] pMethodInfo->Invoke_3(...) failed, hr = %X\n", hr);

        return 0;
    }

    DLL_DEBUG("[+] pMethodInfo->Invoke_3(...) succeeded\n");

    return 1;

I have no idea why Load_3() fails whenever I try to load a different executable than normal. My theories are:

  1. Load_3() crashes if I try to load files bigger than a certain amount -> not true because I tried with smaller applications and still no luck, also I saw blogs of people loading shellcode even bigger than 42KB using Load_3() successfully.
  2. The SafeArray is being created in a wrong way -> I double-checked the output with the working and not-working application and the SafeArray looks the same
  3. I am loading the data incorrectly -> Tried tons of different methods, none of them worked.
  4. Switching to /SUBSYSTEM:WINDOWS in the linker options didn't solve anything.

I'm quite out of ideas on why such a thing happens with different executables, and sadly I couldn't find any documentation regarding Load_3() whatsoever.

What may be causing Load_3() to fail with different executables from the one I created?

Thanks to everyone willing to help me out!

C++
C++
A high-level, general-purpose programming language, created as an extension of the C programming language, that has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation.
3,527 questions
.NET Runtime
.NET Runtime
.NET: Microsoft Technologies based on the .NET software framework.Runtime: An environment required to run apps that aren't compiled to machine language.
1,119 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Castorix31 81,726 Reputation points
    2021-01-01T13:19:04.317+00:00

    I did a quick test with "draft code" (all returned hr should be tested...) and it seems to work with several C# exe that I tested (without parameters) :
    I cannot post C++ code, so a link of the test code : ICLRMetaHost test

    1 person found this answer helpful.