使用 NtQueryInformationProcess 获取进程信息






4.74/5 (14投票s)
获取进程的父进程 ID、命令行等信息
引言
本文将介绍一种主要使用 NtQueryInformationProcess()
和 ReadProcessMemory()
函数来读取进程以下项目的方法:
- 进程 ID
- 父节点 ID
- 亲和力掩码
- 退出代码状态
- 进程命令行
- 进程映像文件路径
- 终端服务会话 ID
- 进程当前是否处于调试状态的标志
- 进程环境块 (PEB) 的地址
这些信息将返回到一个声明为 smPROCESSINFO
结构的变量中。该结构定义在 NtProcessInfo.h 文件中。
typedef struct _smPROCESSINFO { DWORD dwPID; DWORD dwParentPID; DWORD dwSessionID; DWORD dwPEBBaseAddress; DWORD dwAffinityMask; LONG dwBasePriority; LONG dwExitStatus; BYTE cBeingDebugged; TCHAR szImgPath[MAX_UNICODE_PATH]; TCHAR szCmdLine[MAX_UNICODE_PATH]; } smPROCESSINFO;
尽管有一些 Windows API 可以检索上述大多数值,但本文将展示如何在获取 Windows API 无法提供的值的同时,也获取这些值。注意:此方法使用了核心 NTDLL.DLL 中的结构和函数,这些内容在未来版本中可能会发生变化。微软建议使用 Windows API 来“安全地”获取系统信息。
检索上述信息的核心函数在 NtProcessInfo.h 和 NtProcessInfo.cpp 中提供。只需将它们包含到你的项目的 h/cpp 文件中,引用函数,然后编译即可。如果将这些文件包含在你的项目/解决方案列表中,请勿忘记将其从构建中排除。主函数 sm_GetNtProcessInfo()
需要一个进程 ID 和一个声明为 smPROCESSINFO
的变量。我建议按以下顺序调用函数(第 5 步和第 6 步可以互换):
sm_EnableTokenPrivilege
或你自定义的令牌特权函数,用于启用SE_DEBUG_NAME
。sm_LoadNTDLLFunctions
。保留返回的HMODULE
变量,以便稍后释放该库。- 获取进程 ID。可以手动指定一个,或者使用
EnumProcesses
、GetCurrentProcessId
、CreateToolhelp32Snapshot
等。 - 使用进程 ID 和
smPROCESSINFO
变量调用sm_GetNtProcessInfo
。 - 将你的
smPROCESSINFO
变量/数组的内容输出到你想要的介质。 - 使用从
sm_LoadNTDLLFunctions
返回的HMODULE
变量调用sm_FreeNTDLLFunctions
。
本文中的演示应用程序是一个基本的 Win32 应用程序,其中包含一个 ListView 公共控件子窗口,用于列出进程内容。该代码在 MFC 应用程序中也能正常使用。此代码是用 Visual Studio .NET 2003 SP1 编写的,适用于 Win2K 或更高版本。
启用调试特权
为了让当前用户读取大多数进程的信息,我们必须启用调试特权。用户令牌或用户所属的用户组令牌必须已分配调试特权。要确定令牌具有哪些特权,请使用 GetTokenInformation()
函数。对于我们的函数,我们将 SE_DEBUG_NAME
作为参数传递,如果函数成功启用了特权,它将返回 TRUE
。
BOOL sm_EnableTokenPrivilege(LPCTSTR pszPrivilege) { HANDLE hToken = 0; TOKEN_PRIVILEGES tkp = {0}; // Get a token for this process. if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) { return FALSE; } // Get the LUID for the privilege. if(LookupPrivilegeValue(NULL, pszPrivilege, &tkp.Privileges[0].Luid)) { tkp.PrivilegeCount = 1; // one privilege to set tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // Set the privilege for this process. AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES)NULL, 0); if (GetLastError() != ERROR_SUCCESS) return FALSE; return TRUE; } return FALSE; }
枚举进程 ID
为了获取正在运行的进程列表,我们将使用进程状态 API EnumProcesses()
。有几种方法可以获取进程 ID。引言中提到了几种。有了进程 ID,我们就可以调用 sm_GetNtProcessInfo()
函数来填充我们的 smPROCESSINFO
变量。为了演示方便,我将数组限制为 50 个进程(定义为 MAX_PI
)。
DWORD EnumProcesses2Array(smPROCESSINFO lpi[MAX_PI]) { DWORD dwPIDs[MAX_PI] = {0}; DWORD dwArraySize = MAX_PI * sizeof(DWORD); DWORD dwSizeNeeded = 0; DWORD dwPIDCount = 0; //== only to have better chance to read processes ===== if(!sm_EnableTokenPrivilege(SE_DEBUG_NAME)) return 0; // Get a list of Process IDs of current running processes if(EnumProcesses((DWORD*)&dwPIDs, dwArraySize, &dwSizeNeeded)) { HMODULE hNtDll = sm_LoadNTDLLFunctions(); if(hNtDll) { // Get detail info on each process dwPIDCount = dwSizeNeeded / sizeof(DWORD); for(DWORD p = 0; p < MAX_PI && p < dwPIDCount; p++) { if(sm_GetNtProcessInfo(dwPIDs[p], &lpi[p])) { // Do something else upon success } } sm_FreeNTDLLFunctions(hNtDll); } } // Return either PID count or MAX_PI whichever is smaller return (DWORD)(dwPIDCount > MAX_PI) ? MAX_PI : dwPIDCount; }
访问 NTDLL 函数
NtQueryInformationProcess()
函数没有导入库,所以我们必须使用运行时动态链接 来访问 ntdll.dll 中的此函数。在头文件中定义该函数,然后使用 GetProcAddress()
获取入口点地址。
typedef NTSTATUS (NTAPI *pfnNtQueryInformationProcess)( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL ); pfnNtQueryInformationProcess gNtQueryInformationProcess; HMODULE sm_LoadNTDLLFunctions() { // Load NTDLL Library and get entry address // for NtQueryInformationProcess HMODULE hNtDll = LoadLibrary(_T("ntdll.dll")); if(hNtDll == NULL) return NULL; gNtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(hNtDll, "NtQueryInformationProcess"); if(gNtQueryInformationProcess == NULL) { FreeLibrary(hNtDll); return NULL; } return hNtDll; }
获取进程基本信息
我们使用 PROCESS_QUERY_INFORMATION
访问权限打开进程以获取基本信息,并且由于我们将使用 ReadProcessMemory()
函数来读取 PEB,因此进程还必须使用 PROCESS_VM_READ
访问权限打开。
// Attempt to access process HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, dwPID); if(hProcess == INVALID_HANDLE_VALUE) { return FALSE; }
现在,我们为 PROCESS_BASIC_INFORMATION
结构变量分配内存。
// Try to allocate buffer hHeap = GetProcessHeap(); dwSize = sizeof(smPROCESS_BASIC_INFORMATION); pbi = (smPPROCESS_BASIC_INFORMATION)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwSize); // Did we successfully allocate memory if(!pbi) { CloseHandle(hProcess); return FALSE; }
此结构定义在 winternl.h 和 ntddk.h 中。下面的定义来自 Win2003 DDK ntddk.h,因为我的 Visual Studio 2003 中的 winternl.h 和微软 MSDN 上的那个都没有那么详细。我还发现 winternl.h 和 ntddk.h 在编译时会相互冲突,所以我决定在我的头文件 NtProcessInfo.h(包含在下载文件中)中,用一个新的名称(在前面加上 sm 前缀)复制最新的定义。
typedef struct _smPROCESS_BASIC_INFORMATION { LONG ExitStatus; smPPEB PebBaseAddress; ULONG_PTR AffinityMask; LONG BasePriority; ULONG_PTR UniqueProcessId; ULONG_PTR InheritedFromUniqueProcessId; } smPROCESS_BASIC_INFORMATION, *smPPROCESS_BASIC_INFORMATION;
然后,我们通过 NtQueryInformationProcess()
函数获取进程的基本信息。
// Attempt to get basic info on process NTSTATUS dwStatus = gNtQueryInformationProcess(hProcess, ProcessBasicInformation, pbi, dwSize, &dwSizeNeeded); // Did we successfully get basic info on process if(dwStatus >= 0) { // Basic Info spi.dwPID = (DWORD)pbi->UniqueProcessId; spi.dwParentPID = (DWORD)pbi->InheritedFromUniqueProcessId; spi.dwBasePriority = (LONG)pbi->BasePriority; spi.dwExitStatus = (NTSTATUS)pbi->ExitStatus; spi.dwPEBBaseAddress = (DWORD)pbi->PebBaseAddress; spi.dwAffinityMask = (DWORD)pbi->AffinityMask;
读取 PEB
从基本信息中,我们已经获得了 PEB 的基地址(如果存在)在 PebBaseAddress
指针变量中。如果地址不为零,我们只需将此地址传递给 ReadProcessMemory()
函数。如果成功,它应该会在我们的 PEB 结构变量中返回进程信息,该变量还包含 BeingDebugged
和 SessionId
变量。
// Read Process Environment Block (PEB) if(pbi->PebBaseAddress) { if(ReadProcessMemory(hProcess, pbi->PebBaseAddress, &peb, sizeof(peb), &dwBytesRead)) { spi.dwSessionID = (DWORD)peb.SessionId; spi.cBeingDebugged = (BYTE)peb.BeingDebugged;
PEB
结构定义为
typedef struct _smPEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; smPPEB_LDR_DATA Ldr; smPRTL_USER_PROCESS_PARAMETERS ProcessParameters; BYTE Reserved4[104]; PVOID Reserved5[52]; smPPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved6[128]; PVOID Reserved7[1]; ULONG SessionId; } smPEB, *smPPEB;
从这一点开始,我们还可以获得 Ldr
的内存地址,但本文不使用它。基本上,PEB_LDR_DATA
结构包含进程中加载模块的双向链表。我们还可以获得 ProcessParameters
的内存地址,这将为我们提供 CommandLine
...
// We got Process Parameters, is CommandLine filled in if(peb_upp.CommandLine.Length > 0) { // Yes, try to read CommandLine pwszBuffer = (WCHAR *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, peb_upp.CommandLine.Length); // If memory was allocated, continue if(pwszBuffer) { if(ReadProcessMemory(hProcess, peb_upp.CommandLine.Buffer, pwszBuffer, peb_upp.CommandLine.Length, &dwBytesRead)) { // if commandline is larger than our variable, truncate if(peb_upp.CommandLine.Length >= sizeof(spi.szCmdLine)) dwBufferSize = sizeof(spi.szCmdLine) - sizeof(TCHAR); else dwBufferSize = peb_upp.CommandLine.Length; // Copy CommandLine to our structure variable #if defined(UNICODE) || (_UNICODE) // Since core NT functions operate in Unicode // there is no conversion if application is // compiled for Unicode StringCbCopyN(spi.szCmdLine, sizeof(spi.szCmdLine), pwszBuffer, dwBufferSize); #else // Since core NT functions operate in Unicode // we must convert to Ansi since our application // is not compiled for Unicode WideCharToMultiByte(CP_ACP, 0, pwszBuffer, (int)(dwBufferSize / sizeof(WCHAR)), spi.szCmdLine, sizeof(spi.szCmdLine), NULL, NULL); #endif } if(!HeapFree(hHeap, 0, pwszBuffer)) { // failed to free memory bReturnStatus = FALSE; goto gnpiFreeMemFailed; } } } // Read CommandLine in Process Parameters
...以及 ImagePathName
变量,它们是 UNICODE_STRING
结构。Unicode 路径可能长达 32K 个字符,通常以 '\\?\' 或 '\??\' 作为前缀。由于原生 NT API 操作 Unicode,当调用应用程序不是为 Unicode 编译并且使用 TCHAR
而不是 WCHAR
时,我们需要将缓冲区转换为 ANSI。
// We got Process Parameters, is ImagePathName filled in if(peb_upp.ImagePathName.Length > 0) { // Yes, try to read Image Path pwszBuffer = (WCHAR *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, peb_upp.ImagePathName.Length); // If memory was allocated, continue if(pwszBuffer) { if(ReadProcessMemory(hProcess, peb_upp.ImagePathName.Buffer, pwszBuffer, peb_upp.ImagePathName.Length, &dwBytesRead)) { // if image path is larger than our variable, truncate if(peb_upp.ImagePathName.Length >= sizeof(spi.szImgPath)) dwBufferSize = sizeof(spi.szImgPath) - sizeof(TCHAR); else dwBufferSize = peb_upp.ImagePathName.Length; // Copy ImagePathName to our structure variable #if defined(UNICODE) || (_UNICODE) // Since core NT functions operate in Unicode // there is no conversion if application is // compiled for Unicode StringCbCopyN(spi.szImgPath, sizeof(spi.szImgPath), pwszBuffer, dwBufferSize); #else // Since core NT functions operate in Unicode // we must convert to Ansi since our application // is not compiled for Unicode WideCharToMultiByte(CP_ACP, 0, pwszBuffer, (int)(dwBufferSize / sizeof(WCHAR)), spi.szImgPath, sizeof(spi.szImgPath), NULL, NULL); #endif } if(!HeapFree(hHeap, 0, pwszBuffer)) { // failed to free memory bReturnStatus = FALSE; goto gnpiFreeMemFailed; } } } // Read ImagePathName in Process Parameters
对于系统进程(XP 及更高版本 PID 为 4,Win2K 为 8,NT 4 为 2),我们手动指定进程路径,因为我们知道它是 %SystemRoot%\System32\ntoskrnl.exe,但 API 不返回它。如果存在对称多处理 (SMP),Ntoskrnl.exe 也可能是 ntkrnlmp.exe,或者如果存在物理地址扩展 (PAE),则为 ntkrnlpa.exe。无论哪种情况,实际文件名都将是 ntoskrnl.exe,但文件版本块中的 OriginalFilename
字段将包含真实名称。我们使用 ExpandEnvironmentStrings()
API 将 %SystemRoot% 系统环境变量替换为 Windows 的实际根路径。
// System process for WinXP and later is PID 4 and we cannot access // PEB, but we know it is aka ntoskrnl.exe so we will manually define it if(spi.dwPID == 4) { ExpandEnvironmentStrings(_T("%SystemRoot%\\System32\\ntoskrnl.exe"), spi.szImgPath, sizeof(spi.szImgPath)); }
如上文关于 PROCESS_BASIC_INFORMATION
结构所述,RTL_USER_PROCESS_PARAMETERS
结构定义在 winternl.h 和 ntddk.h 中。
typedef struct _smRTL_USER_PROCESS_PARAMETERS { BYTE Reserved1[16]; PVOID Reserved2[10]; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; } smRTL_USER_PROCESS_PARAMETERS, *smPRTL_USER_PROCESS_PARAMETERS;
清理。
在这里,我们释放之前加载的 NTDLL.DLL。就这样!
void sm_FreeNTDLLFunctions(HMODULE hNtDll) { if(hNtDll) FreeLibrary(hNtDll); gNtQueryInformationProcess = NULL; }
关注点
我写这篇文章是为了分享我在尝试获取进程信息以构建一个基本的进程查看器时学到的一种方法,而没有使用简单的工具帮助函数。据说,工具帮助函数从 Win9x 开始,并在 XP 开始的 NT 系统中被勉强采纳。我想深入研究一下,尝试使用 NT 原生 API 来积累经验。
用于获取另一个进程信息的“安全”Windows API 函数包括:
ProcessIdToSessionId
CheckRemoteDebuggerPresent
GetExitCodeProcess
GetProcessAffinityMask
GetProcessImageFileName
CreateRemoteThread
- 其他进程和线程函数
注意:我在调试演示应用程序时收到以下消息,但它似乎发生在 WinMain 之前,并且似乎已被处理,因为没有抛出第二次机会异常。
"First-chance exception at 0x7c918fea in GetNtProcessInfo.exe:
0xC0000005: Access violation writing location 0x00000010."
在 NtProcessInfo.h 和 NtProcessInfo.cpp 的实际实现中,我没有收到此异常。
历史
- 初始发布。