> Windows Syscalls
ntoskrnl.exeT1057T1106

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

NameTypeDirDescription
ProcessHandleHANDLEinHandle to the previous process in the enumeration. NULL to start from the head of the list.
DesiredAccessACCESS_MASKinAccess rights requested on the new handle, e.g. PROCESS_QUERY_LIMITED_INFORMATION.
HandleAttributesULONGinHandle attribute flags such as OBJ_INHERIT or OBJ_CASE_INSENSITIVE. Usually 0.
FlagsULONGinReserved. Must be 0 on current Windows builds.
NewProcessHandlePHANDLEoutReceives the handle to the next process. STATUS_NO_MORE_ENTRIES signals end of list.

Syscall IDs by Windows version

Windows versionSyscall IDBuild
Win10 15070xE8win10-1507
Win10 16070xEBwin10-1607
Win10 17030xEEwin10-1703
Win10 17090xEFwin10-1709
Win10 18030xF0win10-1803
Win10 18090xF1win10-1809
Win10 19030xF2win10-1903
Win10 19090xF2win10-1909
Win10 20040xF7win10-2004
Win10 20H20xF7win10-20h2
Win10 21H10xF7win10-21h1
Win10 21H20xF8win10-21h2
Win10 22H20xF8win10-22h2
Win11 21H20xFDwin11-21h2
Win11 22H20xFEwin11-22h2
Win11 23H20xFEwin11-23h2
Win11 24H20x100win11-24h2
Server 20160xEBwinserver-2016
Server 20190xF1winserver-2019
Server 20220xFCwinserver-2022
Server 20250x100winserver-2025

Kernel module

ntoskrnl.exeNtGetNextProcess

Related APIs

NtGetNextThreadNtQueryInformationProcessNtQuerySystemInformationCreateToolhelp32SnapshotEnumProcessesOpenProcess

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 ENDP

rustEDR 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