Process Enumeration

Author: Moise Medici

Last Update: 23 Nov 2025

Status: Completed

Code and executables download: From GitHub

Introduction

Process discovery is performed for a variety of reasons:

  • Understanding whether the malware is running in a sandbox or a virtual machine by checking for standard VMware/VirtualBox processes
  • Understanding whether the malware is being analyzed by checking for tools like Procmon, Wireshark, and similar applications
  • Finding a process to attach to in order to execute a malicious payload

This section focuses on the first two points, as they are very similar to each other. The main focus is understanding how the enumeration can be done with different sets of Windows APIs. There are generally three methods:

  1. Using CreateToolhelp32Snapshot 1, Process32First 2 and Process32Next 3
  2. Using EnumProcesses 4, OpenProcess 5, EnumProcessModules 6 and GetModuleBaseNameA 7
  3. Using NtQuerySystemInformation 8.

These three approaches are presented below.

Method 1

The following code illustrates how the combination of CreateToolhelp32Snapshot, Process32First and Process32Next can be used to enumerate the active processes running in the system.

This method is quick and easy to implement; however, it is not very stealthy and is easy to analyze.

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <string.h>
#define RET_SUCCESS 0
#define RET_ERROR -1
int main() {
HANDLE hSnapshot = NULL;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE) {
printf("cannot get handle from CreateToolhelp32Snapshot, error: %lu\n", GetLastError());
return RET_ERROR;
}
PROCESSENTRY32 process_s = {.dwSize = sizeof(PROCESSENTRY32)};
if (Process32First(hSnapshot, &process_s) == FALSE) {
printf("cannot get process from Process32First, error: %lu", GetLastError());
CloseHandle(hSnapshot);
return RET_ERROR;
}
do {
printf("Process ID: %lu, Executable: %s\n", process_s.th32ProcessID, process_s.szExeFile);
if (strcmp(process_s.szExeFile, "notepad.exe") == 0) {
printf("Notepad detected. Exiting\n\n");
break;
}
} while (Process32Next(hSnapshot, &process_s));
CloseHandle(hSnapshot);
return RET_SUCCESS;
}

The first function, windows.CreateToolhelp32Snapshot, is called with the arguments 2 and 0. Based on the Microsoft documentation, this is the signature of the function:

HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);

The first value, dwFlags, indicates which portions of the system to include in the snapshot. Here 2 is equal to TH32CS_SNAPPROCESS, which means, quoting the documentation: “Includes all processes in the system in the snapshot. To enumerate the processes, see Process32First.”

The second value, th32ProcessID, indicates which process to take a snapshot of. However, since with dwFlags the snapshot is requested for all the processes, the th32ProcessID argument is ignored, and 0 is passed.

The return value is a handle to the snapshot if successful.

After the execution of this call, the result is a snapshot of the system’s running processes at the moment of the snapshot. To start iterating over the processes, the code calls Process32First, which starts the “cursor” over the list of processes, and continues the iteration by calling Process32Next.

Both Process32First and Process32Next have the same signature:

BOOL Process32First(
[in] HANDLE hSnapshot,
[in, out] LPPROCESSENTRY32 lppe
);

The first parameter is the snapshot and the second is a structure containing information about a single process. In the first iteration, the structure is empty and gets filled in when Process32First and Process32Next are called.

The code then prints the names of the processes and checks them. As an example, it checks if notepad.exe is open. If it is, a message is printed: Notepad detected. Exiting, otherwise no such message appears.

After executing the binary derived from the compilation of the above code, the output will be similar to the following:

Process ID: 5768, Executable: svchost.exe
Process ID: 6992, Executable: dllhost.exe
Process ID: 7276, Executable: svchost.exe
Process ID: 7432, Executable: cmd.exe
Process ID: 7440, Executable: conhost.exe
Process ID: 7680, Executable: myapp.exe

If Notepad is open, the executable recognizes it and stops as soon as it is found:

Process ID: 7276, Executable: svchost.exe
Process ID: 7432, Executable: cmd.exe
Process ID: 7440, Executable: conhost.exe
Process ID: 7844, Executable: notepad.exe
Notepad detected. Exiting

Method 2

The second method uses the EnumProcesses, OpenProcess and EnumProcessModules functions. The idea here is to gather all the process IDs that are currently running in the system, store them in PID, and open each process to get the information for that ID. This is a more effective approach since, by accessing the process, it is already clear that there is access to it. This assumption is not true for the previous method, since CreateToolhelp32Snapshot gathers all admin and non-admin processes without any distinction.

#include <windows.h>
#include <psapi.h>
#include <stdio.h>
#define RET_SUCCESS 0
#define RET_ERROR -1
int main() {
DWORD PID[1024], cbNeeded, cbNeeded2, processCount;
if (EnumProcesses(PID, sizeof(PID), &cbNeeded) == 0) {
printf("cannot execute EnumProcess, error: %lu\n", GetLastError());
return RET_ERROR;
}
processCount = cbNeeded / sizeof(DWORD);
HANDLE hProcess = NULL;
for (int i = 0; i < processCount; i++) {
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, PID[i]);
if (hProcess == NULL) {
continue;
}
HMODULE hModule = NULL;
EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &cbNeeded2);
if (hModule == NULL) {
CloseHandle(hProcess);
continue;
}
char baseName[MAX_PATH] = {0};
if (GetModuleBaseNameA(hProcess, hModule, baseName, sizeof(baseName) / sizeof(CHAR)) == 0) {
CloseHandle(hProcess);
continue;
}
printf("Process ID: %lu, Executable: %s\n", PID[i], baseName);
if (strcmp(baseName, "notepad.exe") == 0) {
printf("Notepad detected. Exiting\n\n");
break;
}
CloseHandle(hProcess);
}
CloseHandle(hProcess);
return RET_SUCCESS;
}

Looking at the function definition:

BOOL EnumProcesses(
[out] DWORD *lpidProcess,
[in] DWORD cb,
[out] LPDWORD lpcbNeeded
);

There are two output variables: lpidProcess, which is a pointer to DWORD that will contain the list of process IDs currently running, and lpcbNeeded, which is the number of bytes in the lpidProcess array. The oddity about lpidProcess is that it needs to be “big enough” for all the PIDs, without knowing in advance how many there will be. So the Microsoft documentation says: “It is a good idea to use a large array, because it is hard to predict how many processes there will be at the time you call EnumProcesses.”

lpcbNeeded is useful to calculate the number of processes, as the documentation suggests: “To determine how many processes were enumerated, divide the lpcbNeeded value by sizeof(DWORD).” This is done at line 15.

Once the number of processes has been determined, the code iterates over each of them to gather more information, specifically the name. Using OpenProcess, the code sets the desired access to both PROCESS_QUERY_INFORMATION and PROCESS_VM_READ. Why these permissions? They are defined in a Microsoft article linked in the appendix 9.

The second parameter checks if the handle needs to be inherited by the process. Since there is no need to use the handle in any of the child processes of the current process, it can be set to 0. Lastly, the process ID to open is provided.

HANDLE OpenProcess(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] DWORD dwProcessId
);

The last step is to get the name of the process. To do so, the code first gets all the modules (a module is an executable or DLL loaded in the process 10) and then extracts the name. With EnumProcessModules it gets the handle needed by GetModuleBaseName.

BOOL EnumProcessModules(
[in] HANDLE hProcess,
[out] HMODULE *lphModule,
[in] DWORD cb,
[out] LPDWORD lpcbNeeded
);

In this function, the cb is again the size of what is being requested, in this case the HMODULE type, since the goal is to retrieve an HMODULE object, whose pointer is passed as the second argument. In this case lpcbNeeded is not used, however it must still be passed to the function.

Once there is a handle to the process and a handle to the module for that process, the code can then get the name.

DWORD GetModuleBaseNameA(
[in] HANDLE hProcess,
[in, optional] HMODULE hModule,
[out] LPSTR lpBaseName,
[in] DWORD nSize
);

The name of the module will be stored in lpBaseName, which is a string. Since the size of the name cannot be known in advance, it is necessary to set a high enough value. Windows provides the MAX_PATH macro, which defines the string as a maximum of 260 characters 11.

Once all the information has been collected, the code searches for the process of interest, in this case, notepad.exe. Similarly to the previous example, the output shows the relationship between PID and process name, and the executable stops if Notepad is running.

Method 3

The last method relies on NtQuerySystemInformation. Note that this function is part of an internal Windows library. It is partially undocumented, especially with respect to the structures returned, and usage is discouraged by Microsoft.

#include <windows.h>
#include <winternl.h>
#include <winnt.h>
#include <stdio.h>
#include <wchar.h>
#define RET_SUCCESS 0
#define RET_ERROR -1
int main() {
ULONG retLen, sysInfoLen = 0;
PSYSTEM_PROCESS_INFORMATION procInfo = {0};
NTSTATUS STATUS = 0;
NtQuerySystemInformation(SystemProcessInformation, NULL, 0, &retLen);
procInfo =
(PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)retLen);
if (procInfo == NULL) {
printf("HeapAlloc failed, error %lu\n", GetLastError());
return RET_ERROR;
}
STATUS = NtQuerySystemInformation(SystemProcessInformation, procInfo, retLen, &sysInfoLen);
if (STATUS != 0x0) {
printf("NtQuerySystemInformation failed, error 0x%0.8lX\n", STATUS);
return RET_ERROR;
}
while (procInfo->NextEntryOffset) {
const UNICODE_STRING* name = &procInfo->ImageName;
if (name->Buffer) {
printf(
"Process ID: %.*ls, Executable: %lu\n",
name->MaximumLength,
name->Buffer,
(unsigned long)(ULONG_PTR)procInfo->UniqueProcessId
);
if (wcscmp(name->Buffer, L"notepad.exe") == 0) {
printf("Notepad detected. Exiting\n\n");
break;
}
}
procInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)procInfo + procInfo->NextEntryOffset);
}
return RET_SUCCESS;
}

The function signature is:

__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);

The first parameter indicates the information that is going to be returned. The type of argument needed is SYSTEM_INFORMATION_CLASS. Since the goal is to retrieve the list of processes, SystemProcessInformation is used, as described by Microsoft: “Returns an array of SYSTEM_PROCESS_INFORMATION structures, one for each process running in the system.

These structures contain information about the resource usage of each process, including the number of threads and handles used by the process, the peak page-file usage, and the number of memory pages that the process has allocated.”

The SYSTEM_INFORMATION_CLASS is partially documented in the appendix 12, however a more complete resource is appendix 13.

NtQuerySystemInformation is a function that needs to be called twice, the first time to populate the size of the information requested and store it in ReturnLength, and the second time to populate the SystemInformation argument.

Once the first call is completed, the heap can be reserved for an object of size ReturnLength. The second call will populate the memory space and the code can then loop over the list of structures.

Within the loop, it prints the usual PID and name string and stops if Notepad is detected. The last line of the loop is only used to move to the next structure, since it is a linked list where each structure is linked to the next one with an address stored in NextEntryOffset.

Appendix

  • 1 Microsoft, "CreateToolHelp32Snapshot function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot. [Accessed: Nov. 23 2025].
  • 2 Microsoft, "Process32First function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32first. [Accessed: Nov. 22 2025].
  • 3 Microsoft, "Process32Next function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next. [Accessed: Nov. 22 2025].
  • 4 Microsoft, "EnumProcesses function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocesses. [Accessed: Jan. 22 2026].
  • 5 Microsoft, "OpenProcess function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess. [Accessed: Jan. 22 2026].
  • 6 Microsoft, "EnumProcessModules function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumprocessmodules. [Accessed: Jan. 22 2026].
  • 7 Microsoft, "GetModuleBaseNameA function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getmodulebasenamea. [Accessed: Jan. 22 2026].
  • 8 Microsoft, "NtQuerySystemInformation function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation. [Accessed: Jan. 22 2026].
  • 9 Microsoft, "Enumerating all processes". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes. [Accessed: Jan. 22 2026].
  • 10 Microsoft, "Module information". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/psapi/module-information. [Accessed: Jan. 22 2026].
  • 11 Microsoft, "Maximum Path Length Limitation". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry. [Accessed: Jan. 22 2026].
  • 12 Microsoft, "NtQuerySystemInformation function". [Online]. Available: https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation#system_process_information. [Accessed: Jan. 22 2026].
  • 13 Geoff Chappell, Software Analyst, "SYSTEM_PROCESS_INFORMATION". [Online]. Available: https://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/sysinfo/process.htm. [Accessed: Jan. 22 2026].