Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security

Kernel-Level Application Blacklisting with C++ and Windows

4.85/5 (5 votes)
22 Jun 2024CPOL6 min read 6.5K   23  
A comprehensive guide to developing and implementing kernel-level application - Blacklisting using C++ for enhanced system security.
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.

C++
extern "C"
NTSTATUS
DriverEntry(
    __in PDRIVER_OBJECT DriverObject,
    __in PUNICODE_STRING RegistryPath
)
{
    NTSTATUS status = STATUS_SUCCESS;

    //DbgBreakPoint();

    UNREFERENCED_PARAMETER(RegistryPath);

    KdPrint(("PocMiniFilter!DriverEntry: Entered\n"));

    MiniFilterData.DriverObject = DriverObject;

    //
    //  Register with FltMgr with our callback routines.
    //

    status = FltRegisterFilter( DriverObject,
                                &FilterRegistration,
                                &MiniFilterData.Filter);

    FLT_ASSERT(NT_SUCCESS(status));

    if (NT_SUCCESS(status)) {

        //
        //  Start filtering i/o
        //

        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.

C++
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.

C++
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.

C++
/*
    MiniFilter callback routines.

    Minifilter driver's PreCreate() callback routine, will check the conditions
    to monitor and potentially restrict certain types of file operations for 
    system 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
    {
        // We only registered for this irp, so thats all we better get!
        NT_ASSERT(Data->Iopb->MajorFunction == IRP_MJ_CREATE);

        // Check the Irql here if APC return with STATUS_UNSUCCESSFUL.
        if (KeGetCurrentIrql() > APC_LEVEL) {
            BlacklistingAppLeave(status);
        }

        FileObject = FltObjects->FileObject;
        if (FileObject == NULL)
        {
            // Can't get a name when there's no file object
            KdPrint(("No FileObject found in PreCreateCallback\n"));
            return callbackStatus;
        }

        if (FlagOn(Data->Iopb->OperationFlags, SL_OPEN_PAGING_FILE)) {
            return callbackStatus;
        }

        //  Opens by file ID are name agnostic. Thus we do not care about this open.
        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;
        }

        // To get a file path using filter manager API.
        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);
                    }

                    // The maximum length does not exceed the USHORT limit
                    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
    {
        //  Release the name information structure (if defined)
        if (NULL != nameInfo) {

            FltReleaseFileNameInformation(nameInfo);
        }

        //  Release the name information structure (if defined)
        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.

C++
BOOLEAN 
DoesExecutableExistInPath(PCUNICODE_STRING fullPath) 
{
    UNICODE_STRING fileName = { 0, };

    // Check if fullPath is NULL or empty
    if (fullPath == NULL || fullPath->Length == 0) {
        return FALSE;
    }

    if (!ExtractFileName(fullPath, &fileName)) {
        return FALSE;
    }

    KdPrint(("Extracted file name: %wZ\n", fileName));

    // Array of executables to block
    PCWSTR blockedExecutables[] = {
        L"Notepad.exe",
        L"Calc.exe",
        L"Calculator.exe",
        L"CalculatorApp.exe",
        L"Excel.exe"
    };

    // Iterate in array and check for executable(s).
    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;
}
C++
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;
        }
    }

    // if not found, return directly.
    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.

C++
;;;
;;; BlacklistingApp inf file.
;;;

;/**************************************************************************
; *                                                                        *
; * Copyright (C) 2024 Rahul Dev Tripathi                                  *
; *                                                                        *
; * This file is part of the [Application Blacklisting].                   *
; *                                                                        *
; * Permission is hereby granted, free of charge, to any person obtaining  *
; * a copy of this software and associated documentation files (the        *
; * "Software"), to deal in the Software without restriction, including    *
; * without limitation the rights to use, copy, modify, merge, publish,    *
; * distribute, sublicense, and/or sell copies of the Software, and to     *
; * permit persons to whom the Software is furnished to do so, subject to  *
; * the following conditions:                                              *
; *                                                                        *
; * The above copyright notice and this permission notice shall be         *
; * included in all copies or substantial portions of the Software.        *
; *                                                                        *
; * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     *
; * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. *
; * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   *
; * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   *
; * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      *
; * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 *
; *                                                                        *
; * For commercial use or if used in production environments,              *
; * please contact Rahul Dev Tripathi at r.tripathi.cse@gmail.com for      *
; * permission.                                                            *
; *                                                                        *
; **************************************************************************/


[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:

  1. bcdedit /set nointegritychecks on
  2. bcdedit /set debug on
  3. bcdedit /set testsigning on
  4. Reboot the machine and,
  5. Hold the Shift button and choose the Restart option in the main Windows menu.
  6. Select Troubleshoot -> Advanced Options -> Startup Settings -> Restart
  7. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)