The following article will help you to understand principles of Windows processes starting. In addition, it will show you how to set some filters for process start, including allowing and forbidding ones.
Contents
Introduction
We will develop a Windows process monitoring tool responsible for installing driver for process start monitoring. The driver will alert the user-mode application on each new Windows process start, as well as provide the PID and name of the process, and request whether to allow or forbid this process start. Until that, the process is awaiting and is not starting to work.
This article would be useful for junior developers and beginners in Windows process programming, driver development, and interactions between driver mode and user one . It will also be interesting for those specialists who develops application monitoring tools or corporate security systems, e.g. using process monitor to monitor file access or start of specific applications.
Code Using
This article describes Windows process monitoring solutions as well as Windows process monitoring techniques. The code presented here illustrates the process blocking technique, thus, it cannot be used as a commercial solution ready for implementation in real projects.
Project Structure
.bin – folder with binary files
.lib – folder with library files
.obj – folder with object files
.procmon – folder with source files
|-> .Common โ Common files and projects
|-> .DrvCppLib – Kernel Library to develop driver in C++.
|-> . DrvSTLPort – Directory with STLPort 4.6 ported for
using in windows drivers.
|-> . includes – Includes that are common for user and driver
|-> . processdll – Main DLL that has all API
|-> . procmon – Driver project
|-> . ProcMonGUI – GUI that is written using MFC
Described Windows process monitoring tool development project supports x86 and x64 Windows architectures. In addition, it provides all needed build configurations.
Windows process loading
In order to create a Windows process, you should call one of the functions below:
CreateProcess
CreateProcessAsUser
CreateProcessWithTokenW
CreateProcessWithLogonW
Developing consists of a few steps performed by following OS components:
- Kernel32.dll (library of Windows client part)
- Performing system
- Windows environment subsystem process (Csrss)
We can separate operations for creating performing system process object (other environment subsystems can use it later) and operations for creating Windows process. It is possible, because Windows architecture supports different environment subsystems.
Thus, some CreateProcess
Windows function actions are specific to the Windows semantics.
Below, there are common steps for process creation using the CreateProcess
Windows function:
- Open image file (EXE), which will be executed in the process.
- Create performing system process object.
- Create initial thread (stack, context, and performing system thread object).
- Windows subsystem receives notification about the process and thread creation.
- Start the initial thread if the
CREATE_SUSPENDED
flag is not set. - Initialize the address space in the context of new process and thread (e.g. load necessary DLLs), and then start the program.
The โMicrosoft Windows Internalsโ by Mark Russinovich will tell you more about the scheme of process start.
Notifications are useful feature of the process creation and process loading technology. Driver developers can receive alerts about process creation and image files mapping to the memory.
The first notification will be sent as soon as system creates a process. The second one will be sent on each time system maps an image file (either executive one or DLL).
Using these notifications, our driver receives information about new process start, and thus, can alert our application about it. Let’s consider the mechanism of the information transferring to user mode.
Tech Aspects
It is supposed that the reader is familiar with common knowledge in simple driver development, so I wonโt explain basic principles, but pay attention to more complicated things while developing Windows process monitoring tool.
Subscription for notifications on process creation and image mapping is the first step for our driver on the initialization stage. There is a corresponding code below:
โฆ
status = PsSetCreateProcessNotifyRoutine(&CreateProcessNotifyRoutine, FALSE);
if (status == STATUS_SUCCESS)
status = PsSetLoadImageNotifyRoutine(&LoadImageNotifyRoutine);
โฆ
First, we should create the list of already started processes. According to the solution architecture, as soon as event occurs, user application requests the list of started processes โ and driver provides it. We have to separate allowed for starting processes from already started ones, so that the list doesnโt include all processes. In order to do that, the project includes ProcessHelper structure. Its instance, created during process creation, takes responsibility for this process. This structure includes info about whether this process was allowed to start, and provides its name, PID, and some additional data.
The following function starts initial process list building: StartupProcessManagerList();
Then we start and initialize driver.
User Mode
The DeviceIoControl()
function is responsible for communication between driver and user mode. Nevertheless, using Init()
function, user mode application helps driver to call user mode at the process start. You can find the corresponding code below:
m_NotifyEvent.Reset();
HANDLE hEvent = m_NotifyEvent.GetHandle();
IoControl(
IOCTL_REGISTER_EVENT,
&hEvent,
sizeof(hEvent),
NULL,
0);
DeviceIoControl
together with IOCTL_REGISTER_EVENT code help us to create and initialize the Event
object, as well as to send it to the driver. Driver gets this event and โwrites it downโ in order to set it later to the signal state, and thus, notify the user mode part about the new process start. Below there is a driver code for event receiving:
. . .
sync::AutoWriteLock autoWriteLock(m_Lock);
m_UserNotifyEvent.Cleanup();
status = m_UserNotifyEvent.Initialize(hEvent) ? STATUS_SUCCESS : STATUS_INVALID_HANDLE;
. . .
While driver is setting an event, it checks all the processes to detect already paused ones. If it discovers such processes, driver immediately alerts user mode application about the processes, which is waiting in the queue.
Now driver is waiting for the process start and is ready to notify the controlling application by means of provided event.
Windows Process Start
It was mentioned above that during the Windows process start, the process creation notification is sent, and then several image mapping notifications appear, depending on the number of loaded libraries.
Code examples will help us to learn this scheme in details.
We should execute the following code after receiving a process creation notification:
ProcessHelper* procHelper = new ProcessHelper(ParentPid, Pid);
if (procHelper)
{
if (!m_ActiveProcesses.Add(procHelper))
return;
}
Please notice that, during process creation, we also create an assistant to store the following info about the process identifier:
- Name
- State (demonstrates if the process is paused)
- Marker(demonstrates if the user has already considered this process concerning allow it or not, or this is a new process)
After that, we receive notification about image mapping. The mapping is performed in the context of the process, which PID we also receive. After that, we check if the process is already in the list (if the notification has been already received) or not. If it is there, we continue to fill in the data about the process.
The check is performed in such way:
ProcessHelper* procHelper = NULL;
// Explicit scope to access and release list
{
ProcessHelperList procList = m_ActiveProcesses.AquireList();
utils::ScopedListReleaser<ProcessHelperHolder> procListGaurd(m_ActiveProcesses);
ProcessHelperList::iterator it = procList.find(Pid);
if (it != procList.end())
procHelper = it->second;
if (!procHelper)
return;
}
Now, we should check whether we have mapped an image or a DLL, and if we have succeeded at obtaining process name. In order to do that, we perform the following simple validations:
// Check if process already has ImageName assigned. If assigned, then,
// current callback for dll image which is mapped in process address space.
// Ignore it in this case.
if (!procHelper->ImageName.empty())
return;
// Convert native image path to DOS path. Lowercase final path to optimize
// path checking speed on rules processing.
PUNICODE_STRING dosName = NULL;
utils::KernelFileNameToDosName(NativeImageName, &dosName);
if (dosName)
{
RtlDowncaseUnicodeString(dosName, dosName, FALSE);
utils::nothrow_string_assign<std::wstring, WCHAR>(&procHelper->ImageName, dosName->Buffer, dosName->Length / sizeof(WCHAR));
ExFreePool(dosName);
}
else
{
utils::nothrow_string_assign<std::wstring, WCHAR>(&procHelper->ImageName, NativeImageName->Buffer, NativeImageName->Length / sizeof(WCHAR));
}
After passing all validations and making sure that the process is valid, we initialize the process event and notify the user mode process about this event. Then we are waiting for the process event initialized before. Process event is the part of the ProcessHelper
and is associated with the specific process.
You could wonder โWhy it is so complicated? Why donโt we perform everything that in the process creation notification?โ. The answer is simple. Process creation notification comes anisochronously, so our waiting will come to nothing. The process will start and perform all its actions. As for image mapping notification, it comes synchronously, so if we stop execution inside the function, the process will be โfrozenโ. That is what we need to monitor Windows processes. The following code demonstrates all described actions:
// Artificial block to sync. access for mRulesLoaded & mInitialization flags.
{
sync::AutoReadLock sharedLock(m_Lock);
if (!m_UserNotifyEvent.Valid())
return;
if (!procHelper->ResumeEvent.Initialize(false, false))
{
ERR_FN(("Failded to initialize resume event for ProcessId: %d\n", procHelper->Pid));
return;
}
// Notify user mode library counterpart (if core is in Normal state)
m_UserNotifyEvent.Set();
}
NTSTATUS waitStatus = STATUS_SUCCESS;
unsigned waitTimeout = 4*60*1000;
if (procHelper->ResumeEvent.Wait(waitTimeout, &waitStatus) && waitStatus == STATUS_TIMEOUT)
Reset();
When driver sends notification on the new process start, user mode application stops waiting and starts request processing.
This is how user mode application waiting looks like:
do
{
if (m_NotifyEvent.Wait() && !m_TerminateMonitoring)
Dispatch();
}
while (!m_TerminateMonitoring);
Using the Dispatch()
function, we request the list of the new started processes from the driver after the last request. Then user should choose processes to be allowed and ones to be blocked. After that, we look through the list sending the corresponding codes of process allow/block.
. . .
if (checkList[i].AddToBlacklist)
Block(checkList[i].ProcessInfo);
else
Allow(checkList[i].ProcessInfo);
. . .
Each Allow
and Block
function calls the DeviceIoControl
function with the IOCTL_ALLOW
or IOCTL_BLOCK
code correspondingly. It is performed in driver. First, we perform searching by process PID. We release the process if it is paused, and if the process marker states that it must be blocked, then we set the TRUE value to the bTerminate flag and forcibly terminate the process.
Several additional actions are required for process termination. We cannot just terminate it in the notification function. We should start the deletion thread and resume the process execution in order to terminate a process. The thread immediately discovers and terminates it. Below you can find the corresponding code:
VOID ScheduleProcessTerminate(HANDLE ProcessId)
{
HANDLE threadHandle;
OBJECT_ATTRIBUTES objAttr;
TERSE_FN(("Schedule terminate process ProcessId: %d\n", ProcessId));
InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
NTSTATUS status = PsCreateSystemThread(&threadHandle, THREAD_ALL_ACCESS,
&objAttr, NULL, NULL, SystemThreadToTerminateTagetProcess, ProcessId);
if (status == STATUS_SUCCESS)
{
ZwClose(threadHandle);
}
}
VOID SystemThreadToTerminateTagetProcess(IN PVOID StartContext)
{
. . .
status = PsLookupProcessByProcessId(processId, &process);
if (status == STATUS_SUCCESS)
{
status = ObOpenObjectByPointer(process, OBJ_KERNEL_HANDLE, NULL,
PROCESS_ALL_ACCESS, NULL, KernelMode, &processHandle);
ObDereferenceObject(process);
if (status == STATUS_SUCCESS)
{
ZwTerminateProcess(processHandle, STATUS_SUCCESS);
ZwClose(processHandle);
TERSE_FN(("Terminated ProcessId: %d\n", processId));
}
}
. . .
Let’s consider the graphical representation of the scheme:
Driver Installation and Usage Example
The given example of Windows process monitoring tool development has the main window providing following options:
- Install driver
- Start driver
- Stop driver
- Delete driver
Main window also contains the Start and Stop buttons for turning monitoring on and off.
After driver installation and start, we should press the Start button, which is placed in the Monitoring group. Then, as you can see on the picture below, another window appears at each new process start.
Conclusion
In this basic process monitor tutorial, I have described Windows process monitoring and managing (blocking) technologies and solutions. You can improve the sample code by adding one of the following options:
- Show a few windows simultaneously
- Delay the process start for unlimited time
- Rules to allow or block specific process types
You can do it yourself in order to learn this subject in details.
Additional Information
You need following global valuables to build the project:
BOOST_ROOT
must contain the path to the root where Boost is installed.
BASEDIR
must contain the path to the root where WDK 7.1.0 is installed.
References
- Mark Russinovich, David Solomon. Microsoft Windows Internals ((Fourth Edition) ed.)
- Dekker, Newcomer. Developing Windows Device Drivers
- MSDN
Download Source and Bin Files of the sample project (ZIP, 3.9MB)
Ready to hire R&D team experienced in process programming and driver development to work on your project like monitoring and managing Windows processes? Just contact us and we will provide you all details!