This article provides a detailed guide on developing and implementing a Windows kernel-level application Blacklisting program using WDM and Filter Manager. It explores the fundamentals of application Blacklisting, delves into kernel programming concepts, and offers practical insights into coding and deployment. By following this guide, new developers can learn about system security by ensuring only authorized applications run at the kernel level, thereby protecting against unauthorized software and potential threats.
Introduction to Application Blacklisting
Application Blacklisting is a security measure that ensures only approved applications can execute on a system. Implementing this technique on a Windows operating system involves working directly with the kernel, providing a robust layer of protection against unauthorized application.
This approach leverages the Filter Manager framework to interact with the kernel, monitoring and controlling the execution of applications.
Background
Application Blacklisting is a security approach that permits only pre-approved software to run on a system, effectively blocking unauthorized or potentially harmful applications. This technique is especially powerful when implemented at the kernel level, as it provides a robust layer of protection against malicious code that might bypass user-space defenses. The kernel, being the core part of an operating system, has the highest level of control over system operations and resources. This article introduces the concept of application whitelisting and its importance, delves into the intricacies of kernel programming with C++, and guides readers through the process of developing a kernel-level whitelisting mechanism. Understanding these foundational concepts will empower developers to create more secure and resilient systems.
Explanation of the Workflow Diagram for Kernel-Level Application Blacklisting
This workflow diagram illustrates the process of kernel-level application Blacklisting, focusing on the interaction between different components and decision points within the system. Here’s a detailed explanation of each step and component:
Components and Flow
User Application: Represents the applications that are attempting to execute on the system.
Filter Manager: Windows Kernel intermediary that manages different filters (MiniFilters) used for pre/post processing I/O operations.
MiniFilter Driver: A specific type of file system filter driver that can intercept and file I/O operations. It is responsible for checking the attributes of the executable files being executed.
DoesExecutableExistInPath
: This function checks whether the executable attempting to run exists in the predefined Blacklist path. If the executable exists in the path (YES), it proceeds to the next step. If it does not (NO), it will be added to the Blocked List.
Blocked List: Contains the list of executables that are not allowed to execute because they are not in the Blacklist path.
PreCreate (IRP_MJ_CREATE) Preprocessing: A decision point that determines if the file name matches with existing defined application name. If the file is Blacklisted, it moves to the Passthrough step. If not, it is blocked (not shown explicitly but implied by the arrow to the Blocked List).
PassThrough: If the file creation request passes the previous checks, this step allows the request to pass through the driver and the IO will be handled by lower drivers.
Lower Driver: Represents the lower-level drivers in the stack that will handle the request if it passes all the previous checks. The executable is allowed to proceed for execution.
Using the code
The DriverEntry
function initializes the MiniFilter driver, registers it with the filter manager, and starts filtering I/O operations. Optionally, it can also register a process creation notification callback. The function handles success and failure scenarios, ensuring proper cleanup if any step fails. This setup is essential for the MiniFilter to start functioning and intercepting relevant I/O operations on the system.
Note: I tried to showcase both way by which an application can be blocked. a process creation notification and PreCreate()
method.
extern "C"
NTSTATUS
DriverEntry(
__in PDRIVER_OBJECT DriverObject,
__in PUNICODE_STRING RegistryPath
)
{
NTSTATUS status = STATUS_SUCCESS;
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("PocMiniFilter!DriverEntry: Entered\n"));
MiniFilterData.DriverObject = DriverObject;
status = FltRegisterFilter( DriverObject,
&FilterRegistration,
&MiniFilterData.Filter);
FLT_ASSERT(NT_SUCCESS(status));
if (NT_SUCCESS(status)) {
status = FltStartFiltering(MiniFilterData.Filter);
if (!NT_SUCCESS(status)) {
FltUnregisterFilter(MiniFilterData.Filter);
}
}
#ifdef _USE_NOTIFY_WAY
status = PsSetCreateProcessNotifyRoutineEx(NotificationCallback, FALSE);
if (!NT_SUCCESS(status)) {
KdPrint(("PsSetCreateProcessNotifyRoutineEx failed with status = 0x%x\n", status));
}
#endif
status = STATUS_SUCCESS;
return status;
}
Brief Description of the FilterUnload Function
The PocMiniFilterUnload
function handles the unloading process for the MiniFilter driver. It unregisters the filter from the filter manager and optionally removes the process creation notification callback if it was previously set. This function ensures that all resources allocated by the driver are properly released, allowing the driver to be unloaded cleanly and safely from the system.
NTSTATUS
BlacklistingAppUnload(
__in FLT_FILTER_UNLOAD_FLAGS Flags
)
{
UNREFERENCED_PARAMETER(Flags);
PAGED_CODE();
KdPrint(("BlacklistingApp!PtUnload: Entered\n"));
FltUnregisterFilter(MiniFilterData.Filter);
#ifdef _USE_NOTIFY_WAY
PsSetCreateProcessNotifyRoutineEx(NotificationCallback, TRUE);
#endif
return STATUS_SUCCESS;
}
NotificationCallback Function
The NotificationCallback
function is a critical component of the MiniFilter driver that monitors process creation events. It checks if the executable file being executed is on a blocked list and prevents its execution by setting the creation status to STATUS_ACCESS_DENIED
. This ensures that unauthorized or potentially harmful executables are not allowed to run on the system.
VOID
NotificationCallback(
_Inout_ PEPROCESS Process,
_In_ HANDLE ProcessId,
_In_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNREFERENCED_PARAMETER(Process);
UNREFERENCED_PARAMETER(ProcessId);
if (CreateInfo != NULL) {
KdPrint(("NotificationCallback: IN = %wZ, CL =%wZ\n", CreateInfo->ImageFileName, CreateInfo->CommandLine));
if (CreateInfo->ImageFileName) {
if (DoesExecutableExistInPath(CreateInfo->ImageFileName))
{
CreateInfo->CreationStatus = STATUS_ACCESS_DENIED;
KdPrint(("Execution blocked by Policy \n"));
}
}
}
}
Breif about PreCreateCallback
The PreCreateCallback()
function intercepts file creation requests, constructs the full file path, and checks if the file is on a blocked list. If the file is blocked, it denies access. The function ensures proper cleanup of resources and handles various validation and early exit scenarios to maintain system stability and security.
FLT_PREOP_CALLBACK_STATUS
BlacklistingAppPreCallback(
_Inout_ PFLT_CALLBACK_DATA Data,
_In_ PCFLT_RELATED_OBJECTS FltObjects,
_Flt_CompletionContext_Outptr_ PVOID* CompletionContext
)
{
UNREFERENCED_PARAMETER(FltObjects);
UNREFERENCED_PARAMETER(CompletionContext = NULL);
UNREFERENCED_PARAMETER(Data);
PAGED_CODE();
PFLT_FILE_NAME_INFORMATION nameInfo = NULL;
FLT_FILE_NAME_OPTIONS NameOptions = {0,};
NTSTATUS status = STATUS_SUCCESS;
PFILE_OBJECT FileObject = NULL;
UNICODE_STRING FileName = { 0 };
UNICODE_STRING fullPathName = { 0 };
UNICODE_STRING Drive_Location = { 0, };
FLT_FILESYSTEM_TYPE VolumeFilesystemType;
BOOLEAN isNetPath = FALSE;
FLT_PREOP_CALLBACK_STATUS callbackStatus = FLT_PREOP_SUCCESS_NO_CALLBACK;
__try
{
NT_ASSERT(Data->Iopb->MajorFunction == IRP_MJ_CREATE);
if (KeGetCurrentIrql() > APC_LEVEL) {
BlacklistingAppLeave(status);
}
FileObject = FltObjects->FileObject;
if (FileObject == NULL)
{
KdPrint(("No FileObject found in PreCreateCallback\n"));
return callbackStatus;
}
if (FlagOn(Data->Iopb->OperationFlags, SL_OPEN_PAGING_FILE)) {
return callbackStatus;
}
if (FlagOn(Data->Iopb->Parameters.Create.Options, FILE_OPEN_BY_FILE_ID)) {
return callbackStatus;
}
status = FltGetFileSystemType(FltObjects->Instance, &VolumeFilesystemType);
if (!NT_SUCCESS(status)){
return FLT_PREOP_SUCCESS_NO_CALLBACK;
}
if (FLT_FSTYPE_MUP == VolumeFilesystemType)
{
KdPrint(("PreCreate : FLT_FSTYPE_MUP\n"));
isNetPath = TRUE;
NameOptions = FLT_FILE_NAME_OPENED | FLT_FILE_NAME_QUERY_FILESYSTEM_ONLY;
}
else {
isNetPath = FALSE;
NameOptions = FLT_FILE_NAME_NORMALIZED;
}
status = FltGetFileNameInformation(Data,
NameOptions,
&nameInfo);
if (!NT_SUCCESS(status)) {
KdPrint(("PreCreate: failed for file =%wZ, with status = 0x%x\n", Data->Iopb->TargetFileObject->FileName , status));
BlacklistingAppLeave(status);
}
status = FltParseFileNameInformation(nameInfo);
if (!NT_SUCCESS(status)) {
KdPrint(("PreCreate: FltParseFileNameInformation failed, status: 0x%x \n", status));
BlacklistingAppLeave(status);
}
if (nameInfo == NULL ||
nameInfo->Name.Buffer == NULL ||
nameInfo->Name.Length == 0)
{
KdPrint(("PreCreate: nameInfo is NULL: exit\n"));
__leave;
}
if (NT_SUCCESS(status)) {
if (nameInfo->Name.MaximumLength <= 32767){
status = IoVolumeDeviceToDosName(FltObjects->FileObject->DeviceObject, &Drive_Location);
if (NT_SUCCESS(status))
{
fullPathName.Length = 0;
fullPathName.MaximumLength =
(USHORT)Drive_Location.MaximumLength + Data->Iopb->TargetFileObject->FileName.MaximumLength + 2;
fullPathName.Buffer = (PWCH) ExAllocatePoolWithTag(NonPagedPool,
fullPathName.MaximumLength,
'VedR');
if (fullPathName.Buffer == NULL) {
status = STATUS_INSUFFICIENT_RESOURCES;
BlacklistingAppLeave(status);
}
if (fullPathName.MaximumLength > 0xFFFF) {
status = STATUS_BUFFER_OVERFLOW;
BlacklistingAppLeave(status);
}
RtlCopyUnicodeString(&fullPathName, &Drive_Location);
RtlAppendUnicodeStringToString(&fullPathName, &Data->Iopb->TargetFileObject->FileName);
KdPrint(("%wZ \r\n", fullPathName));
}
}
}
if (DoesExecutableExistInPath(&fullPathName)) {
status = STATUS_ACCESS_DENIED;
KdPrint(("Application Blocked by Policy \n"));
return FLT_PREOP_COMPLETE;
}
}
__finally
{
if (NULL != nameInfo) {
FltReleaseFileNameInformation(nameInfo);
}
if (NULL != fullPathName.Buffer &&
fullPathName.MaximumLength > 0) {
ExFreePoolWithTag(fullPathName.Buffer,
'VedR');
fullPathName.Length = 0;
fullPathName.MaximumLength = 0;
}
}
return callbackStatus;
}
The Blocking Algorithm
The DoesExecutableExistInPath
function checks if a given file path corresponds to an executable that should be blocked. It extracts the file name from the full path and compares it against a predefined list of blocked executables.
BOOLEAN
DoesExecutableExistInPath(PCUNICODE_STRING fullPath)
{
UNICODE_STRING fileName = { 0, };
if (fullPath == NULL || fullPath->Length == 0) {
return FALSE;
}
if (!ExtractFileName(fullPath, &fileName)) {
return FALSE;
}
KdPrint(("Extracted file name: %wZ\n", fileName));
PCWSTR blockedExecutables[] = {
L"Notepad.exe",
L"Calc.exe",
L"Calculator.exe",
L"CalculatorApp.exe",
L"Excel.exe"
};
for (int i = 0; i < ARRAYSIZE(blockedExecutables); i++)
{
UNICODE_STRING executableName;
RtlInitUnicodeString(&executableName, blockedExecutables[i]);
if (ContainsSubstringIgnoreCase(&fileName, &executableName)) {
KdPrint(("Blocked executable found: %wZ\n", executableName));
return TRUE;
}
}
return FALSE;
}
BOOLEAN
ContainsSubstringIgnoreCase(PUNICODE_STRING source, PCUNICODE_STRING substring)
{
if (source == NULL || substring == NULL || source->Buffer == NULL || substring->Buffer == NULL) {
return FALSE;
}
if (source->Length < substring->Length) {
return FALSE;
}
for (size_t i = 0; i <= static_cast<size_t>(source->Length) - static_cast<size_t>(substring->Length); i += sizeof(WCHAR))
{
BOOLEAN match = TRUE;
for (size_t j = 0; j < substring->Length / sizeof(WCHAR); j++) {
if (RtlUpcaseUnicodeChar(source->Buffer[i / sizeof(WCHAR) + j]) !=
RtlUpcaseUnicodeChar(substring->Buffer[j])) {
match = FALSE;
break;
}
}
if (match) {
return TRUE;
}
}
return FALSE;
}
BOOLEAN
ExtractFileName(PCUNICODE_STRING fullPath, PUNICODE_STRING fileName)
{
if (fullPath == NULL || fullPath->Length == 0 || fileName == NULL) {
return FALSE;
}
WCHAR* buffer = fullPath->Buffer;
ULONG length = fullPath->Length / sizeof(WCHAR);
for (ULONG i = length - 1; i > 0; i--)
{
if (buffer[i] == L'\\' || buffer[i] == L'/') {
fileName->Buffer = &buffer[i + 1];
fileName->Length = (USHORT)((length - i - 1) * sizeof(WCHAR));
fileName->MaximumLength = fileName->Length;
return TRUE;
}
}
RtlCopyUnicodeString(fileName, fullPath);
return TRUE;
}
Installation
The proper way to install a file system mini-filter driver is by using an INF file. The INF files are used to install the hardware-based device drivers. But also can be used to install any driver on the Windows system. A complete treatment of INF files is beyond the scope of this article.
;;;
;;; BlacklistingApp inf file.
;;;
;
[Version]
Signature = "$Windows NT$"
Class = "ActivityMonitor" ;This is determined by the work this filter driver does
ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by the Class
Provider = %Msft%
DriverVer = 12/05/2024,1.0.0.0
CatalogFile = BlacklistingApp.cat
;PnpLockdown=0
[DestinationDirs]
DefaultDestDir = 12
MiniFilter.DriverFiles = 12 ;%windir%\system32\drivers
;;
;; Default install sections
;;
[DefaultInstall]
OptionDesc = %ServiceDescription%
CopyFiles = MiniFilter.DriverFiles
[DefaultInstall.Services]
AddService = %ServiceName%,,MiniFilter.Service
;;
;; Default uninstall sections
;;
[DefaultUninstall]
DelFiles = MiniFilter.DriverFiles
[DefaultUninstall.Services]
DelService = %ServiceName%,0x200 ;Ensure service is stopped before deleting
;
; Services Section
;
[MiniFilter.Service]
DisplayName = %ServiceName%
Description = %ServiceDescription%
ServiceBinary = %12%\%DriverName%.sys ;%windir%\system32\drivers\
Dependencies = FltMgr
ServiceType = 2 ;SERVICE_FILE_SYSTEM_DRIVER
StartType = 3 ;SERVICE_DEMAND_START
ErrorControl = 1 ;SERVICE_ERROR_NORMAL
LoadOrderGroup = "FSFilter Activity Monitor"
AddReg = MiniFilter.AddRegistry
;
; Registry Modifications
;
[MiniFilter.AddRegistry]
HKR,,"DebugFlags",0x00010001 ,0x0
HKR,,"SupportedFeatures",0x00010001,0x3
HKR,"Instances","DefaultInstance",0x00000000,%DefaultInstance%
HKR,"Instances\"%Instance1.Name%,"Altitude",0x00000000,%Instance1.Altitude%
HKR,"Instances\"%Instance1.Name%,"Flags",0x00010001,%Instance1.Flags%
;
; Copy Files
;
[MiniFilter.DriverFiles]
%DriverName%.sys
[SourceDisksFiles]
BlacklistingApp.sys = 1,,
[SourceDisksNames]
1 = %DiskId1%,,,
;;
;; String Section
;;
[Strings]
Msft = "BlacklistingApp Driver"
ServiceDescription = "BlacklistingApp driver"
ServiceName = "BlacklistingApp"
DriverName = "BlacklistingApp"
UserAppName = "BlacklistingApp"
DiskId1 = "MiniFilter Device Installation Disk"
;Instances specific information.
DefaultInstance = "BlacklistingApp Instance"
Instance1.Name = "BlacklistingApp Instance"
Instance1.Altitude = "370000"
Instance1.Flags = 0x0 ; Suppress automatic attachments
;
; Filter manager definitions
;
FltMgrServicesKey = "SYSTEM\CurrentControlSet\Services\FltMgr\"
AttachWhenLoaded = "AttachWhenLoaded"
For more information on how to modify the INF, please look at https://docs.microsoft.com/en-us/windows-hardware/drivers/ifs/creating-an-inf-file-for-a-file-system-driver.
Installing the Driver
Once the INF file is adequately modified and the driver code compiled, it is ready to be installed. The simplest install method is to copy the driver package (SYS, INF, and CAT files) to the target system, then right-click on the INF file explorer and select INSTALL. This will run the INF.
NOTE: If in INF, the start type of driver is 0, then reboot the system, and the driver gets loaded at boot time. For this sample, the start type of driver is Demand start, so no reboot is required.
At this point, the POC mini-filter driver is installed and can be loaded with the fltmc
command line tool (use elevated command prompt) and run.
C:\> fltmc load BlacklistingApp
As soon as the driver is loaded into the system, the InstanceSetup
callback will identify the device bus type and act accordingly.
To unload the driver from the system, use the following command:
c:\> fltmc unload BlacklistingApp.sys
Test Environment Preparation
You can temporarily disable the digital driver signature check to start a Windows driver test. In Windows 11, you can do this in the following way:
- bcdedit /set nointegritychecks on
- bcdedit /set debug on
- bcdedit /set testsigning on
- Reboot the machine and,
- Hold the Shift button and choose the Restart option in the main Windows menu.
- Select Troubleshoot -> Advanced Options -> Startup Settings -> Restart
- In Startup Settings, push F7 to choose the Disable driver signature enforcement option.
Important Note:
This article provides an educational overview of how to develop and implement kernel-level solution to enhance system security. While the techniques and examples discussed here aim to illustrate key concepts, they are not intended to serve as a fully robust, production-ready solution. Proper security measures require thorough testing, validation, and consideration of many factors beyond the scope of this guide.
That's It & RFC
That's it! Check it out and post any questions or comments about anything in the article. I hope you find it valuable. Please feel free to point out the mistakes made. I'll try to rectify them in future versions.
Points of Interest
Writing kernel-level code for application Blacklisting was a rewarding experience that provided deep insights into the Windows operating system. It involved handling complex tasks like file path normalization, memory management, and ensuring proper IRQL levels. While challenging, the process was also fun and enlightening, with clever implementations and solutions adding to the overall satisfaction of developing such a robust security mechanism.
History
- 19th June, 2024: First publication