Table of Contents:
- DLL Injection: Background & DLL Proxying (1/5)
- DLL Injection: Windows Hooks (2/5)
- DLL Injection: Remote Threads (3/5)
- DLL Injection: Thread Context Hijacking (4/5)
- DLL Injection: Manual Mapping (5/5)
Another common technique to perform DLL injection is to use the CreateRemoteThreadEx function. As its name suggests, this function creates a thread that begins execution in the address space of another process. The CreateRemoteThreadEx function has the following prototype:
HANDLE WINAPI CreateRemoteThreadEx(HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
LPDWORD lpThreadId);
This function looks daunting at first, but most of the parameters are optional. For the purposes of DLL injection, where you only just care about getting the thread created and started – and not worrying about things like the thread’s various customizations – these optional parameters will not be used. The only parameters that are important here are the process handle (hProcess), thread start address (lpStartAddress), and the parameter for the start address (lpParameter).
Using CreateRemoteThreadEx to perform DLL injection becomes intuitive once you see that you can start this thread at any address, and with a parameter of your choice. Given control over these two parameters, what would happen if you set the thread start address to the address of the LoadLibraryA function, and passed in a pointer to the library path as the parameter? Your remote thread would be performing a LoadLibraryA call — which would load your DLL — in your target processes address space.
Getting the target process handle
Having understood how the DLL injection will work, it is time to get started on the implementation. There are three things needed: the handle to the target process, the address of the LoadLibraryA function, and a pointer in the target process to a string with the DLL path. Obtaining the process handle is done with the OpenProcess call.
HANDLE GetTargetProcessHandle(const DWORD processId) {
const auto processHandle{ OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE,
false, processId) };
if (processHandle == nullptr) {
PrintErrorAndExit("OpenProcess");
}
return processHandle;
}
Opening a process handle to the target process.
The handle will need the PROCESS_CREATE_THREAD, PROCESS_VM_OPERATION, and PROCESS_VM_WRITE access rights. The first permission is needed for CreateRemoteThreadEx to create the remote thread. The latter two permissions are needed to write in the path to the DLL into the target processes address space.
Getting the address of LoadLibraryA
Having obtained a handle to the process, the next step is to find the address of the LoadLibraryA function. This function is found in kernel32.dll, which is a system DLL that on Windows has the same load address in all processes. This means that you can get the address of LoadLibraryA in your process, and trust that it will be the same address in the target process. The implementation for this is shown below:
void* GetLoadLibraryAddress() {
return GetProcAddress(GetModuleHandleA(
"kernel32.dll"), "LoadLibraryA");
}
Obtaining the address of the LoadLibraryA function.
It suffices to use the address returned from this function as the thread start address in CreateRemoteThreadEx. However, I would like to also present an implementation that is a bit more generic and will work to retrieve the address of a function in any DLL without requiring a DLL to be loaded at the same address in every process.
void* GetRemoteModuleFunctionAddress(const std::string moduleName,
const std::string functionName, const DWORD processId) {
void* localModuleBaseAddress{ GetModuleHandleA(moduleName.c_str()) };
if (localModuleBaseAddress == nullptr) {
localModuleBaseAddress = LoadLibraryA(moduleName.c_str());
if (localModuleBaseAddress == nullptr) {
PrintErrorAndExit("LoadLibraryA");
}
}
const void* const localFunctionAddress{
GetProcAddress(static_cast<HMODULE>(localModuleBaseAddress),
functionName.c_str()) };
if (localFunctionAddress == nullptr) {
PrintErrorAndExit("GetProcAddress");
}
const auto functionOffset{ PointerToRva(
localFunctionAddress, localModuleBaseAddress) };
const auto snapshotHandle{ CreateToolhelp32Snapshot(
TH32CS_SNAPMODULE, processId) };
if (snapshotHandle == INVALID_HANDLE_VALUE) {
PrintErrorAndExit("CreateToolhelp32Snapshot");
}
MODULEENTRY32 module {
.dwSize = sizeof(MODULEENTRY32)
};
if (!Module32First(snapshotHandle, &module)) {
PrintErrorAndExit("Module32First");
}
do {
auto currentModuleName{ std::string{module.szModule} };
std::transform(currentModuleName.begin(), currentModuleName.end(),
currentModuleName.begin(),
[](unsigned char letter) { return std::tolower(letter); });
if (currentModuleName == moduleName) {
return reinterpret_cast<void*>(module.modBaseAddr + functionOffset);
}
} while (Module32Next(snapshotHandle, &module));
return nullptr;
}
An implementation that can get the address of an exported function from any DLL.
This function works by first getting the base address of the DLL in your process; either obtaining it with GetModuleHandleA if the DLL is already loaded, or explicitly loading the DLL with LoadLibraryA. After obtaining the base address, the pointer to the target function is found, and the offset between the DLL base address and the function address is calculated. While the base addresses of DLLs may differ across processes, the offset between the base address and the address of the target function will be the same.
Once the offset is known, the next step is to find the DLL base address in the target process. This is accomplished by using CreateToolhelp32Snapshot to create a snapshot of the target processes loaded modules. The loaded modules can then be iterated over with the Module32First and Module32Next functions. These calls populate a MODULEENTRY32 structure with information about the current module, including a modBaseAddr field that indicates the DLL’s base address in the target process. While iterating, if the current module matches the module that you are looking for, return the module’s base address plus the target function offset. This will be the absolute address in the target processes virtual address space of your target function.
Writing the injected DLL path
With a process handle, and now the address of the LoadLibraryA function, there is one last value that needs to be obtained: a pointer to the path of the DLL that will be injected. The target process will not know anything about your DLL, so you will definitely not find a pointer to its path anywhere in the target processes address space. That means that you need to write in the path to your DLL. Fortunately, the combination of the VirtualAllocEx and WriteProcessMemory functions can accomplish this task.
template <typename T>
void* WriteBytesToTargetProcess(const HANDLE processHandle,
const std::span<T> bytes, bool makeExecutable = false) {
static_assert(sizeof(T) == sizeof(uint8_t), "Only bytes can be written.");
const auto remoteBytesAddress{ VirtualAllocEx(processHandle, nullptr,
bytes.size(), MEM_RESERVE | MEM_COMMIT,
makeExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE) };
if (remoteBytesAddress == nullptr) {
PrintErrorAndExit("VirtualAllocEx");
}
size_t bytesWritten{};
const auto result{ WriteProcessMemory(processHandle, remoteBytesAddress,
bytes.data(), bytes.size(), &bytesWritten) };
if (result == 0) {
PrintErrorAndExit("WriteProcessMemory");
}
return remoteBytesAddress;
}
Writing in a span of bytes to a process.
The VirtualAllocEx function is used to allocate a block of memory in the target process. The value returned from VirtualAllocEx will contain a pointer in the target processes address space of where the memory was allocated. This pointer can then be passed to WriteProcessMemory to write in a span of bytes, which will be the file path that the DLL to be injected is found at.
The file path can be passed in as a hardcoded value, or, as a more flexible option, can be retrieved at runtime.
std::string GetInjectedDllPath(const std::string& moduleName) {
char imageName[MAX_PATH]{};
DWORD bytesWritten{ MAX_PATH };
auto result{ QueryFullProcessImageNameA(GetCurrentProcess(),
0, imageName, &bytesWritten) };
if (result == 0) {
PrintErrorAndExit("QueryFullProcessImageNameA");
}
std::string currentDirectoryPath{ imageName, bytesWritten };
const auto fullModulePath{ currentDirectoryPath.substr(
0, currentDirectoryPath.find_last_of('\\') + 1)
+ moduleName };
return fullModulePath;
}
Obtaining the absolute path of the DLL that will be injected.
The GetInjectedDllPath function obtains the absolute path of the current process, which will be loading the DLL. The process name is removed from this path, and the DLL name is put in its place. This function assumes that the loader process and the DLL that is being injected are in the same directory, but that limitation still provides more flexibility over a hardcoded path.
Creating the remote thread
The CreateRemoteThreadEx function can finally be called since the parameters for it have all been retrieved.
void InjectWithRemoteThread(const DWORD processId, std::string& fullModulePath) {
const auto processHandle{ GetTargetProcessHandle(processId) };
const auto remoteStringAddress{ WriteBytesToTargetProcess<char>(
processHandle, fullModulePath) };
const auto* const loadLibraryAddress{ GetRemoteModuleFunctionAddress(
"kernel32.dll", "LoadLibraryA", processId) };
const auto threadHandle{ CreateRemoteThreadEx(processHandle, nullptr, 0,
reinterpret_cast<LPTHREAD_START_ROUTINE>(loadLibraryAddress),
remoteStringAddress, 0, nullptr, nullptr) };
if (threadHandle == nullptr) {
PrintErrorAndExit("CreateRemoteThread");
}
CloseHandle(processHandle);
}
int main(int argc, char* argv[]) {
auto fullModulePath{ GetInjectedDllPath("Ch10_GenericDll.dll") };
const auto processId{ GetTargetProcessAndThreadId(
"Untitled - Notepad").first };
InjectWithRemoteThread(processId, fullModulePath);
return 0;
}
A loader that injects a DLL with CreateRemoteThreadEx.
The loader begins by opening a handle to the target process. Next, the full path to the DLL that is to be injected is written into the target process. Finally, the address of the LoadLibraryA function is found. CreateRemoteThreadEx is then called, where it is told to create a new thread in the target process that begins its execution at LoadLibraryA and has the address of the full path to the DLL as its argument.
Running the demo
Note: If you are using the new UWP Notepad that is in the latest Windows version, you will need to downgrade to the classic versionfor the demo to work.
The CreateRemoteThread project provides the full implementation that was presented in this section. To test this locally, build both the GenericDll project and the CreateRemoteThread loader project. After a successful build, launch Notepad and then the loader application. You should observe a message box popping up that says “DLL Injected!”, as shown below. Do not dismiss this message box yet.
To see GenericDll.dll in the notepad.exe address space, open up Process Hacker, find the notepad.exe process, and navigate to the Modules tab. Verify that GenericDll.dll has been loaded; this should be self evident though if the message box has come up. Next, click on the Threads tab. There will be a thread whose start address is listed as kernel32.dll!LoadLibraryA.
This is the thread that was created by the CreateRemoteThreadEx call. If you dismiss the message box, you will see this thread exit. The presence of GenericDll.dll in the notepad.exe address space, and a new thread executing at LoadLibraryA show that the DLL was successfully injected and that the message box is executing in the context of the Notepad process.