This is the fourth part of the DLL Injection series. For your convenience you can find other parts in the table of contents in Part 1 – Registry
Las time we saw how to inject DLL into target process using remote threads. However, throughout this whole series we were injecting only native DLL, e.g., libraries compiled to native code. Today we will inject managed library.
Table of Contents
Test program
We will use the following program to demonstrate the library injection:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; using System.Threading; namespace ExceptionThrower { class Program { static void Main(string[] args) { while (true) { try { Console.WriteLine("Throwing exception: " + DateTime.Now); throw new Exception(); } catch (Exception e) { Console.WriteLine("Handling exception in catch"); } Thread.Sleep(2000); } } } } |
We throw exception every two seconds and immediately handle it with catch
clause. We will use this program to demonstrate the DLL injection. We will inject a library which will handle first chance exception and print to console so we will indeed see that our code is executing in the same application domain as the code above. This is only a sample program and in general we do not require our target process to be .NET application, we will use this only for demonstration purposes.
Executing .NET code
First, we need to obtain a handle for .NET framework loaded into the process. You can load .NET into any native application using CLRCreateInstance function. You can have only one .NET framework loaded into the process. We will use this function to obtain a handle for already loaded framework (since our test application is written in C#). Once it’s loaded we can execute any managed code using ExecuteInDefaultAppDomain method.
The plan looks easy at first sight. We simply inject native DLL, next in DLLMain
we get a handle for .NET framework, and finally we execute a piece of code. It looks good, however, we need to very careful when executing code in DLLMain
method because of loader lock. Loading any library in this method results in a deadlock. And we probably need to load libraries in order to load .NET framework.
However, we can use very similar approach. Basically, we will create two remote threads: first one will load our native dll, the second one will load .NET and execute managed code. Let’s see some code.
Native DLL
We start with the native library. First, the code for loading .NET runtime:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
__declspec(dllexport) resultESULT ImplantDotNetAssembly(_In_ LPCTSTR lpCommand) { resultESULT result; ICLRMetaHost *metaHost = NULL; ICLRRuntimeInfo *runtimeInfo = NULL; ICLRRuntimeHost *runtimeHost = NULL; // Load .NET result = CLRCreateInstance(CLSID_CLRMetaHost, IID_PPV_ARGS(&metaHost)); wprintf_s((L"CLR Instance: " + std::to_wstring(result == S_OK ? 1 : 0) + L"\n").c_str()); // Replace .NET version with the one you want to load (or which is already loaded) result = metaHost->GetRuntime(L"v4.0.30319", IID_PPV_ARGS(&runtimeInfo)); wprintf_s((L"Runtime: " + std::to_wstring(result == S_OK ? 1 : 0) + L"\n").c_str()); result = runtimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_PPV_ARGS(&runtimeHost)); wprintf_s((L"Runtime info: " + std::to_wstring(result == S_OK ? 1 : 0) + L"\n").c_str()); // Start runtime result = runtimeHost->Start(); wprintf_s((L"Runtime started: " + std::to_wstring(result == S_OK ? 1 : 0) + L"\n").c_str()); // Execute managed assembly DWORD returnValue; result = runtimeHost->ExecuteInDefaultAppDomain( "Path do managed DLL", "Type containing method to run", "Method to run", "Argument to pass", &returnValue); wprintf_s((L"Function executed: " + std::to_wstring(returnValue)).c_str()); // Unload .NET result = runtimeHost->Stop(); // free resources metaHost->Release(); runtimeInfo->Release(); runtimeHost->Release(); return result; } |
We will inject this library and execute the ImplantDotNetAssembly
method. You can use the code from previous part to inject the library. Now we need to get the address of ImplantDotNetAssembly
method in the target address space. See the code below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// Load library into this process HMODULE libraryAddress = LoadLibrary("Native library.dll"); // Get address of function to invoke void* functionAddress = GetProcAddress(libraryAddress , "ImplantDotNetAssembly"); // Compute the distance between the base address and the function to invoke DWORD_PTR offset = (DWORD_PTR)functionAddress - (DWORD_PTR)libraryAddress ; // Unload library FreeLibrary(libraryAddress ); // return the offset to the function return offset; |
We load native library into our process and calculate the offset of the function. We assume that the offset will be the same in the target process. Now we need to get the handle for library in the target process:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
MODULEENTRY32 moduleEntry; HANDLE snapshot = INVALID_HANDLE_VALUE; // Get snapshot of all modules in the remote process moduleEntry.dwSize = sizeof(MODULEENTRY32); snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, processId); if (!Module32First(snapshot, &moduleEntry)) { CloseHandle(snapshot); return 0; } // Find the module which we look for while (wcscmp(moduleEntry.szModule, "Native library.dll") != 0 && Module32Next(snapshot, &moduleEntry)); // close the handle CloseHandle(snapshot); // check if module handle was found and return it if (wcscmp(moduleEntry.szModule, "Native library.dll") == 0) return (DWORD_PTR)moduleEntry.modBaseAddr; return 0; |
Finally, we add offset to the module base address and use it as a function address for remote thread. We create remote thread as usually.
Injected managed code
All we need to do is write a managed library which will handle first chance exception. We can use the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
using System; namespace ExceptionHandler { public class Handler { public static int AddHandler(string arg) { AppDomain.CurrentDomain.FirstChanceException += (sender, args) => Console.WriteLine("First Chance exception!"); return 1; } } } |
As we can see, our function accepts one string
argument and returns int
so it can be executed. We simply call this function from our native library and we should see that the lambda is called every time an exception is thrown.
Summary
As we can see, we are able to inject both native and managed libraries into other processes. We are also able to choose application domain for our injected code thanks to API for loading .NET into a process. In fact, you can configure many more options when loading framework: you can implement custom memory manager or override routines for creating .NET threads. However, today we only inject managed code.