What is Process Injection?
According to MITRE ATT&CK T1055 - Process injection is a method of executing arbitrary code in the address space of a separate live process. Running code in the context of another process may allow access to the process's memory, system/network resources, and possibly elevated privileges. Execution via process injection may also evade detection from security products since the execution is masked under a legitimate process.
What’s below?
An overview of process injection written in C and the Windows APIs used to do the following:
Find the target process
Allocate memory within the target process
Write to allocated memory in the target process
Execute the injected DLL from the target process
How is this implemented?
The source file of remote-injection.exe used for this demonstration can be broken down into a two part sequence:
Retrieve a handle to the target process
Inject malicious DLL into the target process
The aptly named DLL, bad.dll to be injected contains simple code to create a window popup.
What Windows APIs are used?
The following APIs are used by remote-injection.exe to achieve its process injection:
DLL Injection Function:
GetProcAddress - Retrieves the address of an exported function (also known as a procedure) or variable from the specified dynamic-link library
GetModuleHandle - Returns a module handle for the specified module if the file has been mapped into the address space of the calling process.
VirtualAllocEx - Reserves, commits, or changes the state of a region of memory within the virtual address space of a specified process. The function initializes the memory it allocates to zero.
WriteProcessMemory - Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or the operation fails.
Retrieve Handle Function:
CreateRemoteThread - Creates a thread that runs in the virtual address space of another process.
CreateToolhelp32Snapshot - Takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.
Process32First - Retrieves information about the first process encountered in a system snapshot.
Proces32Next - Retrieves information about the next process recorded in a system snapshot.
Retrieving The Target Process Handle
Below is an outline of the function GetHandle which is used to retrieve the target process handle. All peripheral code relating to error handling and other minutiae will be deprecated from the overview.
First we need to define the size of the PROCESSENTRY32
structure to be used which is defined by Microsoft as: An entry from a list of the processes residing in the system address space when a snapshot was taken.
Microsoft specified that the dwSize
parameter of this structure needs to be set to sizeof(PROCESSENTRY32)
before we use the API Process32First
.
// Structure Syntax
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH];
} PROCESSENTRY32;
// Actual use of the structure
PROCESS32ENTRY Proc = {
.dwSize = sizeof(PROCESSENTRY32)
}
Then we need to take a snapshot of all the running processes on the host:
// Function Syntax
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
// Actual use of the function
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
The first argument TH32CS_SNAPPROCESS
(dwFlags
) ensures that all processes on the system are included in the snapshot. The second parameter (th32ProcessID
) is only taken into account when certain flags are used and can be set to NULL
for our purpose.
We can now read the first process within the snapshot that was taken using Process32First
:
// Function Syntax
BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);
// Actual use of the function
Process32First(hSnapShot, &Proc)
The first argument is a handle to the snapshot we just took and the second parameter is an address pointer to the PROCESSENTRY32
structure which contains process information such as PIDs and file names.
Now that the first process was iterated we can now use Process32Next for all remaining processes in the snapshot:
// Function Syntax
BOOL Process32Next(
[in] HANDLE hSnapshot,
[out] LPPROCESSENTRY32 lppe
);
// Actual use of the function
Process32First(hSnapShot, &Proc)
Once the matching process name is found within the list, the function will break.
Below is pseudocode of the function flow:
BOOL GetHandle(Process Name, Process ID, Handle To Process) {
Snapshot = CreateToolhelp32Snapshot
if (Snapshot is unsucessful) {
Error handling;
}
View first process here;
if (Process32First is unsucessful) {
Error handling;
}
do { Process name comparisons and break if a match is found}
while { There are processes still in the list }
}
Injecting the DLL Into The Target Process
Now we will do the same for the function responsible for the actual process injection.
First we need to retrieve the base address for the LoadLibrary WinAPI:
// Function syntax
FARPROC GetProcAddress(
[in] HMODULE hModule,
[in] LPCSTR lpProcName
);
HMODULE GetModuleHandle (LPCTSTR lpModuleName);
// Actual use of the function(s)
pLoadLibraryW = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "LoadLibraryW");
The above nesting of functions is self-explanatory in its mission to retrieve the address of the LoadLibraryW
function address located within kernel32.dll
. This result is then held within a variable signifying its contents.
Then we need to allocate space within the targeted process to hold our DLL name:
// Function Syntax
LPVOID VirtualAllocEx(
[in] HANDLE hProcess,
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
// Actual use of the function
pAddress = VirtualAllocEx(hProcess, NULL, dwWriteSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
The VirtualAllocEx
function returns the base address of the allocated region into the pAddress variable. The first parameter specified out target process. The second parameter can be left NULL
as it is used for specifying a starting address for the allocated memory which we don’t need. The third parameter represents the size of the region to allocate. The fourth parameter MEM_COMMIT | MEM_RESERVE
is used to reserve and commit the memory in on step. The last parameter sets the allocated memory permissions to PAGE_READWRITE
which is requited for dynamic memory.
Quick Memory Allocation Type Explanation:
Reserved - Amount of memory reserved by the memory manager
Committed - Reserved memory is use which becomes valid pages in physical memory once accessed
Once we have allocated the necessary memory we can write the DLL name to it:
// Function Syntax
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
// Actual use of the function
WriteProcessMemory(hProcess, pAddress, Dll, dwWriteSize, &lpWrittenBytes)
The first and second parameter specify the target process and the address of the allocated memory, respectively. The third parameter specifies the full DLL path that’s going to be injected. The fourth represents the number of bytes to write. Lastly, we have the parameter which should be a pointer to a variable that receives the number of bytes transferred into the specified process.
Now we can create the remote thread for LoadLibrary:
// Function Syntax
HANDLE CreateRemoteThread(
[in] HANDLE hProcess,
[in] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in] LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out] LPDWORD lpThreadId
);
// Actual use of the function
hThread = CreateRemoteThread(hProcess, NULL, NULL, pLoadLibraryW, pAddress, NULL, NULL);
The only parameters specified in this function call are the handle to the target process, the pointer to the LoadLibraryW function and the pointer to the address of the allocated memory.
Below is pseudocode of the function flow:
BOOL InjectDll {
Initlization of needed variables such as pAddress here;
pLoadLibrary = GetProcAddress(GetModuleHandle))
if (GetProcessAddress is unsucessful) {
Error handling;
}
pAddress = VirtualAllocEx
if (VirtualAllocEx is unsucessful) {
Error handling;
}
Write Process Memory here;
if (WriteProcessMemory is unsucessful) {
Error handling;
}
hThread = CreateRemoteThread
if (CreateRemoteThread is unsucessful) {
Error handling;
}
}
Demonstration
- Memory Allocation
- Writing into memory
- Running the payload