Abstract
Intercepting and tracing process execution is a very useful mechanism for
implementing NT Task Manager-like applications and systems that require
manipulations of external processes. Notifying interested parties upon starting
of a new processes is a classic problem of developing process monitoring
systems and system-wide hooks. Win32 API provides set of great libraries (PSAPI
and ToolHelp [1]) that allow you to enumerate processes currently running in
the system. Although these APIs are extremely powerful they don't permit you to
get notifications when a new process starts or ends up. This article provides
an efficient and robust technique based on a documented interface for achieving
this goal.
Solution
Luckily, NT/2K provides a set of APIs, known as "Process Structure Routines"
[2] exported by NTOSKRNL. One of these APIs PsSetCreateProcessNotifyRoutine()
offers the ability to register system-wide callback function which is called by
OS each time when a new process starts, exits or is terminated. The
mentioned API can be employed as an easy to implement method for tracking down
processes simply by implementing a NT kernel-mode driver and a user mode Win32
control application. The role of the driver is to detect process execution and
notifiy the control program about these events.
Requirements
-
Provide a simple, efficient, reliable and thread-safe mechanism for monitoring
process execution
-
Resolve synchronization issues between the driver and the user mode application
-
Build an easy to use and extend OOP user-mode framework
-
Allow registering and un-registering of the callback as well as ability to
dynamically load and unload the kernel driver
How it works
The control application register the kernel mode driver under
HKLM\SYSTEM\CurrentControlSet\Services and dynamically loads it. The kernel
driver then creates a named event object that is used to signal the user-mode
application when new event has been fired (i.e. process starts or ends up). The
control application opens the same event object and creates a listening thread
that waits on this event. Next, the user mode application sends a request to
the driver to start monitoring. The driver invokes PsSetCreateProcessNotifyRoutine()
,
which accepts two parameters. One of them specifies the entry point of a
caller-supplied callback routine, responsible for receiving all notifications
from Windows. Upon a notification, that takes place in the callback, the driver
signals that event in order to inform the user-mode application that something
has happened. The control application then gets the data for that particular
event from the driver and stores it in a special queue container for further
processing. If there is no need for detecting process execution anymore the
user mode application sends a request to the driver to stop monitoring. The
driver then deactivates the observing mechanism. Later the control mode
application can unload the driver and un-register it.
Design and implementation
NT Kernel mode driver (ProcObsrv)
The entry point DriverEntry()
(ProcObsrv.c) performs the driver's
initialization only. The I/O manager calls this function when the driver is
loaded. Since PsSetCreateProcessNotifyRoutine()
allows to
un-register the callback I implemented the actual process of registration and
un-registration in the driver's dispatch routine. This allows me dynamically to
start and stop the monitoring activities by using a single IOCTL (control code
IOCTL_PROCOBSRV_ACTIVATE_MONITORING
). Once the callback is
registered each time when a process starts or terminates the OS calls user
supplied ProcessCallback()
. This function populates a buffer that
will be picked up by the user mode application. Next the driver signals the
named event object, thus the user-mode application that waits on it will be
informed that there is available information to be retrieved.
Control application (ConsCtl)
For the sake of simplicity I decided to provide a simple console application,
leaving the implementation of the fancy GUI stuff to you. Designing of an
application to be multithreaded allows that application to scale and be more
responsive. On the other hand, it is very important to take into account
several considerations related to synchronizing the access to information
provided by the publisher (i.e. kernel driver) and retrieved by the subscriber
(i.e. control application). The other important key point is that a detecting
system must be reliable, and makes sure that no events are missed out. To
simplify the design process, first I needed to assign the responsibilities
between different entities in the user mode application, responsible for
handling the driver. However it isn't difficult to do it by answering these
questions [5]:
-
What are the processes in the system
-
What are the roles in the framework
-
Who does what and how do they collaborate
Follows UML class diagram, that illustrates the relations between classes:
CApplicationScope
implements a singleton and wraps up the main
interface to the framework. It exposes two public methods that start and stop
the monitoring process.
class CApplicationScope
{
.. Other Other details ignored for the sake of simplicity ....
public:
BOOL StartMonitoring(PVOID pvParam);
void StopMonitoring();
};
CProcessThreadMonitor
is the thread that waits on the created by
the driver event to be signaled. As soon as a process has been created or ended
up, the driver signals this event object and CProcessThreadMonitor
's
thread wakes up. Then the user mode application retrieves the data from the
driver. Next, the data is appended to queue container (CQueueContainer
)
using its method Append()
.
CQueueContainer
is a thread-safe queue controller that offers an
implementation of the Monitor/Condition variable pattern. The main purpose of
this class is to provide a thread-safe semaphore realization of a queue
container. This is how the method Append()
works:
-
Lock access to the aggregated STL deque object
-
Add the data item
-
Signal
m_evtElementAvailable
event object
-
Unlock the deque
And here is its actual implementation:
BOOL CQueueContainer::Append(const QUEUED_ITEM& element)
{
BOOL bResult = FALSE;
DWORD dw = ::WaitForSingleObject(m_mtxMonitor, INFINITE);
bResult = (WAIT_OBJECT_0 == dw);
if (bResult)
{
m_Queue.push_back(element);
::SetEvent(m_evtElementAvailable);
}
::ReleaseMutex(m_mtxMonitor);
return bResult;
}
Since it is designed to notify when there is an element available in the queue,
it aggregates an instance of CRetreivalThread
, which waits until
an element becomes available in the local storage. This is its pseudo
implementation:
-
Wait on
m_evtElementAvailable
event object
-
Lock access to the STL deque object
-
Extract the data item
-
Unlock the deque
-
Process the data that has been retrieved from the queue
Here is the method invoked when something has been added to the queue:
CQueueContainer::DoOnProcessCreatedTerminated() {
QUEUED_ITEM element;
BOOL bRemoveFromQueue = TRUE;
while (bRemoveFromQueue) {
DWORD dwResult = ::WaitForSingleObject( m_mtxMonitor, INFINITE );
if (WAIT_OBJECT_0 == dwResult) {
bRemoveFromQueue = (m_Queue.size() > 0);
if (bRemoveFromQueue) {
element = m_Queue.front(); m_Queue.pop_front();
}
else
::ResetEvent(m_evtElementAvailable);
}
if (bRemoveFromQueue)
m_pHandler->OnProcessEvent( &element, m_pvParam );
else
break;
}
}
CCustomThread
- To help manage the complexity of maintaining raw
threads I encapsulated all thread's related activities in an abstract class. It
provides a pure virtual method Run()
, that must be implemented by
any specific thread class (e.g. CRetrievalThread
and CProcessThreadMonitor
).
CCustomThread
is designed to ensure that thread function returns
when you want the thread to terminate as the only way to make sure that all
thread's resources are cleaned up properly. It offers a means to shut any of
its instances down by signaling a named event m_hShutdownEvent
.
CCallbackHandler
is an abstract class that has been designed to
provide interface for performing user-supplied actions when process is created
or terminated. It exposes a pure virtual method OnProcessEvent()
,
which must be implemented according to the specific requirements of the system.
In the sample code you will see a class CMyCallbackHandler
, that
inherits from CCallbackHandler
and implements OnProcessEvent()
method. One of the parameters pvParam
of OnProcessEvent()
method allows you to pass any kind of data, that's why it is declared as PVOID
.
In the sample code a pointer to an instance of CWhatheverYouWantToHold
is passed to the OnProcessEvent()
. You might want to use this
parameter to pass just a handle to a window, that could be used for sending a
message to it within OnProcessEvent()
implementation.
class CCallbackHandler
{
public:
CCallbackHandler();
virtual ~CCallbackHandler();
virtual void OnProcessEvent(
PQUEUED_ITEM pQueuedItem,
PVOID pvParam
) = 0;
};
Compiling the sample code
You need to have installed MS Platform SDK on your machine. Provided sample
code of the user-mode application can be compiled for ANSI or UNICODE. In case
you would like to compile the driver you have to install Windows DDK as well.
Running the sample
However, it is not a problem if you don't have Windows DDK installed,
since the sample code contains a compiled debug version of ProcObsrv.sys kernel
driver as well as it source code. Just place control program along with the
driver in single directory and let it run.
For demonstration purposes, the user mode application dynamically installs the
driver and initiates process of monitoring. Next, you will see 10 instances of
notepad.exe launched and later on closed. Meanwhile you can peek at the console
window and see how the process monitor works. If you want you can start some
program and see how the console will display its process ID along with its
name.
Conclusion
This article demonstrated how you can employ a documented interface for
detecting NT/2K process execution. However it is by far not the only one
solution to this issue and certainly might miss some details, but I hope
you would find it helpful for some real scenarios.
References:
-
Single interface for
enumerating processes and modules under NT and Win9x/2K, Ivo Ivanov
-
Windows DDK Documentation, Process Structure Routines
-
Nerditorium, Jim Finnegan, MSJ January 1999
-
Windows NT Device Driver Development, Peter G. Viscarola and W.
Anthony Mason
-
Applying UML and Patterns, Craig Larman
-
Using predicate waits with Win32 threads, D.
Howard, C/C++ Users Journal, May 2000