34-进程枚举 - NtQuerySystemInformation
简介
本模块将讨论一种更独特的方式来使用 NtQuerySystemInformation
进行进程枚举,它是一个 系统调用(有关系统调用的更多信息,请见后面)。NtQuerySystemInformation
是从 ntdll.dll
模块导出的,因此需要使用 GetModuleHandle
和 GetProcAddress
。
Microsoft 的文档 中提到,NtQuerySystemInformation
能够返回大量有关系统的信息。本模块将重点介绍如何使用它来执行进程枚举。
获取 NtQuerySystemInformation 的地址
如前所述,需要使用 GetProcAddress
和 GetModuleHandle
从 ntdll.dll
中获取 NtQuerySystemInformation
的地址。
// 函数指针
typedef NTSTATUS (NTAPI* fnNtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
// 指向 NtQuerySystemInformation 函数的函数指针
fnNtQuerySystemInformation pNtQuerySystemInformation = NULL;
// 获取 NtQuerySystemInformation 的地址
pNtQuerySystemInformation = (fnNtQuerySystemInformation)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQuerySystemInformation");
if (pNtQuerySystemInformation == NULL) {
printf("[!] GetProcAddress 失败,错误代码:%d\n", GetLastError());
return FALSE;
}
NtQuerySystemInformation 的参数
NtQuerySystemInformation
的参数如下所示。
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
SystemInformationClass
- 决定函数返回哪种类型的系统信息。SystemInformation
- 指向将接收所请求信息的缓冲区的指针。返回的信息将以根据SystemInformationClass
参数指定类型的结构的形式返回。SystemInformationLength
- 由SystemInformation
参数指向的缓冲区大小(以字节为单位)。ReturnLength
- 指向将接收写入SystemInformation
的信息的实际大小的 ULONG 变量的指针。
由于目的是枚举进程,因此将使用 SystemProcessInformation 标记。使用此标记将使函数通过 SystemInformation
参数返回一个 SYSTEM_PROCESS_INFORMATION
结构的数组(对于系统中运行的每个进程一个)。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/nt-108508463-27e8a0b8-4d4e-4391-bf1d-8d75ad2567d3.png)
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;
重点关注 UNICODE_STRING ImageName
(包含进程名)和 UniqueProcessId
(即进程 ID)。此外,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 失败了,错误代码:%d\n", GetLastError());
return FALSE;
}
// 第二次 NtQuerySystemInformation 调用
// 使用正确的参数调用 NtQuerySystemInformation,输出将保存到 'SystemProcInfo'
STATUS = pNtQuerySystemInformation(SystemProcessInformation, SystemProcInfo, uReturnLen1, &uReturnLen2);
if (STATUS != 0x0) {
printf("[!] NtQuerySystemInformation 失败了,错误代码:0x%0.8X \n", STATUS);
return FALSE;
}
遍历进程
现在已经成功地检索出了数组,下一步是对它进行循环,并访问保存进程名称的 ImageName.Buffer
。每次迭代都会把进程名称与目标进程名称进行比较。
想要访问数组中 SYSTEM_PROCESS_INFORMATION
类型的每个元素,必须使用 NextEntryOffset
成员。若要查找下一个元素的地址,请把前一个元素的地址添加到 NextEntryOffset
中。如下面的代码段所示。
// 'SystemProcInfo' 现在表示数组中的一个新元素
SystemProcInfo = (PSYSTEM_PROCESS_INFORMATION)((UINT_PTR)SystemProcInfo + SystemProcInfo->NextEntryOffset);
释放已分配内存
在把 SystemProcInfo
移动到数组的新元素之前,已分配内存的初始地址需要被保存,以便以后可以被释放。因此,在循环开始之前,该地址需要被保存到一个临时变量中。
// 由于我们会修改 'SystemProcInfo',我们会在 while 循环之前保存其初始值以便以后释放它
pValueToFree = SystemProcInfo;
使用 NtQuerySystemInformation
枚举进程
下面显示了使用 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;
}
NtQuerySystemInformation
的未文档化部分
NtQuerySystemInformation
仍然是未完全文档化的,很大一部分仍然是未知的。例如,注意 SYSTEM_PROCESS_INFORMATION
中的 Reserved
成员。
![图像](https://maldevacademy.s3.amazonaws.com/images/Intermediate/nt-208666134-5c070d23-50f4-4e1d-978f-11122892a9c3.png)
此模块中提供的代码使用不同版本的 SYSTEM_PROCESS_INFORMATION
结构。无论如何,Microsoft 的版本和模块代码中使用的版本都会导致相同的结果。主要区别在于此模块中使用的结构包含更多信息,而 Microsoft 的受限版本包含多个 Reserved
成员。此外,还使用了另一个 SYSTEM_INFORMATION_CLASS
结构版本,该版本也比 Microsoft 的版本有更多文档说明。可以通过以下链接查看这两个结构。
ReactOS 文档 中的
SYSTEM_PROCESS_INFORMATION
System Informer 文档 中的
SYSTEM_INFORMATION_CLASS
Demo
下图展示了本模块中展示的代码编译并运行后的输出。目标进程在 Windows 10 上为 notepad.exe
,在 Windows 11 上为 Notepad.exe
。
![image](https://maldevacademy.s3.amazonaws.com/images/Intermediate/nt-308665154-9c8bdf73-bfb4-40b5-a39f-3b6ee2044076.png)