进程枚举技术探讨

在Windows操作系统中,枚举当前运行的进程是一个常见的需求,但遗憾的是,并没有一个统一的标准方法来实现这一功能。在Windows 95和Windows 98中,可以使用ToolHelp API函数(位于Kernel32.dll中)。然而,微软NT团队出于某些原因并不喜欢ToolHelp函数,因此决定不在Windows NT中加入这些函数。相反,他们提供了自己的一组进程状态函数,即PSAPI,并将其添加到一个外部模块(位于psapi.dll中)。最终,在Windows 2000中,开发者选择提供这两种枚举方法。

CEnumProcess类简介

CEnumProcess是一个简单的类,用于使用PSAPI或ToolHelp枚举正在运行的进程。首选的方法是在运行时决定的。它包含两个类:CEnumProcess::CProcessEntry和CEnumProcess::CModuleEntry,用于存储结果。

在创建类的实例时,它尝试加载相应的模块并找到PSAPI/ToolHelp相关的函数。根据找到的函数集,类将枚举方法设置为最合适的。例如,以下是找到PSAPI相关函数的代码:

Try to load psapi.dll PSAPI = ::LoadLibrary(TEXT("PSAPI")); if (PSAPI) { Find PSAPI functions FEnumProcesses = (PFEnumProcesses)::GetProcAddress(PSAPI, TEXT("EnumProcesses")); FEnumProcessModules = (PFEnumProcessModules)::GetProcAddress(PSAPI, TEXT("EnumProcessModules")); #ifdef UNICODE FGetModuleFileNameEx = (PFGetModuleFileNameEx)::GetProcAddress(PSAPI, TEXT("GetModuleFileNameExW")); #else FGetModuleFileNameEx = (PFGetModuleFileNameEx)::GetProcAddress(PSAPI, TEXT("GetModuleFileNameExA")); #endif }

类中有七个公共函数:

  • int GetAvailableMethods()
  • int GetSuggestedMethod()
  • int SetMethod(int method)
  • BOOL GetProcessNext(CProcessEntry *pEntry)
  • BOOL GetProcessFirst(CProcessEntry* pEntry)
  • BOOL GetModuleNext(DWORD dwPID, CModuleEntry* pEntry)
  • BOOL GetModuleFirst(DWORD dwPID, CModuleEntry* pEntry)

枚举方法相关的函数设置/返回在ENUM_METHOD命名空间中找到的值之一:

namespace ENUM_METHOD { const int NONE = 0x0; const int PSAPI = 0x1; const int TOOLHELP = 0x2; const int PROC16 = 0x4; }

ENUM_METHOD::NONE用于不太可能发生的情况,例如NT用户删除了psapi.dll。在Windows 2000下,GetAvailableMethods返回ENUM_METHOD::PSAPI + ENUM_METHOD::TOOLHELP。建议的方法是使用ToolHelp API。如果要枚举16位进程,应将ENUM_METHOD::PROC16添加到枚举方法中。这通常默认完成。

这些函数接受一个指向CProcessEntry/CModuleEntry的指针作为输入,并根据枚举成功或失败返回TRUE/FALSE。类中有用的成员如下:

CProcessEntry { LPTSTR lpFilename; // 文件名 DWORD dwPID; // 进程ID WORD hTask16; // 如果这是一个16位进程,返回任务句柄,否则为0 }

成员hTask16仅在NT和Win2k上使用。如果在这些操作系统上hTask16不为0,则这是一个16位进程。在这种情况下,lpFilename将是16位进程的路径,而dwPID将是当前运行的NTVDM的标识符(参见"关于16位进程的说明")。

发现许多模块没有加载到它们的首选基址。如果一个模块没有加载到它的首选基址,使用它的应用程序将需要更多的内存,并在初始化时遭受性能损失。这是因为只有当基址等于加载地址时,模块才能映射到磁盘上的.dll上。

PVOID CEnumProcess::GetModulePreferredBase(DWORD dwPID, PVOID pModBase) { if (ENUM_METHOD::NONE == m_method) return NULL; HANDLE hProc = OpenProcess(PROCESS_VM_READ, FALSE, dwPID); if (hProc) { IMAGE_DOS_HEADER idh = {0}; IMAGE_NT_HEADERS inh = {0}; ReadProcessMemory(hProc, pModBase, &idh, sizeof(idh), NULL); if (IMAGE_DOS_SIGNATURE == idh.e_magic) { ReadProcessMemory(hProc, (PBYTE)pModBase + idh.e_lfanew, &inh, sizeof(inh), NULL); CloseHandle(hProc); if (IMAGE_NT_SIGNATURE == inh.Signature) return (PVOID) inh.OptionalHeader.ImageBase; } } return NULL; }

对于不熟悉PE格式的人来说,这些是可执行文件的标准头。当可执行文件加载到内存中时,整个文件都会被映射,包括头。这里可以找到很多有用的信息,比如映射文件的大小。

在Windows 95和Windows 98中,ToolHelp API像处理任何其他进程一样处理16位应用程序。在NT下则不是这样。相反,CEnumProcess将返回它当前运行的NT虚拟DOS机(NTVDM)的名称。这意味着如果运行测试应用程序并找到一些像"ntvdm.exe"这样的进程名,就找到了一个16位进程。如果ENUM_METHOD::PROC16当前被设置,GetNextProcess返回的下一个条目将是这个虚拟机中当前运行的16位进程。

有些进程具有安全属性,可以防止读取其内存。这意味着无法枚举模块。如果使用PSAPI,甚至无法检索到文件名。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485