模块 33 - 进程枚举 - EnumProcesses
恶意软件开发课程 - 进程枚举 - EnumProcesses
模块 33 - 进程枚举 - EnumProcesses#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
进程枚举 - EnumProcesses#
简介#
在之前的进程注入模块中,演示了一种使用 CreateToolhelp32Snapshot ↗ 进行进程枚举的方法 (Snapshot 方法)。本模块将演示另一种使用 EnumProcesses ↗ 进行进程枚举的方法 (PSAPI 方法)。
对于恶意软件作者来说,能够以多种方式在恶意软件中实现一种技术非常重要,以便在行动中保持不可预测性。
EnumProcesses#
首先回顾微软关于 EnumProcesses ↗ 的文档。请注意,该函数将进程 ID (PID) 作为数组返回,而不包含关联的进程名称。问题在于,只有 PID 而没有关联的进程名称,使得从人类的角度很难识别进程。
解决方案是使用 OpenProcess ↗, GetModuleBaseName ↗ 和 EnumProcessModules ↗ WinAPI。
OpenProcess将用于打开具有PROCESS_QUERY_INFORMATION和PROCESS_VM_READ访问权限的 PID 句柄。EnumProcessModules将用于枚举已打开进程内的所有模块。这是步骤 3 所必需的。GetModuleBaseName将根据步骤 2 中枚举的进程模块确定进程的名称。
💡 初学者提示:模块 (Module) 是什么?
在 Windows 中,“模块”通常指加载到进程中的 PE 文件(如 .exe 或 .dll)。 一个进程的第一个模块通常是可执行文件本身(例如
notepad.exe)。这就是为什么我们需要枚举模块来获取进程名称。
EnumProcesses 的优势#
使用 CreateToolhelp32Snapshot 进程枚举方法时,会创建一个快照并执行字符串比较以确定进程名称是否与预期目标进程匹配。该方法的问题在于,当有多个进程实例以不同的权限级别运行时,在字符串比较期间无法区分它们。例如,一些 svchost.exe 进程以普通用户权限运行,而另一些则以提升的权限运行。在字符串比较期间无法确定 svchost.exe 的权限级别。因此,判断它是否为特权进程的唯一指标是 OpenProcess 调用是否失败(假设实现是以普通用户权限运行)。
另一方面,使用 EnumProcesses 进程枚举方法提供了 PID 和进程句柄,目标是获取进程名称。此方法保证成功,因为进程句柄已经存在(或者更准确地说,我们是先尝试打开句柄,能打开才能获取名称,这本身就是一种过滤)。
代码演练#
本节将解释基于 微软的进程枚举示例 ↗ 的代码片段。
PrintProcesses 函数#
PrintProcesses 是一个自定义函数,用于打印枚举进程的进程名称和 PID。只能检索与实现运行具有相同权限的进程的信息。关于提升进程的信息无法检索,同样,假设实现是以普通用户权限运行。尝试使用 OpenProcess 打开高权限进程的句柄将导致 ERROR_ACCESS_DENIED 错误。
可以使用 OpenProcess 的响应作为指示器来确定是否可以定位该进程。无法打开句柄的进程不能作为目标,而成功打开句柄的进程可以作为目标。
BOOL PrintProcesses() {
// 定义一个足够大的数组来存储 PID
DWORD adwProcesses [1024 * 2],
dwReturnLen1 = NULL,
dwReturnLen2 = NULL,
dwNmbrOfPids = NULL;
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR szProc [MAX_PATH];
// 获取 PID 数组
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen1)) {
printf("[!] EnumProcesses Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 计算数组中的元素数量 (返回的总字节数 / 每个 PID 的大小)
dwNmbrOfPids = dwReturnLen1 / sizeof(DWORD);
printf("[i] Number Of Processes Detected : %d \n", dwNmbrOfPids);
for (int i = 0; i < dwNmbrOfPids; i++) {
// 如果进程 ID 不为 NULL (PID 0 是系统空闲进程,通常忽略)
if (adwProcesses[i] != NULL) {
// 打开进程句柄
// 只需要查询信息和读取 VM 的权限即可获取名称
if ((hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, adwProcesses[i])) != NULL) {
// 如果句柄有效
// 获取进程 'hProcess' 中的一个模块句柄
// 'GetModuleBaseName' 需要模块句柄
// 第一个模块通常是主 exe
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwReturnLen2)) {
printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// 如果 EnumProcessModules 成功
// 获取 'hProcess' 的名称并将其保存在 'szProc' 变量中
if (!GetModuleBaseName(hProcess, hModule, szProc, sizeof(szProc) / sizeof(WCHAR))) {
printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// 打印进程名称及其 PID
wprintf(L"[%0.3d] Process \"%s\" - Of Pid : %d \n", i, szProc, adwProcesses[i]);
}
}
// 关闭进程句柄
CloseHandle(hProcess);
}
}
// 遍历 PID 数组
}
return TRUE;
}cGetRemoteProcessHandle 函数#
下面的代码片段是对前面 PrintProcesses 函数的更新。GetRemoteProcessHandle 将执行与 PrintProcesses 相同的任务,除了它将返回指定进程的句柄。
更新后的函数使用 wcscmp 来验证目标进程。此外,OpenProcess 的访问控制从 PROCESS_QUERY_INFORMATION | PROCESS_VM_READ 更改为 PROCESS_ALL_ACCESS,以提供对返回的进程对象的更多访问权限(方便后续注入)。
BOOL GetRemoteProcessHandle(LPCWSTR szProcName, DWORD* pdwPid, HANDLE* phProcess) {
DWORD adwProcesses [1024 * 2],
dwReturnLen1 = NULL,
dwReturnLen2 = NULL,
dwNmbrOfPids = NULL;
HANDLE hProcess = NULL;
HMODULE hModule = NULL;
WCHAR szProc [MAX_PATH];
// 获取 PID 数组
if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen1)) {
printf("[!] EnumProcesses Failed With Error : %d \n", GetLastError());
return FALSE;
}
// 计算数组中的元素数量
dwNmbrOfPids = dwReturnLen1 / sizeof(DWORD);
printf("[i] Number Of Processes Detected : %d \n", dwNmbrOfPids);
for (int i = 0; i < dwNmbrOfPids; i++) {
// 如果 PID 不为 NULL
if (adwProcesses[i] != NULL) {
// 打开进程句柄 (这次使用 PROCESS_ALL_ACCESS,因为我们要注入它)
if ((hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, adwProcesses[i])) != NULL) {
// 如果句柄有效
// 获取进程 'hProcess' 中的模块句柄。
// 'GetModuleBaseName' 需要模块句柄
if (!EnumProcessModules(hProcess, &hModule, sizeof(HMODULE), &dwReturnLen2)) {
printf("[!] EnumProcessModules Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// 如果 EnumProcessModules 成功
// 获取 'hProcess' 的名称并将其保存在 'szProc' 变量中
if (!GetModuleBaseName(hProcess, hModule, szProc, sizeof(szProc) / sizeof(WCHAR))) {
printf("[!] GetModuleBaseName Failed [ At Pid: %d ] With Error : %d \n", adwProcesses[i], GetLastError());
}
else {
// 执行比较逻辑
if (wcscmp(szProcName, szProc) == 0) {
wprintf(L"[+] FOUND \"%s\" - Of Pid : %d \n", szProc, adwProcesses[i]);
// 通过引用返回
*pdwPid = adwProcesses[i];
*phProcess = hProcess;
break;
}
}
}
CloseHandle(hProcess);
}
}
}
// 检查 pdwPid 或 phProcess 是否为 NULL
if (*pdwPid == NULL || *phProcess == NULL)
return FALSE;
else
return TRUE;
}cPrintProcesses - 示例#

GetRemoteProcessHandle - 示例#

🎯 总结#
在本模块中,我们学习了:
- PSAPI 枚举:使用
EnumProcesses作为CreateToolhelp32Snapshot的替代方案。 - 获取进程名:通过
OpenProcess->EnumProcessModules->GetModuleBaseName链来获取 PID 对应的名称。 - 权限过滤:
OpenProcess的成功与否隐式地过滤掉了我们无法访问(权限不足)的进程。
💡 关键要点
- 多样性:不要只依赖一种 API。EDR 可能会 hook
CreateToolhelp32Snapshot,但可能忘记 hookEnumProcesses。- 局限性:
EnumProcesses需要打开进程句柄才能获取名称,这意味着你无法列出那些你没有权限访问的进程(如 System 进程或受保护进程 PPL)。Snapshot 方法则可以列出所有进程,即使你无法打开它们。- 库依赖:PSAPI 函数需要链接
Psapi.lib。
📚 下一步学习
下一个模块将介绍 Process Enumeration - NtQuerySystemInformation。我们将深入内核层 API,学习更底层的进程枚举技术!