Hey, here is the CLR bootstrapper I wrote a while ago. It supports loading & unloading of loaded assemblies. To use it, you'd inject the clrhost once and then call it's exports. Never unload the clrhost.dll from the target process, that would cause undefined behaviour and would probably cause a crash on reattach.
So if you want to load a new .NET assembly into the target process:
- Check if clrhost.dll is already present in the process, if not: inject it
- Call the Load export along with a pointer to a ClrLoadInfo struct (allocate that in the remote process with VirtualAllocEx)
- To unload just do the same thing as above but pass a ClrUnloadInfo struct
- To check if a .NET assembly is already present, use the IsLoded export with a ClrAssemblyIdentifier struct.
Here are the struct definitions:
Code:
struct ClrLoadInfo {
const wchar_t assemblyPath[MAX_PATH]; // folder to assembly
const wchar_t assemblyName[MAX_PATH]; // filename
const wchar_t mainClassName[100]; // namespace which has "DllMain"-class
const void* loadParameter;
};
struct ClrUnloadInfo {
const wchar_t assemblyName[MAX_PATH];
const void* unloadParameter;
};
struct ClrAssemblyIdentifier {
const wchar_t assemblyName[MAX_PATH];
};
Don't forget to free these after the call, or you will leak memory inside the target process ;)
To compile the clrhost you should create a exports definition file (.def), like this:
Code:
LIBRARY
EXPORTS
Load=LoadInternal
Unload=UnloadInternal
IsLoaded=IsLoadedInternal
And here is the code for my CLR-bootstrapper:
Code:
#include "stdafx.h"
#include <string>
#include <unordered_map>
#include "Mutex.h"
#import "asmbase.tlb" no_namespace named_guids raw_interfaces_only
struct ClrLoadInfo {
const wchar_t assemblyPath[MAX_PATH]; // folder to assembly
const wchar_t assemblyName[MAX_PATH]; // filename
const wchar_t mainClassName[100]; // namespace which has "DllMain"-class
const void* loadParameter;
};
struct ClrUnloadInfo {
const wchar_t assemblyName[MAX_PATH];
const void* unloadParameter;
};
struct ClrAssemblyIdentifier {
const wchar_t assemblyName[MAX_PATH];
};
struct AssemblyInfo {
std::wstring assemblyPath;
std::wstring assemblyName;
std::wstring mainClassName;
CComPtr<mscorlib::_AppDomain> appDomain;
CComPtr<IManagedAssembly> mainClassInstance;
};
HMODULE gModuleHandle = NULL;
CComPtr<ICorRuntimeHost> gRuntimeHost = nullptr;
SysUtils::CriticalSection gThreadLock;
std::unordered_map<std::wstring, AssemblyInfo> gLoadedModules;
CComPtr<ICorRuntimeHost> CreateRuntimeHost() {
CComPtr<ICLRMetaHost> metaHost = nullptr;
HRESULT hr = CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, reinterpret_cast<void**>(&metaHost));
if(FAILED(hr))
throw _com_error(hr);
CComPtr<ICLRRuntimeInfo> runtimeInfo = nullptr;
hr = metaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, reinterpret_cast<void**>(&runtimeInfo));
if(FAILED(hr))
throw _com_error(hr);
CComPtr<ICorRuntimeHost> runtimeHost = nullptr;
hr = runtimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, reinterpret_cast<void**>(&runtimeHost));
if(FAILED(hr))
throw _com_error(hr);
hr = runtimeHost->Start();
if (FAILED(hr))
throw _com_error(hr);
return runtimeHost;
}
DWORD WINAPI LoadInternal(LPVOID lpParam) {
if (lpParam != nullptr) try {
if (!gRuntimeHost)
gRuntimeHost = CreateRuntimeHost();
CComPtr<IUnknown> appDomainSetupUnk = nullptr;
HRESULT hr = gRuntimeHost->CreateDomainSetup(&appDomainSetupUnk);
if (FAILED(hr))
throw _com_error(hr);
CComPtr<mscorlib::IAppDomainSetup> appDomainSetup = nullptr;
hr = appDomainSetupUnk->QueryInterface(&appDomainSetup);
if (FAILED(hr))
throw _com_error(hr);
ClrLoadInfo* loadInfo = reinterpret_cast<ClrLoadInfo*>(lpParam);
appDomainSetup->put_ApplicationBase(CComBSTR(loadInfo->assemblyPath));
appDomainSetup->put_ApplicationName(CComBSTR(loadInfo->assemblyName));
appDomainSetup->put_ShadowCopyFiles(CComBSTR("true")); // loads a shadow copy instead (allows us to "unload")
CComPtr<IUnknown> appDomainUnk = nullptr;
hr = gRuntimeHost->CreateDomainEx(loadInfo->assemblyName, appDomainSetup, nullptr, &appDomainUnk);
if (FAILED(hr))
throw _com_error(hr);
CComPtr<mscorlib::_AppDomain> appDomain = nullptr;
hr = appDomainUnk->QueryInterface(&appDomain);
if (FAILED(hr))
throw _com_error(hr);
std::wstring fullAssemblyPath;
fullAssemblyPath += loadInfo->assemblyPath;
fullAssemblyPath += L'\\';
fullAssemblyPath += loadInfo->assemblyName;
CComPtr<mscorlib::_ObjectHandle> objectHandle;
hr = appDomain->CreateInstanceFrom(CComBSTR(fullAssemblyPath.c_str()), CComBSTR(loadInfo->mainClassName), &objectHandle);
if (FAILED(hr))
throw _com_error(hr);
CComVariant mainClassVariant;
hr = objectHandle->Unwrap(&mainClassVariant);
if (FAILED(hr))
throw _com_error(hr);
if (!mainClassVariant.punkVal)
throw std::exception("AppDomain::CreateInstanceFrom failed. Cannot unwrap object!");
CComPtr<IManagedAssembly> mainClassInstance = nullptr;
hr = mainClassVariant.punkVal->QueryInterface(__uuidof(IManagedAssembly), (void**) &mainClassInstance);
if (FAILED(hr))
throw _com_error(hr);
long returnValue = FALSE;
hr = mainClassInstance->Load((long) loadInfo->loadParameter, &returnValue);
if (FAILED(hr))
throw _com_error(hr);
AssemblyInfo assemblyInfo;
assemblyInfo.appDomain = appDomain;
assemblyInfo.mainClassInstance = mainClassInstance;
assemblyInfo.assemblyPath = loadInfo->assemblyPath;
assemblyInfo.assemblyName = loadInfo->assemblyName;
assemblyInfo.mainClassName = loadInfo->mainClassName;
SysUtils::ScopedLock lockGuard(gThreadLock);
gLoadedModules[assemblyInfo.assemblyName] = assemblyInfo;
return returnValue;
}
catch (_com_error &e) {
MessageBox(nullptr, e.ErrorMessage(), e.Description(), MB_OK | MB_ICONERROR);
}
catch (std::exception &e) {
MessageBoxA(nullptr, e.what(), "Error!", MB_OK | MB_ICONERROR);
}
return FALSE;
}
DWORD WINAPI UnloadInternal(LPVOID lpParam) {
if (lpParam != nullptr) try {
SysUtils::ScopedLock lockGuard(gThreadLock);
ClrUnloadInfo* unloadInfo = reinterpret_cast<ClrUnloadInfo*>(lpParam);
auto assemblyInfoItr = gLoadedModules.find(unloadInfo->assemblyName);
if (assemblyInfoItr == gLoadedModules.end())
return FALSE;
AssemblyInfo& assemblyInfo = assemblyInfoItr->second;
if (assemblyInfo.mainClassInstance != nullptr) {
long returnValue = FALSE;
HRESULT hr = assemblyInfo.mainClassInstance->Unload(
reinterpret_cast<long>(unloadInfo->unloadParameter), &returnValue);
if (FAILED(hr))
throw _com_error(hr);
if (!returnValue) // app returned false on unload
return FALSE;
}
HRESULT hr = gRuntimeHost->UnloadDomain(assemblyInfo.appDomain);
if (FAILED(hr))
throw _com_error(hr);
gLoadedModules.erase(assemblyInfoItr);
return TRUE;
}
catch (_com_error &e) {
MessageBox(nullptr, e.ErrorMessage(), e.Description(), MB_OK | MB_ICONERROR);
}
return FALSE;
}
DWORD WINAPI IsLoadedInternal(LPVOID lpParam) {
if (lpParam != nullptr) {
SysUtils::ScopedLock lockGuard(gThreadLock);
ClrAssemblyIdentifier* identifier = reinterpret_cast<ClrAssemblyIdentifier*>(lpParam);
auto assemblyInfoItr = gLoadedModules.find(identifier->assemblyName);
return assemblyInfoItr != gLoadedModules.end();
}
return FALSE;
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
gModuleHandle = hModule;
DisableThreadLibraryCalls(hModule);
break;
case DLL_PROCESS_DETACH:
if (gRuntimeHost != nullptr)
gRuntimeHost->Stop();
break;
}
return TRUE;
}
It creates a new AppDomain for every assembly so you won't run into problems. That way you can also unload assemblies (would be impossible if you load them into the default AppDomain).
Greets