检测 Windows NT/2K 进程执行






4.94/5 (65投票s)
一篇关于当进程启动时如何从操作系统获取通知的文章。
摘要
拦截和跟踪进程执行是实现类似 NT 任务管理器应用程序和需要操作外部进程的系统的非常有用的机制。在启动新进程时通知相关方是开发进程监控系统和系统范围挂钩的经典问题。Win32 API 提供了一组强大的库(PSAPI 和 ToolHelp [1]),可以枚举系统中当前运行的进程。尽管这些 API 功能强大,但它们不允许您在新进程启动或结束时收到通知。本文提供了一种基于已记录接口的有效且健壮的技术来实现此目标。
解决方案
幸运的是,NT/2K 提供了一组称为“进程结构例程”[2] 的 API,由 NTOSKRNL 导出。其中一个 API PsSetCreateProcessNotifyRoutine()
提供了注册系统范围回调函数的能力,该函数在每次新进程启动、退出或终止时由操作系统调用。上述 API 可用作一种易于实现的跟踪进程的方法,只需实现一个 NT 内核模式驱动程序和一个用户模式 Win32 控制应用程序。驱动程序的作用是检测进程执行并通知控制程序有关这些事件。
要求
- 提供一个简单、高效、可靠且线程安全的机制来监控进程执行
- 解决驱动程序和用户模式应用程序之间的同步问题
- 构建一个易于使用和扩展的面向对象的 (OOP) 用户模式框架
- 允许注册和注销回调,以及动态加载和卸载内核驱动程序的能力
工作原理
控制应用程序在 HKLM\SYSTEM\CurrentControlSet\Services 下注册内核模式驱动程序并动态加载它。然后,内核驱动程序创建一个命名的事件对象,用于在发生新事件(即进程启动或结束)时向用户模式应用程序发出信号。控制应用程序打开同一个事件对象并创建一个监听线程,该线程等待此事件。接下来,用户模式应用程序向驱动程序发送请求以开始监控。驱动程序调用 PsSetCreateProcessNotifyRoutine()
,它接受两个参数。其中一个参数指定了调用者提供的回调例程的入口点,该例程负责接收来自 Windows 的所有通知。在回调中发生通知时,驱动程序会发出该事件的信号,以告知用户模式应用程序发生了某些事情。然后,控制应用程序从驱动程序获取特定事件的数据,并将其存储在特殊的队列容器中以供进一步处理。如果不再需要检测进程执行,用户模式应用程序会向驱动程序发送请求以停止监控。然后,驱动程序将禁用观察机制。稍后,控制模式应用程序可以卸载驱动程序并注销它。
设计和实现
NT 内核模式驱动程序 (ProcObsrv)
入口点 DriverEntry()
(ProcObsrv.c) 仅执行驱动程序的初始化。当驱动程序加载时,I/O 管理器会调用此函数。由于 PsSetCreateProcessNotifyRoutine()
允许注销回调,因此我在驱动程序的调度例程中实现了实际的注册和注销过程。这允许我使用单个 IOCTL(控制代码 IOCTL_PROCOBSRV_ACTIVATE_MONITORING
)动态地启动和停止监控活动。一旦注册了回调,每次进程启动或终止时,操作系统都会调用用户提供的 ProcessCallback()
。此函数填充一个缓冲区,该缓冲区将被用户模式应用程序拾取。接下来,驱动程序会发出命名的事件对象信号,从而让正在等待它的用户模式应用程序得知有可用的信息可以检索。
控制应用程序 (ConsCtl)
为了简单起见,我决定提供一个简单的控制台应用程序,将华丽的 GUI 实现留给您。设计一个多线程应用程序可以使其具有可伸缩性和更高的响应性。另一方面,非常重要的是要考虑与同步发布者(即内核驱动程序)提供的信息以及订阅者(即控制应用程序)检索的信息的访问相关的几个注意事项。另一个重要的关键点是,检测系统必须是可靠的,并确保不会丢失任何事件。为了简化设计过程,首先我需要将用户模式应用程序中负责处理驱动程序的各个实体之间的职责分配。然而,通过回答这些问题 [5] 来做到这一点并不困难:
- 系统中有什么进程
- 框架中的角色是什么
- 谁做什么以及他们如何协作
以下是说明类之间关系的 UML 类图
CApplicationScope
实现单例模式,并封装了框架的主要接口。它公开了两个公共方法来启动和停止监控过程。
class CApplicationScope { .. Other Other details ignored for the sake of simplicity .... public: // Initiates process of monitoring process BOOL StartMonitoring(PVOID pvParam); // Ends up the whole process of monitoring void StopMonitoring(); };
CProcessThreadMonitor
是一个线程,它等待由驱动程序创建的事件被信号。一旦进程创建或结束,驱动程序就会发出此事件对象的信号,CProcessThreadMonitor
的线程就会唤醒。然后,用户模式应用程序从驱动程序检索数据。接下来,将数据追加到队列容器(CQueueContainer
)的 Append()
方法中。
CQueueContainer
是一个线程安全的队列控制器,它提供了 Monitor/Condition 变量模式的实现。此类主要目的是提供队列容器的线程安全信号量实现。Append()
方法的工作原理如下:
- 锁定对聚合的 STL deque 对象的访问
- 添加数据项
- 发出
m_evtElementAvailable
事件对象的信号 - 解锁 deque
这是它的实际实现
// Insert data into the queue BOOL CQueueContainer::Append(const QUEUED_ITEM& element) { BOOL bResult = FALSE; DWORD dw = ::WaitForSingleObject(m_mtxMonitor, INFINITE); bResult = (WAIT_OBJECT_0 == dw); if (bResult) { // Add it to the STL queue m_Queue.push_back(element); // Notify the waiting thread that there is // available element in the queue for processing ::SetEvent(m_evtElementAvailable); }// ::ReleaseMutex(m_mtxMonitor); return bResult; }
由于它被设计为在队列中有可用元素时发出通知,因此它聚合了 CRetreivalThread
的实例,该实例会等待直到本地存储中出现元素。这是它的伪实现:
- 等待
m_evtElementAvailable
事件对象 - 锁定对 STL deque 对象的访问
- 提取数据项
- 解锁 deque
- 处理从队列中检索到的数据
这是添加到队列时调用的方法:
// Implement specific behavior when kernel mode driver notifies // the user-mode app void CQueueContainer::DoOnProcessCreatedTerminated() { QUEUED_ITEM element; // Initially we have at least one element for processing BOOL bRemoveFromQueue = TRUE; while (bRemoveFromQueue) { DWORD dwResult = ::WaitForSingleObject( m_mtxMonitor, INFINITE ); if (WAIT_OBJECT_0 == dwResult) { // Is there anything in the queue bRemoveFromQueue = (m_Queue.size() > 0); if (bRemoveFromQueue) { // Get the element from the queue element = m_Queue.front(); m_Queue.pop_front(); } // if else // Let's make sure that the event hasn't been // left in signaled state if there are no items // in the queue ::ResetEvent(m_evtElementAvailable); } // if ::ReleaseMutex(m_mtxMonitor); // Process it only if there is an element that has // been picked up if (bRemoveFromQueue) m_pHandler->OnProcessEvent( &element, m_pvParam ); else break; } // while }
CCustomThread
- 为了帮助管理原始线程的维护复杂性,我将所有线程相关的活动封装在一个抽象类中。它提供了一个纯虚方法 Run()
,任何特定线程类(例如 CRetrievalThread
和 CProcessThreadMonitor
)都必须实现此方法。CCustomThread
设计用于确保在您希望线程终止时线程函数返回,这是确保所有线程资源得到妥善清理的唯一方法。它通过发出命名事件 m_hShutdownEvent
的信号来提供关闭其任何实例的手段。
CCallbackHandler
是一个抽象类,旨在提供一个接口,用于在进程创建或终止时执行用户提供的操作。它公开了一个纯虚方法 OnProcessEvent()
,必须根据系统的特定要求来实现该方法。在示例代码中,您将看到一个继承自 CCallbackHandler
并实现 OnProcessEvent()
方法的类 CMyCallbackHandler
。OnProcessEvent()
方法的一个参数 pvParam
允许您传递任何类型的数据,因此它被声明为 PVOID
。在示例代码中,将指向 CWhatheverYouWantToHold
实例的指针传递给 OnProcessEvent()
。您可能希望使用此参数仅传递窗口句柄,该句柄可以在 OnProcessEvent()
实现中使用以向其发送消息。
class CCallbackHandler { public: CCallbackHandler(); virtual ~CCallbackHandler(); // Define an abstract interface for receiving notifications virtual void OnProcessEvent( PQUEUED_ITEM pQueuedItem, PVOID pvParam ) = 0; };
编译示例代码
您需要在机器上安装 MS Platform SDK。提供的用户模式应用程序示例代码可以编译为 ANSI 或 UNICODE。如果您想编译驱动程序,您还必须安装 Windows DDK。
运行示例
然而,如果您没有安装 Windows DDK,那也不是问题,因为示例代码包含 ProcObsrv.sys 内核驱动程序的已编译调试版本及其源代码。只需将控制程序和驱动程序放在同一个目录中,然后运行它。
出于演示目的,用户模式应用程序动态安装驱动程序并启动监控过程。接下来,您将看到启动并随后关闭 10 个 notepad.exe 实例。在此期间,您可以查看控制台窗口,了解进程监视器的工作原理。如果您愿意,可以启动某个程序,看看控制台如何显示其进程 ID 及其名称。
结论
本文演示了如何利用已记录的接口来检测 NT/2K 进程执行。然而,这远非唯一的解决方案,肯定可能遗漏了一些细节,但我希望它能在某些实际场景中对您有所帮助。
参考文献
- 用于枚举 NT 和 Win9x/2K 下进程和模块的单一接口,Ivo Ivanov
- Windows DDK 文档,进程结构例程
- Nerditorium,Jim Finnegan,MSJ 1999 年 1 月
- Windows NT 设备驱动程序开发,Peter G. Viscarola 和 W. Anthony Mason
- 应用 UML 和模式,Craig Larman
- 使用谓词等待和 Win32 线程,D. Howard,C/C++ 用户杂志,2000 年 5 月