NtGetNextProcess
Parcourt la liste des processus du noyau et retourne un handle vers le processus suivant.
Prototype
NTSTATUS NtGetNextProcess( HANDLE ProcessHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, ULONG Flags, PHANDLE NewProcessHandle );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Handle vers le processus précédent dans l'énumération. NULL pour partir du début de la liste. |
| DesiredAccess | ACCESS_MASK | in | Droits d'accès demandés sur le nouveau handle, ex. PROCESS_QUERY_LIMITED_INFORMATION. |
| HandleAttributes | ULONG | in | Drapeaux d'attributs de handle tels que OBJ_INHERIT ou OBJ_CASE_INSENSITIVE. Généralement 0. |
| Flags | ULONG | in | Réservé. Doit valoir 0 sur les builds Windows actuels. |
| NewProcessHandle | PHANDLE | out | Reçoit le handle du processus suivant. STATUS_NO_MORE_ENTRIES indique la fin de la liste. |
IDs de syscalls par version de Windows
| Version de Windows | ID de syscall | 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 |
Module noyau
APIs liées
Stub du syscall
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
Notes non documentées
NtGetNextProcess parcourt la chaîne EPROCESS ancrée à PsActiveProcessHead et retourne un nouveau handle vers chaque processus. Passez NULL en ProcessHandle au premier appel ; aux appels suivants passez le handle précédemment retourné (la fonction le ferme pour vous uniquement sous Windows 8+ quand STATUS_NO_MORE_ENTRIES est renvoyé — sinon vous devez NtClose). La routine soeur NtGetNextThread itère les ETHREAD d'un processus donné. Toutes deux sont exportées par ntdll.dll mais n'ont pas d'enveloppe Win32 — `CreateToolhelp32Snapshot` et `EnumProcesses` empruntent des chemins entièrement différents (Toolhelp via une section snapshot, PSAPI via NtQuerySystemInformation(SystemProcessInformation)).
Usage courant par les malwares
Le tradecraft moderne soucieux d'OPSEC évite de plus en plus Toolhelp32 et PSAPI car ils sont fortement hookés par les agents user-mode des EDR et génèrent des événements ETW bruyants. Itérer PsActiveProcessHead via NtGetNextProcess donne à l'implant la liste des processus sans jamais appeler les API d'énumération évidentes. Usages typiques : localiser lsass.exe avant un dump d'identifiants, trouver les processus EDR/AV pour les unhook ou usurper leur identité, choisir un hôte sacrificiel pour l'injection, éviter les processus d'outillage d'analyse (Process Hacker, x64dbg) avant le dépaquetage. PoolParty et plusieurs PoC publics utilisent le couple NtGetNextProcess + NtGetNextThread pour énumérer les threads du pool sans toucher à Toolhelp.
Opportunités de détection
NtGetNextProcess a une empreinte légitime en user-mode très faible — Windows lui-même l'appelle rarement hors de quelques composants système. Un processus user-mode qui l'invoque de manière répétée doit être considéré comme anormal. L'ETW Microsoft-Windows-Threat-Intelligence n'expose pas directement ce syscall, mais les opérations *suivantes* (NtOpenProcess sur lsass, ProcessAccess avec PROCESS_VM_READ) sont bien instrumentées (Sysmon Event ID 10). Les EDR à callbacks noyau qui hookent PspInsertProcess ne verront pas cette énumération — seulement l'accès qui suit. Cherchez des binaires non signés qui résolvent `NtGetNextProcess` depuis la table d'exports de ntdll.
Exemples de syscalls directs
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>();
}
}Mappings MITRE ATT&CK
Last verified: 2026-05-20