Local Thread Enumeration
Exploring Thread Hijacking: Local Thread Enumeration
Thread hijacking is one of the most used techniques in malware development that permits code execution in an existing running thread. Conventionally, it would require a newly created thread via CreateThread
and edit its context. We can employ another approach wherein enumerating and hijacking are done to existing threads using CreateToolhelp32Snapshot
.
Thread Enumeration Explained
CreateToolhelp32Snapshot is a WinAPI function that creates a snapshot of all running threads and processes on the system. Utilizing the parameter TH32CS_SNAPTHREAD
, it returns, for every running thread-a THREADENTRY32
structure that includes, among other things, the identifier of the currently running thread and the process identifier of the owner process.
1
2
3
4
5
6
7
8
9
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;
Determining Thread Ownership
To determine which threads are part of a given process, simply compare the process identifier with the th32OwnerProcessID
member of the THREADENTRY32
structure. If the PIDs match, the thread is part of the target process.
Essential WinAPIs for Thread Enumeration
Following are a few critical WinAPIs necessary for thread enumeration:
- CreateToolhelp32Snapshot : This API captures the snapshot of running threads using the
TH32CS_SNAPTHREAD
flag. Thread32First Retrieves information about the first thread in the snapshot. - Thread32Next : Retrieves information about subsequent threads in the snapshot.
- OpenThread : Opens a handle to a target thread by its ID.
- GetCurrentProcessId : Retrieves the PID of the local process; this is needed to identify which threads are created by this process.
Understanding Worker Threads
Even without explicitly creating threads using CreateThread
, the Windows operating system generates worker threads within processes. These threads, such as those running ntdll.dll!EtwNotificationRegister+0x2d0
, are valid targets for hijacking. They are created by the OS to handle specific functions, like Event Tracing for Windows.
Implementing Thread Enumeration
The GetLocalThreadHandle
function enumerates threads step by step. It takes three arguments: the ID of the main thread, a pointer to a DWORD for a hijackable thread ID, and a pointer to a HANDLE for 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
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;
}
Hijacking a Local Thread
Once a valid handle to the target thread is obtained, it can be passed to the HijackThread
function. The latter now calls SuspendThread
to suspend the thread and updates the RIP
register to point to the payload base address using GetThreadContext
and SetThreadContext
and then resumes the thread with ResumeThread
.
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 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;
}