NtGetNextThread
Returns a handle to the next thread within a target process by walking the kernel thread list.
Prototype
NTSTATUS NtGetNextThread( HANDLE ProcessHandle, HANDLE ThreadHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, ULONG Flags, PHANDLE NewThreadHandle );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Handle to the process whose threads are being enumerated. Requires PROCESS_QUERY_INFORMATION. |
| ThreadHandle | HANDLE | in | Handle to the previously returned thread. NULL to begin enumeration. |
| DesiredAccess | ACCESS_MASK | in | Access mask for the new thread handle, e.g. THREAD_GET_CONTEXT | THREAD_SET_CONTEXT. |
| HandleAttributes | ULONG | in | Handle attribute flags. Usually 0. |
| Flags | ULONG | in | Reserved. Pass 0. |
| NewThreadHandle | PHANDLE | out | Receives the new thread handle. STATUS_NO_MORE_ENTRIES indicates the end of the list. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0xE9 | win10-1507 |
| Win10 1607 | 0xEC | win10-1607 |
| Win10 1703 | 0xEF | win10-1703 |
| Win10 1709 | 0xF0 | win10-1709 |
| Win10 1803 | 0xF1 | win10-1803 |
| Win10 1809 | 0xF2 | win10-1809 |
| Win10 1903 | 0xF3 | win10-1903 |
| Win10 1909 | 0xF3 | win10-1909 |
| Win10 2004 | 0xF8 | win10-2004 |
| Win10 20H2 | 0xF8 | win10-20h2 |
| Win10 21H1 | 0xF8 | win10-21h1 |
| Win10 21H2 | 0xF9 | win10-21h2 |
| Win10 22H2 | 0xF9 | win10-22h2 |
| Win11 21H2 | 0xFE | win11-21h2 |
| Win11 22H2 | 0xFF | win11-22h2 |
| Win11 23H2 | 0xFF | win11-23h2 |
| Win11 24H2 | 0x101 | win11-24h2 |
| Server 2016 | 0xEC | winserver-2016 |
| Server 2019 | 0xF2 | winserver-2019 |
| Server 2022 | 0xFD | winserver-2022 |
| Server 2025 | 0x101 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 01 01 00 00 mov eax, 0x101 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
NtGetNextThread iterates the ThreadListHead of the EPROCESS opened in ProcessHandle. It's the thread-level counterpart to NtGetNextProcess and similarly has no documented Win32 wrapper — `Thread32First`/`Thread32Next` go through Toolhelp's snapshot. Each call returns a fresh kernel handle that the caller must close (except when STATUS_NO_MORE_ENTRIES terminates the walk). The function honours `PROCESS_QUERY_LIMITED_INFORMATION` if the caller doesn't need stronger rights on the process, but the desired *thread* access must be requested up front — the handle isn't easily upgradeable.
Common malware usage
Used together with NtGetNextProcess to enumerate every thread on the system without touching Toolhelp32. The PoolParty injection family (which abuses thread-pool internals) iterates the thread pool's worker threads with this exact API pair to locate a target TP_WORKER thread to hijack. It's also used by stealthy thread-hijacking implants that pick a victim thread, snapshot its context with NtGetContextThread, modify RIP, and resume — all without ever calling OpenThread by TID. Avoiding `Thread32First/Next` is meaningful because Toolhelp creates a `_THSnapshot` section visible to handle-table inspectors.
Detection opportunities
Like NtGetNextProcess, this syscall has low background noise — most legitimate process enumerators use PSAPI or Toolhelp. A spike of NtGetNextThread invocations from a single non-system process is suspicious. Threat-hunting queries that combine ETW Microsoft-Windows-Kernel-Process (thread-create) with the *absence* of expected Toolhelp section creations can surface this pattern. Sysmon Event ID 8 (CreateRemoteThread) will fire on the follow-on hijack only if a new thread is created, not on context-swap hijacks — so the high-value signal is actually NtSetContextThread against a foreign thread immediately following enumeration.
Direct syscall examples
cStealth thread enumeration of a target process
// Walk every thread of a given process without Toolhelp32.
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *PNT_GET_NEXT_THREAD)(HANDLE, HANDLE, ACCESS_MASK, ULONG, ULONG, PHANDLE);
void EnumerateThreads(HANDLE hProcess) {
HMODULE nt = GetModuleHandleA("ntdll.dll");
PNT_GET_NEXT_THREAD pNtGetNextThread = (PNT_GET_NEXT_THREAD)GetProcAddress(nt, "NtGetNextThread");
HANDLE hThread = NULL;
HANDLE hNext = NULL;
while (pNtGetNextThread(hProcess, hThread, THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME,
0, 0, &hNext) == 0) {
if (hThread) CloseHandle(hThread);
hThread = hNext;
// ... inspect TEB / GetThreadContext / hijack candidate ...
}
if (hThread) CloseHandle(hThread);
}asmx64 direct stub (Win11 24H2, SSN 0x101)
NtGetNextThread PROC
mov r10, rcx
mov eax, 101h
syscall
ret
NtGetNextThread ENDPMITRE ATT&CK mappings
Last verified: 2026-05-20