NtGetNextProcess
Walks the kernel's process list and returns a handle to the next process after a given one.
Prototype
NTSTATUS NtGetNextProcess( HANDLE ProcessHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, ULONG Flags, PHANDLE NewProcessHandle );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Handle to the previous process in the enumeration. NULL to start from the head of the list. |
| DesiredAccess | ACCESS_MASK | in | Access rights requested on the new handle, e.g. PROCESS_QUERY_LIMITED_INFORMATION. |
| HandleAttributes | ULONG | in | Handle attribute flags such as OBJ_INHERIT or OBJ_CASE_INSENSITIVE. Usually 0. |
| Flags | ULONG | in | Reserved. Must be 0 on current Windows builds. |
| NewProcessHandle | PHANDLE | out | Receives the handle to the next process. STATUS_NO_MORE_ENTRIES signals end of list. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0xE8 | win10-1507 |
| Win10 1607 | 0xEB | win10-1607 |
| Win10 1703 | 0xEE | win10-1703 |
| Win10 1709 | 0xEF | win10-1709 |
| Win10 1803 | 0xF0 | win10-1803 |
| Win10 1809 | 0xF1 | win10-1809 |
| Win10 1903 | 0xF2 | win10-1903 |
| Win10 1909 | 0xF2 | win10-1909 |
| Win10 2004 | 0xF7 | win10-2004 |
| Win10 20H2 | 0xF7 | win10-20h2 |
| Win10 21H1 | 0xF7 | win10-21h1 |
| Win10 21H2 | 0xF8 | win10-21h2 |
| Win10 22H2 | 0xF8 | win10-22h2 |
| Win11 21H2 | 0xFD | win11-21h2 |
| Win11 22H2 | 0xFE | win11-22h2 |
| Win11 23H2 | 0xFE | win11-23h2 |
| Win11 24H2 | 0x100 | win11-24h2 |
| Server 2016 | 0xEB | winserver-2016 |
| Server 2019 | 0xF1 | winserver-2019 |
| Server 2022 | 0xFC | winserver-2022 |
| Server 2025 | 0x100 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 00 01 00 00 mov eax, 0x100 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
NtGetNextProcess walks the EPROCESS chain anchored at PsActiveProcessHead, returning a fresh handle to each process in turn. Pass NULL as ProcessHandle on the first call; on subsequent calls pass the previously returned handle (the function closes it for you only on Windows 8+ when STATUS_NO_MORE_ENTRIES is returned — otherwise you must NtClose it). The companion routine NtGetNextThread iterates ETHREAD entries within a given process. Both are exported by ntdll.dll but have no Win32 wrapper — `CreateToolhelp32Snapshot` and `EnumProcesses` route through entirely different paths (Toolhelp via a snapshot section, PSAPI via NtQuerySystemInformation(SystemProcessInformation)).
Common malware usage
Modern OPSEC-aware tradecraft increasingly avoids Toolhelp32 and PSAPI because they are heavily hooked by EDR user-mode agents and produce noisy ETW events. Iterating PsActiveProcessHead via NtGetNextProcess gives the implant a process list without ever calling the obvious enumeration APIs. Typical uses include locating lsass.exe before a credential dump, finding EDR/AV processes for unhooking or impersonation, picking a sacrificial host for process injection, and avoiding analyst-tooling processes (Process Hacker, x64dbg) before unpacking. PoolParty and several public injection PoCs use the NtGetNextProcess + NtGetNextThread pair to enumerate thread-pool worker threads without touching Toolhelp.
Detection opportunities
NtGetNextProcess has very little legitimate user-mode footprint — Windows itself rarely calls it outside a handful of system components. A user-mode process repeatedly invoking it should be considered anomalous. Microsoft-Windows-Threat-Intelligence ETW does not surface this syscall directly, but the *follow-on* operations (NtOpenProcess against lsass, ProcessAccess with PROCESS_VM_READ) are well-instrumented (Sysmon Event ID 10). Kernel-callback-based EDRs that hook PspInsertProcess won't see this enumeration — only the access that follows. Look for unsigned binaries that resolve `NtGetNextProcess` from ntdll's export table.
Direct syscall examples
cLSASS PID discovery without Toolhelp
// Locate lsass.exe by walking PsActiveProcessHead via NtGetNextProcess.
// Avoids CreateToolhelp32Snapshot / EnumProcesses entirely.
#include <windows.h>
#include <winternl.h>
typedef NTSTATUS (NTAPI *PNT_GET_NEXT_PROCESS)(HANDLE, ACCESS_MASK, ULONG, ULONG, PHANDLE);
typedef NTSTATUS (NTAPI *PNT_QUERY_INFO_PROCESS)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
DWORD FindLsassPid(void) {
HMODULE nt = GetModuleHandleA("ntdll.dll");
PNT_GET_NEXT_PROCESS pNtGetNextProcess = (PNT_GET_NEXT_PROCESS)GetProcAddress(nt, "NtGetNextProcess");
PNT_QUERY_INFO_PROCESS pNtQueryInformationProcess = (PNT_QUERY_INFO_PROCESS)GetProcAddress(nt, "NtQueryInformationProcess");
HANDLE hProcess = NULL;
HANDLE hNext = NULL;
while (pNtGetNextProcess(hProcess, PROCESS_QUERY_LIMITED_INFORMATION, 0, 0, &hNext) == 0) {
if (hProcess) CloseHandle(hProcess);
hProcess = hNext;
BYTE buf[1024];
ULONG ret = 0;
if (pNtQueryInformationProcess(hProcess, ProcessImageFileName, buf, sizeof(buf), &ret) == 0) {
PUNICODE_STRING us = (PUNICODE_STRING)buf;
if (us->Buffer && wcsstr(us->Buffer, L"lsass.exe")) {
PROCESS_BASIC_INFORMATION pbi = {0};
pNtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &ret);
CloseHandle(hProcess);
return (DWORD)(ULONG_PTR)pbi.UniqueProcessId;
}
}
}
if (hProcess) CloseHandle(hProcess);
return 0;
}asmx64 direct stub (Win11 24H2, SSN 0x100)
; Direct syscall stub for NtGetNextProcess on Win11 24H2.
NtGetNextProcess PROC
mov r10, rcx
mov eax, 100h
syscall
ret
NtGetNextProcess ENDPrustEDR process scan via windows-sys + dynamic SSN
// Cargo: windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_System_LibraryLoader"] }
use std::ffi::CString;
use std::ptr::null_mut;
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress};
type NtGetNextProcessFn = unsafe extern "system" fn(HANDLE, u32, u32, u32, *mut HANDLE) -> i32;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
fn enumerate_processes(mut callback: impl FnMut(HANDLE)) {
unsafe {
let ntdll = GetModuleHandleA(CString::new("ntdll.dll").unwrap().as_ptr() as *const u8);
let name = CString::new("NtGetNextProcess").unwrap();
let f: NtGetNextProcessFn = std::mem::transmute(GetProcAddress(ntdll, name.as_ptr() as *const u8));
let mut prev: HANDLE = 0;
let mut next: HANDLE = 0;
while f(prev, PROCESS_QUERY_LIMITED_INFORMATION, 0, 0, &mut next) == 0 {
if prev != 0 { CloseHandle(prev); }
prev = next;
callback(prev);
}
if prev != 0 { CloseHandle(prev); }
let _ = null_mut::<u8>();
}
}MITRE ATT&CK mappings
Last verified: 2026-05-20