跳至主要內容

34-进程枚举 - NtQuerySystemInformation

Maldevacademy大约 5 分钟安全开发

简介

本模块将讨论一种更独特的方式来使用 NtQuerySystemInformation 进行进程枚举,它是一个 系统调用(有关系统调用的更多信息,请见后面)。NtQuerySystemInformation 是从 ntdll.dll 模块导出的,因此需要使用 GetModuleHandleGetProcAddress

Microsoft 的文档open in new window 中提到,NtQuerySystemInformation 能够返回大量有关系统的信息。本模块将重点介绍如何使用它来执行进程枚举。

获取 NtQuerySystemInformation 的地址

如前所述,需要使用 GetProcAddressGetModuleHandlentdll.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 变量的指针。

由于目的是枚举进程,因此将使用 SystemProcessInformationopen in new window 标记。使用此标记将使函数通过 SystemInformation 参数返回一个 SYSTEM_PROCESS_INFORMATION 结构的数组(对于系统中运行的每个进程一个)。

image
image

SYSTEM_PROCESS_INFORMATION 结构

下一步是查看 微软文档open in new window 来了解 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 (0xC0000004open in new window) 错误,因为我们仅仅简单地传递这些参数是为了获取数组大小。

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 成员。

图像
图像

此模块中提供的代码使用不同版本的 SYSTEM_PROCESS_INFORMATION 结构。无论如何,Microsoft 的版本和模块代码中使用的版本都会导致相同的结果。主要区别在于此模块中使用的结构包含更多信息,而 Microsoft 的受限版本包含多个 Reserved 成员。此外,还使用了另一个 SYSTEM_INFORMATION_CLASS 结构版本,该版本也比 Microsoft 的版本有更多文档说明。可以通过以下链接查看这两个结构。

Demo

下图展示了本模块中展示的代码编译并运行后的输出。目标进程在 Windows 10 上为 notepad.exe,在 Windows 11 上为 Notepad.exe

image
image