模块 34 - 进程枚举 - NtQuerySystemInformation
恶意软件开发课程 - 进程枚举 - NtQuerySystemInformation
模块 34 - 进程枚举 - NtQuerySystemInformation#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
进程枚举 - NtQuerySystemInformation#
简介#
本模块讨论了一种更独特的进程枚举方法,使用 NtQuerySystemInformation,这是一个 syscall(稍后将详细介绍 syscall)。NtQuerySystemInformation 从 ntdll.dll 模块导出,因此需要使用 GetModuleHandle 和 GetProcAddress 来获取其地址。
微软关于 NtQuerySystemInformation 的文档 ↗ 显示它可以返回有关系统的许多信息。本模块的重点是使用它来执行进程枚举。
💡 初学者提示:WinAPI vs NTAPI
- WinAPI (如 Kernel32.dll):微软官方记录并推荐使用的 API。它们通常只是对底层 NTAPI 的封装。
- NTAPI (如 Ntdll.dll):直接与内核交互的底层 API。
- 为什么用 NTAPI?:
- 绕过 Hook:安全软件通常 Hook 高层的 WinAPI (如
CreateToolhelp32Snapshot),直接调用 NTAPI 可能绕过监控。- 更多信息:NTAPI 通常能提供 WinAPI 无法提供的详细底层信息。
- 更难分析:对于不熟悉内核结构的分析人员来说,代码更难理解。
获取 NtQuerySystemInformation 的地址#
如前所述,需要 GetProcAddress 和 GetModuleHandle 从 ntdll.dll 中检索 NtQuerySystemInformation 的地址。
// 函数指针定义
typedef NTSTATUS (NTAPI* fnNtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
fnNtQuerySystemInformation pNtQuerySystemInformation = NULL;
// 获取 NtQuerySystemInformation 的地址
pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation == NULL) {
printf("[!] GetProcAddress Failed With Error : %d\n", GetLastError());
return FALSE;
}cNtQuerySystemInformation 参数#
NtQuerySystemInformation 的参数如下所示。
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);cSystemInformationClass- 决定函数返回什么类型的系统信息。SystemInformation- 指向接收请求信息的缓冲区的指针。返回的信息将采用根据SystemInformationClass参数指定的类型的结构形式。SystemInformationLength-SystemInformation参数指向的缓冲区的大小,以字节为单位。ReturnLength- 指向 ULONG 变量的指针,该变量将接收写入SystemInformation的信息的实际大小。
由于目标是进程枚举,因此将使用 SystemProcessInformation ↗ 标志。使用此标志将使函数返回 SYSTEM_PROCESS_INFORMATION 结构数组(通过 SystemInformation 参数),系统中的每个正在运行的进程对应一个结构。

SYSTEM_PROCESS_INFORMATION 结构#
下一步是查看 微软的文档 ↗ 以了解 SYSTEM_PROCESS_INFORMATION 结构的样子。
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
PVOID Reserved2;
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;c重点关注包含进程名称的 UNICODE_STRING ImageName 和进程 ID 的 UniqueProcessId。此外,NextEntryOffset 将用于移动到返回数组中的下一个元素。
由于使用 SystemProcessInformation 标志调用 NtQuerySystemInformation 将返回未知大小的 SYSTEM_PROCESS_INFORMATION 数组,因此需要调用 NtQuerySystemInformation 两次。第一次调用将检索数组大小,用于分配缓冲区,然后第二次调用将使用分配的缓冲区。
预计第一次 NtQuerySystemInformation 调用将失败,并出现 STATUS_INFO_LENGTH_MISMATCH (0xC0000004 ↗) 错误,因为传递了无效参数仅仅是为了检索数组大小。
ULONG uReturnLen1 = NULL,
uReturnLen2 = NULL;
PSYSTEM_PROCESS_INFORMATION SystemProcInfo = NULL;
NTSTATUS STATUS = NULL;
// 第一次 NtQuerySystemInformation 调用
// 这将失败并返回 STATUS_INFO_LENGTH_MISMATCH
// 但它将提供有关需要分配多少内存的信息 (uReturnLen1)
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());
return FALSE;
}
// 第二次 NtQuerySystemInformation 调用
// 使用正确的参数调用 NtQuerySystemInformation,输出将保存到 'SystemProcInfo'
STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2);
if (STATUS != 0x0) {
printf("[!] NtQuerySystemInformation Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}c遍历进程#
现在已成功检索数组,下一步是遍历它并访问保存进程名称的 ImageName.Buffer。每次迭代都会将进程名称与目标进程名称进行比较。
要访问数组中 SYSTEM_PROCESS_INFORMATION 类型的每个元素,必须使用 NextEntryOffset 成员。要查找下一个元素的地址,请将前一个元素的地址添加到 NextEntryOffset。如下面的代码片段所示。
// 'SystemProcInfo' 现在将表示数组中的一个新元素
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((ULONG_PTR)SystemProcInfo + SystemProcInfo->NextEntryOffset);c释放分配的内存#
在将 SystemProcInfo 移动到数组中的新元素之前,需要保存分配内存的初始地址以便稍后释放。因此,就在循环开始之前,需要将地址保存到一个临时变量中。
// 因为我们将修改 'SystemProcInfo' 指针,我们需要在 while 循环之前保存它的初始值以便稍后释放
pValueToFree = SystemProcInfo;cNtQuerySystemInformation 进程枚举#
使用 NtQuerySystemInformation 执行进程枚举的完整代码如下所示。
BOOL GetRemoteProcessHandle(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());
return FALSE;
}
pNtQuerySystemInformation(SystemProcessInformation, NULL, NULL, &uReturnLen1);
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (SIZE_T)uReturnLen1);
if (SystemProcInfo == NULL) {
printf("[!] HeapAlloc Failed With Error : %d\n", GetLastError());
return FALSE;
}
// 因为我们将修改 'SystemProcInfo',我们需要在 while 循环之前保存它的初始值以便稍后释放
pValueToFree = SystemProcInfo;
STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2);
if (STATUS != 0x0) {
printf("[!] NtQuerySystemInformation Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
while (TRUE) {
// 检查进程名称大小
// 将枚举的进程名称与预期的目标进程进行比较
if (SystemProcInfo->ImageName.Length && wcscmp(SystemProcInfo->ImageName.Buffer, szProcName) == 0) {
// 打开目标进程的句柄,保存它,然后中断
*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);
}
// 使用初始地址释放内存
HeapFree(GetProcessHeap(), 0, pValueToFree);
// 检查我们是否成功获取了目标进程句柄
if (*pdwPid == NULL || *phProcess == NULL)
return FALSE;
else
return TRUE;
}cNtQuerySystemInformation 的未文档化部分#
NtQuerySystemInformation 在很大程度上仍未记录,其中很大一部分仍然未知。例如,注意 SYSTEM_PROCESS_INFORMATION 中的 Reserved 成员。

本模块中提供的代码使用了 SYSTEM_PROCESS_INFORMATION 结构的不同版本。无论如何,微软的版本和模块代码中使用的版本都会导致相同的输出。主要区别在于本模块中使用的结构包含更多信息,而不是包含多个 Reserved 成员的微软受限版本。此外,还使用了另一个版本的 SYSTEM_INFORMATION_CLASS 结构,该结构也比微软的版本有更多的记录。可以通过下面的链接查看这两个结构。
- 来自 ReactOS 文档 ↗ 的
SYSTEM_PROCESS_INFORMATION - 来自 System Informer 文档 ↗ 的
SYSTEM_INFORMATION_CLASS
📚 知识扩展:文档的来源
既然是“未文档化”的,这些结构定义是从哪来的?
- 逆向工程:安全研究员分析 Windows 内核。
- 泄漏源码:历史上的 Windows 源码泄漏。
- 符号文件 (PDB):微软发布的调试符号有时包含结构定义。
- 开源项目:ReactOS(旨在二进制兼容 Windows 的开源 OS)和 System Informer (原 Process Hacker) 是查找这些未文档化结构的宝库。
演示#
下图显示了编译并运行本模块中显示的代码后的输出。目标进程是 notepad.exe(在 Windows 10 上)和 Notepad.exe(在 Windows 11 上)。

🎯 总结#
在本模块中,我们学习了:
- NTAPI 枚举:使用
NtQuerySystemInformation(Syscall) 进行进程枚举,这比 WinAPI 更底层,更难被传统的用户模式 Hook 监控。 - 处理未文档化结构:如何定义和使用未在官方头文件中完全披露的结构(如
SYSTEM_PROCESS_INFORMATION)。 - 内存遍历:如何使用
NextEntryOffset手动遍历内核返回的结构链表。
💡 关键要点
- 在恶意软件开发中,直接使用 NTAPI 被称为 “Native API 调用”。
- 它是规避 EDR 的第一步,因为许多 EDR 只能监控 Kernel32/User32 等高层 API。
- 下一步进化是 “Direct Syscalls”(直接系统调用),完全绕过
ntdll.dll,我们将在后续模块中深入探讨。
📚 下一步学习
下一个模块将介绍 Thread Hijacking - Local Thread Creation。我们将开始探索如何不仅仅是注入代码,而是劫持程序原本的执行流!