NtGetNextProcess
Recorre la lista de procesos del kernel y devuelve un handle al siguiente proceso.
Prototipo
NTSTATUS NtGetNextProcess( HANDLE ProcessHandle, ACCESS_MASK DesiredAccess, ULONG HandleAttributes, ULONG Flags, PHANDLE NewProcessHandle );
Argumentos
| Name | Type | Dir | Description |
|---|---|---|---|
| ProcessHandle | HANDLE | in | Handle al proceso anterior en la enumeración. NULL para empezar desde el inicio de la lista. |
| DesiredAccess | ACCESS_MASK | in | Derechos de acceso solicitados en el nuevo handle, p. ej. PROCESS_QUERY_LIMITED_INFORMATION. |
| HandleAttributes | ULONG | in | Banderas de atributos del handle como OBJ_INHERIT u OBJ_CASE_INSENSITIVE. Habitualmente 0. |
| Flags | ULONG | in | Reservado. Debe ser 0 en los builds actuales de Windows. |
| NewProcessHandle | PHANDLE | out | Recibe el handle del siguiente proceso. STATUS_NO_MORE_ENTRIES indica el final de la lista. |
IDs de syscalls por versión de Windows
| Versión 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 |
Módulo del kernel
APIs relacionadas
Stub del 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
Notas no documentadas
NtGetNextProcess recorre la cadena EPROCESS anclada en PsActiveProcessHead y devuelve un handle nuevo a cada proceso por turnos. Pase NULL como ProcessHandle en la primera llamada; en las siguientes pase el handle devuelto previamente (la función lo cierra por usted solo en Windows 8+ cuando se devuelve STATUS_NO_MORE_ENTRIES — de lo contrario debe NtClose-arlo). La rutina hermana NtGetNextThread itera los ETHREAD dentro de un proceso dado. Ambas están exportadas por ntdll.dll pero no tienen un wrapper Win32 — `CreateToolhelp32Snapshot` y `EnumProcesses` siguen caminos completamente distintos (Toolhelp vía una sección de snapshot, PSAPI vía NtQuerySystemInformation(SystemProcessInformation)).
Uso común por malware
El tradecraft moderno consciente de OPSEC evita cada vez más Toolhelp32 y PSAPI porque están muy hookeados por los agentes user-mode de los EDR y producen eventos ETW ruidosos. Iterar PsActiveProcessHead vía NtGetNextProcess da al implante una lista de procesos sin llamar nunca a las API de enumeración obvias. Usos típicos: localizar lsass.exe antes de un volcado de credenciales, encontrar procesos EDR/AV para des-hookearlos o suplantarlos, escoger un host sacrificial para inyección, y evitar procesos de herramientas de análisis (Process Hacker, x64dbg) antes del desempaquetado. PoolParty y varios PoC públicos usan la pareja NtGetNextProcess + NtGetNextThread para enumerar hilos del thread pool sin tocar Toolhelp.
Oportunidades de detección
NtGetNextProcess tiene una huella legítima en user-mode muy escasa — el propio Windows raramente lo invoca fuera de un puñado de componentes del sistema. Un proceso user-mode que lo invoque repetidamente debe considerarse anómalo. El ETW Microsoft-Windows-Threat-Intelligence no expone este syscall directamente, pero las operaciones *posteriores* (NtOpenProcess contra lsass, ProcessAccess con PROCESS_VM_READ) están bien instrumentadas (Sysmon Event ID 10). Los EDR con callbacks de kernel que hookean PspInsertProcess no verán esta enumeración — solo el acceso que sigue. Busque binarios sin firmar que resuelvan `NtGetNextProcess` desde la tabla de exports de ntdll.
Ejemplos de syscalls directos
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>();
}
}Mapeos MITRE ATT&CK
Last verified: 2026-05-20