简单的进程枚举方法
2026/6/4 1:03:19 网站建设 项目流程

前言

在常见的攻击链中的很多操作都依赖于找到正确的目标进程,本文记录了我在很久前学习的一些基础的进程枚举方法,当然还有很多更复杂的像是通过syscall以及驱动层面的内核枚举,这里简单记录便于自己查阅,希望对读到这篇文章的朋友有所帮助,如果有错误望指正。

CreateToolhelp32Snapshot

最简单的方法,但也最有限的方法

#include<windows.h>#include<iostream>#include<string>#include<tlhelp32.h>#include<processthreadsapi.h>#include<DbgHelp.h>usingnamespacestd;intGetPid(){HRESULT hr;DWORD pid=-1;PROCESSENTRY32 ed;ed.dwSize=sizeof(PROCESSENTRY32);HANDLE snapshot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,NULL);//创建快照存储进snapshotif(Process32First(snapshot,&ed)==TRUE){while(Process32Next(snapshot,&ed)){if(wcscmp(ed.szExeFile,L"lsass.exe")==0){pid=ed.th32ProcessID;}}}CloseHandle(snapshot);if(pid!=0)returnpid;}intmain(){GetPid();//使用时注意系统会存在同名进程的问题,例如avp.exe、svchost.exe等intpid=GetPid();printf("%d\n",pid);return0;}

使用CreateToolhelp32Snapshot我们能拿到的是:

PROCESSENTRY32{DWORD th32ProcessID;//PIDDWORD th32ParentProcessID;//PPIDTCHAR szExeFile[MAX_PATH];// 进程名}

szExeFile只是文件名,没有:

  • Token
  • Integrity Level
  • Session
  • 是否是 SYSTEM
  • 是否是 PPL

那么通过字符串比较的问题在哪呢?

典型代码长这样:

if(_wcsicmp(pe.szExeFile,L"svchost.exe")==0){// 命中}

问题来了 ,Windows 上的现实情况是:

svchost.exe (User, Medium IL) svchost.exe (SYSTEM, High / System IL) svchost.exe (PPL) svchost.exe (不同 Session)

但我们能拿到的只有:

svchost.exe svchost.exe svchost.exe svchost.exe

字符串完全一样

在DLL、ShellCode注入,ReadProcessMemory / WriteProcessMemory等等技术中我们都需要依赖目标进程的权限

如果非要使用CreateToolhelp32Snapshot还需要使用OpenProcess进一步判断目标进程的权限,而且高权限的进程还会失败,这在很多的进程身上是很麻烦的

svchost.exe 是最典型的例子:

  • 同名
  • 多实例
  • 不同权限
  • 不同服务组

所以:

你可能想注入一个“用户态 svchost”,
却随机选中了一个SYSTEM svchost
然后所有操作失败。

这就是CreateToolhelp32Snapshot的Toolhelp结构性缺陷,因为它的功能就是给普通的程序列个列表

EnumProcesses

EnumProcesses的优势不在“信息多”,而在于:

它能在权限受限的情况下,稳定、快速、低摩擦地拿到「所有 PID」
而且不会因为某些高权限进程而中断或失败

EnumProcesses只返回PID,PID 是 Windows 中最“低权限可见”的进程标识

这意味着即使是普通用户权限也几乎能拿到所有包括:

  • SYSTEM 进程 PID
  • PPL 进程 PID
  • Session 0 的 PID

而 Toolhelp 在某些环境(EDR、沙箱、老系统、WOW64 边界)下:

  • 可能漏
  • 可能被 Hook
  • 可能被策略限制

CreateToolhelp32Snapshot在遇到高权限进程时可能会异常,而EnumProcesses会完全无感,不会被“你无权访问的进程”影响

#include<stdio.h>#include<windows.h>#include<tchar.h>#include<psapi.h>// 枚举并打印当前系统中的所有进程BOOLPrintProcesses(){// 用来存放获取到的 PID(进程ID)DWORD adwProcesses[1024*2],// EnumProcesses 返回的字节数dwReturnLen1=0,// EnumProcessModules 返回的字节数dwReturnLen2=0,// 实际获取到的 PID 数量dwNmbrOfPids=0;// 进程句柄HANDLE hProcess=NULL;// 模块句柄(通常第一个模块就是主程序 EXE)HMODULE hModule=NULL;// 用于保存进程名称WCHAR szProc[MAX_PATH];// ----------------------------------------------------// 获取系统中所有进程的 PID// ----------------------------------------------------if(!EnumProcesses(adwProcesses,// 输出缓冲区sizeof(adwProcesses),// 缓冲区大小&dwReturnLen1))// 实际返回的字节数{printf("[!] EnumProcesses Failed With Error : %d\n",GetLastError());returnFALSE;}// 计算获取到了多少个 PID// 返回值是字节数,因此需要除以 DWORD 大小dwNmbrOfPids=dwReturnLen1/sizeof(DWORD);printf("[i] Number Of Processes Detected : %d\n",dwNmbrOfPids);// 遍历所有 PIDfor(inti=0;i<dwNmbrOfPids;i++){// PID 为 0 通常表示 Idle Processif(adwProcesses[i]!=0){// ------------------------------------------------// 尝试打开进程//// PROCESS_QUERY_INFORMATION:// 查询进程信息//// PROCESS_VM_READ:// 读取目标进程内存//// 有些系统进程由于权限不足可能无法打开// ------------------------------------------------hProcess=OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ,FALSE,adwProcesses[i]);// 打开成功if(hProcess!=NULL){// --------------------------------------------// 获取该进程加载的模块信息//// 一般第一个模块就是主程序 EXE// --------------------------------------------if(!EnumProcessModules(hProcess,&hModule,sizeof(HMODULE),&dwReturnLen2)){printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n",adwProcesses[i],GetLastError());}else{// ----------------------------------------// 根据模块句柄获取进程名称//// 例如:// explorer.exe// notepad.exe// chrome.exe// ----------------------------------------if(!GetModuleBaseName(hProcess,hModule,szProc,sizeof(szProc)/sizeof(WCHAR))){printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n",adwProcesses[i],GetLastError());}else{// 打印进程名称和 PIDwprintf(L"[%0.3d] Process \"%s\" - Of Pid : %d\n",i,szProc,adwProcesses[i]);}}// 使用完句柄后必须关闭CloseHandle(hProcess);}}// 继续处理下一个 PID}returnTRUE;}intmain(){PrintProcesses();return0;}

这段代码体现的是:

  • 正确理解权限边界
  • 不假设自己能访问所有进程
  • 不因为失败就中断枚举

NtQuerySystemInformation

NtQuerySystemInformation的作用是获取指定的系统信息,它是一个系统调用,ntdll.dll中导出,所以需要动态调用。

__kernel_entry NTSTATUSNtQuerySystemInformation([in]SYSTEM_INFORMATION_CLASS SystemInformationClass,[in,out]PVOID SystemInformation,[in]ULONG SystemInformationLength,[out,optional]PULONG ReturnLength);
  • SystemInformationClass - 决定函数返回哪种类型的系统信息
  • SystemInformation - 指向用于接收请求信息的缓冲区的指针。返回的信息将以结构体的形式呈现,结构体的类型由 SystemInformationClass 参数指定
  • SystemInformationLength - 由 SystemInformation 参数指向的缓冲区的大小,以字节为单位。
  • ReturnLength - 指向 ULONG 变量的指针,该变量将接收写入 SystemInformation 信息的实际大小。

因为我们的目的是便利进程,所以我们使用SystemProcessInformation

SYSTEM_PROCESS_INFORMATION的结构如下:

typedefstruct_SYSTEM_PROCESS_INFORMATION{ULONG NextEntryOffset;ULONG NumberOfThreads;BYTE Reserved1[48];UNICODE_STRING ImageName;KPRIORITY BasePriority;HANDLE UniqueProcessId;HANDLE InheritedFromUniqueProcessId;ULONG HandleCount;ULONG SessionId;PVOID Reserved3;SIZE_T PeakVirtualSize;SIZE_T VirtualSize;ULONG Reserved4;SIZE_T PeakWorkingSetSize;SIZE_T WorkingSetSize;PVOID Reserved5;SIZE_T QuotaPagedPoolUsage;PVOID Reserved6;SIZE_T QuotaNonPagedPoolUsage;SIZE_T PagefileUsage;SIZE_T PeakPagefileUsage;SIZE_T PrivatePageCount;LARGE_INTEGER Reserved7[6];}SYSTEM_PROCESS_INFORMATION;

由于使用SystemProcessInformation标志调用NtQuerySystemInformation将返回一个大小未知的SYSTEM_PROCESS_INFORMATION数组,因此NtQuerySystemInformation需要调用两次。第一次调用将检索数组大小用于分配缓冲区,然后第二次调用将使用已分配的缓冲区。

我们重点关注包含进程名称的UNICODE_STRING ImageName和进程 ID 的UniqueProcessId。此外,NextEntryOffset将用于移动到返回数组中的下一个元素。

#include<stdio.h>#include<windows.h>#include<tchar.h>#include<psapi.h>#include<winternl.h>// Function pointertypedefNTSTATUS(NTAPI*fnNtQuerySystemInformation)(SYSTEM_INFORMATION_CLASS SystemInformationClass,PVOID SystemInformation,ULONG SystemInformationLength,PULONG ReturnLength);BOOLGetRemoteProcessHandle(LPCWSTR szProcName,DWORD*pdwPid,HANDLE*phProcess){fnNtQuerySystemInformation pNtQuerySystemInformation=NULL;ULONG uReturnLen1=NULL,uReturnLen2=NULL;PSYSTEM_PROCESS_INFORMATION SystemProcInfo=NULL;NTSTATUS STATUS=NULL;PVOID pValueToFree=NULL;pNtQuerySystemInformation=(fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"),"NtQuerySystemInformation");if(pNtQuerySystemInformation==NULL){printf("[!] GetProcAddress Failed With Error : %d\n",GetLastError());returnFALSE;}pNtQuerySystemInformation(SystemProcessInformation,NULL,NULL,&uReturnLen1);//探测式调用,为例获取缓冲区大小//一个连续内存块,里面是所有 SYSTEM_PROCESS_INFORMATION 链表节点SystemProcInfo=(PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,(SIZE_T)uReturnLen1);if(SystemProcInfo==NULL){printf("[!] HeapAlloc Failed With Error : %d\n",GetLastError());returnFALSE;}//因为要修改SystemProcInfo 指针本身用于遍历,所以这里保存初始值pValueToFree=SystemProcInfo;//第二次调用,这次是真正的拿取数据STATUS=pNtQuerySystemInformation(SystemProcessInformation,SystemProcInfo,uReturnLen1,&uReturnLen2);if(STATUS!=0x0){printf("[!] NtQuerySystemInformation Failed With Error : 0x%0.8X \n",STATUS);returnFALSE;}while(TRUE){//ImageName.Length - 排除PID为0以及无名字的进程//wcscmp - 精准匹配,区分大小写if(SystemProcInfo->ImageName.Length&&wcscmp(SystemProcInfo->ImageName.Buffer,szProcName)==0){// 获取PID + 打开进程*pdwPid=(DWORD)SystemProcInfo->UniqueProcessId;*phProcess=OpenProcess(PROCESS_ALL_ACCESS,FALSE,(DWORD)SystemProcInfo->UniqueProcessId);break;}// 如果 NextEntryOffset 为 0,则表示已到达数组末尾if(!SystemProcInfo->NextEntryOffset)break;// 移动到数组中的下一个元素 枚举的核心逻辑SystemProcInfo=(PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo+SystemProcInfo->NextEntryOffset);}// Free using the initial addressHeapFree(GetProcessHeap(),0,pValueToFree);// Check if we successfully got the target process handleif(*pdwPid==NULL||*phProcess==NULL)returnFALSE;elsereturnTRUE;}intmain(){DWORD dwPid=0;HANDLE hProcess=NULL;if(!GetRemoteProcessHandle(L"notepad.exe",&dwPid,&hProcess)){printf("[!] Failed to get notepad.exe process handle\n");return-1;}printf("[+] notepad.exe found!\n");printf(" PID : %lu\n",dwPid);printf(" HANDLE : 0x%p\n",hProcess);printf("[*] Press Enter to close handle...\n");getchar();CloseHandle(hProcess);return0;}

运行结果如下:

三者的区别

  • CreateToolhelp32Snapshot
    👉用户态快照,简单、稳定,但最容易被“藏”
  • EnumProcesses(PSAPI)
    👉用户态进程列表,比 Toolhelp 稍“近内核”,但仍然可控
  • NtQuerySystemInformation(SystemProcessInformation)
    👉内核视角,信息最全、最底层,也是 EDR/Rootkit 的主战场

CreateToolhelp32Snapshot

工作机制
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0)Process32First/Process32Next

本质是:

  • 内核生成一个进程快照
  • 拷贝到用户态
  • 你在用户态遍历

特点

优点:

  • API 非常稳定
  • 代码简单
  • 兼容性极好
缺点:
  • 快照是“静态”的

  • 容易被:

    • API Hook
    • 内核过滤
    • Rootkit 隐藏

可见信息
  • PID
  • PPID
  • EXE 名
  • 线程数

❌ 没有完整路径
❌ 没有命令行
❌ 没有 PEB / Session 细节


对抗 / 检测视角
  • 最容易被隐藏
  • 很多 rootkit 只要过滤这一条路径
  • EDR 并不依赖它作为“真相来源”

适合场景
  • 正常软件
  • 工具类程序
  • 不追求“绝对准确性”的枚举

EnumProcesses(PSAPI)

工作机制
EnumProcesses(...)→ 返回 PID 列表 → 再 OpenProcess+GetModuleFileNameEx

底层依赖:

  • NtQuerySystemInformation的某些变体
  • 或内部内核接口(实现细节版本相关)

特点

优点:

  • 比 Toolhelp 稍微“靠近内核”

  • 可以配合:

    • OpenProcess
    • GetProcessImageFileName
缺点:
  • 本身只给 PID
  • 后续信息获取容易失败(权限 / PPL)

可见信息
  • PID(直接)
  • Image Path(间接)
  • 模块信息(可选)

❌ 不直接给命令行
❌ 不给创建时间
❌ 不给完整结构信息


对抗 / 检测视角
  • 比 Toolhelp 难藏一点

  • 但:

    • Hook psapi.dll
    • 内核过滤 PID
      仍然可行

适合场景
  • 管理工具
  • 资源监控
  • 老代码(PSAPI 很多遗留)

NtQuerySystemInformation(SystemProcessInformation)

工作机制
NtQuerySystemInformation(SystemProcessInformation,buffer,size,&retLen)

返回的是一个内核生成的链表结构

SYSTEM_PROCESS_INFORMATION ├── UniqueProcessId ├── InheritedFromUniqueProcessId ├── ImageName (UNICODE_STRING) ├── CreateTime ├── SessionId ├── ThreadCount ├── CPU / Memory 统计 └── NextEntryOffset

特点

优点:

  • 信息最全

  • 接近内核真实状态

  • 可见:

    • PPID
    • Session
    • 创建时间
    • 线程数
    • ImageName(不是路径)
缺点:
  • 结构不稳定(版本差异)
  • 需要自己遍历链表
  • EDR/AV 强监控 API

可见信息(最全)

✔ PID
✔ PPID
✔ ImageName
✔ 创建时间
✔ SessionId
✔ 线程数
✔ 资源统计

但注意:

  • ImageName ≠ 完整路径
  • CommandLine 仍需读 PEB

对抗 / 检测视角
  • EDR 的“事实源”之一

  • Rootkit 想隐藏进程:

    • 必须 hook / patch 这里
  • 安全产品会:

    • 对比 Toolhelp vs NtQuerySystemInformation
    • 检测差异

适合场景
  • 安全产品
  • 红队
  • Rootkit / Anti-rootkit
  • 高级进程枚举

核心差异对比

维度ToolhelpEnumProcessesNtQuerySystemInformation
数据来源用户态快照用户态接口内核
实时性
信息量
易被隐藏极高
实现复杂度
EDR关注度极高

一句话终极总结

CreateToolhelp32Snapshot 是“给应用看的”,EnumProcesses 是“给系统工具看的”,NtQuerySystemInformation 是“给安全产品和内核自己看的”。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询