文/Ivo Ivanov
摘要
拦截和追踪进程的执行对实现NT任务管理器——像需要外部进程处理的应用程序和系统——是非常有用的。在新进程的开始之上通知感兴趣的部分是开发进程监视系统和全系统钩子的一个典型问题。WIN32 API提供了一个很好的库(PSAPI and ToolHelp [1])的集合,这些库允许你列举当前在系统中运行的进程。虽然这些API非常有用,但当一个新的进程开始或结束时它们不允许你获得通知。这篇文章提供了一个有效而且完整的技术,为了完成这个目标,它基于一个公开的接口。
解决方案
幸运地,NT/2K提供了一个API集合,知道的像“Process Structure Routines“[2]通过NTOSKRNL输出。一个API函数PsSetCreateProcessNotifyRoutine()提供了注册全系统回叫函数的能力,每次当一个进程开始,存在或已经终止时这些回叫函数都会被OS呼叫。上面提到的API函数对简单地通过执行一个NT核心模式的驱动程序和用户模式的WIN32控制程序追捕进程时可以作为一个容易实现的方法来使用。驱动程序的角色是检测进程的执行和通知控制程序这些事件。
要求
● 为监视进程的执行提供一个简单,有效,可靠和线程安全机制
● 在驱动程序和用户的应用程序之间解决同步问题
● 打造一个易于使用和扩展的OOP用户模式的框架
● 允许回叫函数的注册和解注册,还要有动态加载和卸载核心驱动程序的能力
它怎样工作
控制程序在HKLM\SYSTEM\CurrentControlSet\Services下注册核心模式驱动程序,并且动态加载。核心驱动程序然后建立一个指定的事件对象,当新的事件已经触发时(如进程开始或结束),这个事件对象就向用户模式的应用程序发出信号。控制程序打开相同的事件对象,然后建立一个等待这个事件的监听线程。接下来,用户模式的应用程序发送一个请求给驱动程序来开始监视。驱动程序调用PsSetCreateProcessNotifyRoutine(),这个函数接受两个参数。一个指定呼叫者提供的回叫程序的入口指针,责任是从WINDOWS接受所有的通知。在回叫时发生的一个通知上面,驱动程序发出那个信号事件是为了通知用户模式应用程序说有事情发生了。控制程序然后从驱动程序中得到那个特定事件的数据,然后为了更多的进程,将它存储进一个特殊的列队容器。如果不再检测进程执行,用户模式应用程序发送一个请求给驱动程序来停止监视。然后驱动程序释放观察机制。再后来,控制模式应用程序可以卸载驱动程序和对它解注册。
设计和实现
NT模式驱动程序(ProcObsrv)
入口指针DriverEntry() (ProcObsrv.c)只执行驱动程序的初始化。当驱动程序加载时,I/O管理器呼叫这个函数。既然PsSetCreateProcessNotifyRoutine()允许解注册回叫,这些回叫是我在驱动程序的调度程序中执行的已注册或解注册的实际的进程。这就允许我通过使用一个单独的IOCTL(控制代码IOCTL_PROCOBSRV_ACTIVATE_MONITORING)来动态地开始和停止监视活动。一旦回叫被注册,每次当一个进程开始或终止时,OS就呼叫用户提供的ProcessCallback()。这个函数被植入一个会通过用户模式应用程序选择的缓冲区中。接下来发出指定的事件对象信号,因此等待它的用户模式应用程序会被通知说有可利用的信息检索。
控制程序(ConsCtl)
为了简单的缘故,我决定提供一个简单的控制台应用程序,把想象的GUI要素的实现留给你。设计一个多线程的应用程序允许那个应用程序调整和更易于作出反应。在另一方面,考虑一些与同步存取信息相关的考虑非常重要,这些信息是出版者(如核心模式)提供的和用户(如控制程序)检索的信息。另一重要的关键点是一个检测系统必须可靠,而且确信没有事件漏掉。为了简化进程的设计,首先我需要在用户模式应用程序的不同实体间分配责任,责任是处理驱动程序。然而回答了这些问题,就不难办到了[5]:
1. 系统里的进程是什么?
2. 在框架里的作用是什么?
3. 谁做什么,4. 怎样合作?
下面是一个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的线程被唤醒。然后用户模式应用程序从驱动程序检索数据。接下来,数据被附加给使用它的方法Append()的(CQueueContainer) 列队容器。
CqueueContainer是一个线程安全列队控制器,它提供一个Monitor/Condition变量模式的执行。它主要的目的是提供一个列队容器的线程安全信号。这是方法Append()怎样工作的:
1. 锁住对集合的STL列队对象的访问
2. 添加数据项
3. 发出m_evtElementAvailable事件对象信号
4. 解锁列队
这是它实际的执行:
// 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的例子,CretreivalThread等待直到一个元素在本地存储器中变的可用。这是它的伪执行:
1. 等待m_evtElementAvailable事件对象
2. 锁住对STL列队对象的访问
3. 抽取数据项目
4. 解锁列队
5. 处理已经从列队中检索出的信息。
这是当有东西增加进列队时的方法调用:
// 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)
{
bRemoveFromQueue = (m_Queue.size() > 0);
// Is there anything in the queue
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 there is an element
// that has been picked up
if (bRemoveFromQueue)
m_pHandler->OnProcessEvent( &element, m_pvParam );
else
break;
} // while
}
CcustomThread——帮助管理保持原始线程的复杂性。我在抽象类里面封装了所有线程的相关活动。它提供了一个纯虚方法Run(),Run()必须被任何一个特别的线程类(如CretrievalThread和CProcessThreadMonitor)执行。Ccustom线程是设计来保证当你想线程终止时线程函数的返回,这是作为确保所有线程的资源都被适当地清除干净的唯一方式。它提供一个均值,通过发出m_hShutdownEvent事件信号来关闭它的任何实例。
CcallbackHandler是一个抽象类,它设计来在线程已创建或终止时为执行用户提供的行为提供接口。它暴露了一个纯虚方法OnProcessEvent(),这个方法必须根据系统特定的要求执行。在示例代码中你将看见一个类CmyCallbackHandler,它从CcallbackHandler继承,而且执行方法OnProcessEvent()。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进程的执行,你怎样使用一个公开的接口。但是这并不是这个问题唯一的解决方案,而且它的确错过了一些细节。但是你会发现它对一些真实的情况是有用的。
参考
1. Single interface for enumerating processes and modules under NT and Win9x/2K, Ivo Ivanov
2. Windows DDK Documentation, Process Structure Routines
3. Nerditorium, Jim Finnegan, MSJ January 1999
4. Windows NT Device Driver Development, Peter G. Viscarola and W. Anthony Mason
5. Applying UML and Patterns, Craig Larman
6. Using predicate waits with Win32 threads, D. Howard, C/C++ Users Journal, May 2000