We write a simple driver for Windows to block USB devices

    It is unlikely that a home PC user will be interested in blocking devices on their PC. But if it comes to the corporate environment, then everything becomes different. There are users who can be trusted in absolutely everything, there are those who can be delegated to something, and there are those who cannot be trusted at all. For example, you blocked the Internet access to one of the users, but did not block the devices of this PC. In this case, the user just needs to bring a USB modem, and he will have the Internet. Those. it’s more than just blocking access to the Internet.

    Once, such a task was in front of me. There was no time to search for any solutions on the Internet, and they, as a rule, are not free. Therefore, it was easier for me to write such a driver, and its implementation took me one day.

    In this article I will tell a little theoretical part on the basis of which everything is under construction, and I will tell the principle of the decision itself.

    Full source codes can also be found in the USBLock folder of the git repository at: https://github.com/anatolymik/samples.git .

    Structure DRIVER_OBJECT


    For each loaded driver, the system generates a DRIVER_OBJECT structure. The system actively uses this structure when it monitors the status of the driver. Also, the driver is responsible for its initialization, in particular for the initialization of the MajorFunction array. This array contains the addresses of the handlers for all requests for which the driver may be responsible. Therefore, when the system sends a request to the driver, it will use this array to determine which function of the driver is responsible for a particular request. The following is an example of initializing this structure.

    for ( ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
    	DriverObject->MajorFunction[i] = DispatchCommon;
    }
    DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_CLEANUP] = DispatchCleanup;
    DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
    DriverObject->DriverUnload = DriverUnload;
    DriverObject->DriverExtension->AddDevice = DispatchAddDevice; 

    This initialization is usually performed when the system calls the driver entry point, a prototype of which is shown below.

    NTSTATUS DriverEntry( PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath ); 

    As you can see from the example, first the entire MajorFunction array is initialized with the same handler. In reality, there are more types of queries than in the example. Therefore, previously the entire array is initialized so that requests that are not supported by the driver are processed correctly. For example, they failed. After array initialization, handlers are usually initialized for those requests for which the driver is responsible.

    The DriverUnload field of the structure is also initialized, which contains the address of the handler responsible for shutting down the driver. This field may be left uninitialized, in which case the driver becomes non-downloadable.

    Please note that in the field DriverExtension-> AddDevice the address of the handler is set, which is called whenever the system detects a new device for which the driver is responsible. This field may be left uninitialized, in which case the driver will not be able to process this event.

    This structure is described in more detail at: https://msdn.microsoft.com/en-us/library/windows/hardware/ff544174(v=vs.85).aspx .

    DEVICE_OBJECT structure


    The DEVICE_OBJECT structure represents one or another driver functionality. Those. this structure can represent a physical device, a logical device, a virtual device, or just some functionality provided by the driver. Therefore, when the system sends requests, it will indicate the address of this structure in the request itself. Thus, the driver will be able to determine what functionality is requested from it. If you do not use such a model, then the driver can only process any one functionality, and in the modern world this is unacceptable. The prototype of the function that processes the particular request is given below.

    NTSTATUS Dispatch( PDEVICE_OBJECT DeviceObject, PIRP Irp ); 

    The MajorFunction array of the previously mentioned DRIVER_OBJECT structure contains the addresses of the handlers with exactly this prototype.

    The DEVICE_OBJECT structure itself is always created by the driver using the IoCreateDevice function. If the system sends a request to the driver, then it always sends it to some DEVICE_OBJECT, as follows from the above prototype. Also, the prototype accepts a second parameter that contains the address of the IRP structure. This structure describes the request itself, and it exists in memory until the driver completes it. The request is sent to the driver for processing using the IoCallDriver function both by the system and other drivers.

    A name may also be associated with the DEVICE_OBJECT structure. So this DEVICE_OBJECT can be found in the system.

    The structure of DEVICE_OBJECT is described in more detail at: https://msdn.microsoft.com/en-us/library/windows/hardware/ff543147(v=vs.85).aspx . And the IRP structure is described at: https://msdn.microsoft.com/en-us/library/windows/hardware/ff550694(v=vs.85).aspx .

    Filtration


    Filtering is a mechanism that allows you to intercept all requests directed to a specific DEVICE_OBJECT. To set such a filter, you need to create another instance of DEVICE_OBJECT and attach it to DEVICE_OBJECT, whose requests must be intercepted. Filter attachment is done through the IoAttachDeviceToDeviceStack function. All DEVICE_OBJECT attached to the intercepted DEVICE_OBJECT together with it form the so-called device stack, as shown below.


    The arrow shows the progress of the request. First, the request will be processed by the driver of the top DEVICE_OBJECT, then by the middle driver and, finally, the driver of the target DEVICE_OBJECT will receive control over the processing of the request. Also, the bottom DEVICE_OBJECT is called the bottom of the stack, as he is not attached to anyone.

    The presence of such a mechanism allows you to add functionality that is not originally in the drivers. For example, in this way, without modifying the FAT file system supplied with Windows, you can add a check for access rights to the files of this file system.

    PnP Manager


    The PnP manager is responsible for dispatching devices throughout the system. His tasks include detecting devices, collecting information about them, loading their drivers, calling these drivers, managing hardware resources, starting and stopping devices, and removing them.

    When the driver of a particular bus detects devices on its interfaces, it creates DEVICE_OBJECT for each child device. This DEVICE_OBJECT is also called a Physical Device Object or PDO. Then, through the IoInvalidateDeviceRelations function, it notifies the PnP manager that a change has occurred on the bus. In response to this, the PnP manager sends a request with a minor code IRP_MN_QUERY_DEVICE_RELATIONS in order to request a list of child devices. In response to this request, the bus driver returns a list of PDOs. Below is an example of such a situation.


    As shown in the figure, in this example, the USB hub driver acts as a bus. The specific DEVICE_OBJECT device stacks of this hub are not shown for brevity and in order to preserve the sequence of explanations.

    As soon as the PnP manager receives a list of all PDOs, he individually collects all the necessary information about these devices. For example, a query will be sent with minor code IRP_MN_QUERY_ID. Through this request, the PnP manager will receive device identifiers, both hardware and compatible. Also, the PnP manager will collect all the necessary information about the required hardware resources by the device itself. Etc.

    After all the necessary information has been collected, the PnP manager will create the so-called DevNode, which will reflect the status of the device. PnP will also create a registry branch for a specific instance of the device or open an existing one if the device was previously connected to a PC.

    The next PnP task is to start the device driver. If the driver has not been previously installed, then PnP will wait for the installation. Otherwise, if necessary, PnP will load it and transfer control to it. It was mentioned earlier that the DriverExtension-> AddDevice field of the DRIVER_OBJECT structure contains the address of the handler, which is called whenever the system detects a new device. A prototype of this handler is shown below.

    NTSTATUS DispatchAddDevice( 
    	PDRIVER_OBJECT DriverObject, 
    	PDEVICE_OBJECT PhysicalDeviceObject 
    ); 

    Those. whenever PnP detects a device that is controlled by a particular driver, a registered handler of that driver is called, where a pointer to a PDO is passed to it. Information about the installed driver is also stored in the corresponding registry branch.

    The handler's task is to create DEVICE_OBJECT and attach it to the PDO. The attached DEVICE_OBJECT is also called a Functional Device Object or FDO. It is this FDO that will be responsible for the operation of the device and the presentation of its interfaces in the system. The following is an example when PnP completed a call to the driver responsible for the operation of the device.


    As reflected in the example, in addition to the device driver itself, the lower and upper filters of the device class can also be registered. Therefore, if any, PnP will also load their drivers and call their AddDevice handlers. Those. The procedure for calling drivers is as follows: first, registered lower filters are loaded and called, then the driver of the device itself is loaded and called, and finally, the upper filters are loaded and called. The bottom and top filters are regular DEVICE_OBJECTs that create drivers and attach them to PDOs in their AddDevice handlers. The number of lower and upper filters is not limited.

    At this point, the device stacks are fully formed and ready to go. Therefore, PnP sends a request with minor code IRP_MN_START_DEVICE. In response to this request, all device stack drivers must prepare the device for operation. And if there are no problems in this process, then the request completes successfully. Otherwise, if any of the drivers cannot start the device, then it completes the request with an error. Therefore, the device will not start.

    Also, when the bus driver determines that a change has occurred on the bus, it notifies PnP through the IoInvalidateDeviceRelations function that it is necessary to re-gather information about connected devices. At this point, the driver does not delete the previously created PDO. Just when you receive a request with minor code IRP_MN_QUERY_DEVICE_RELATIONS, it will not include this PDO in the list. Then, on the basis of the list received, PnP identifies new devices and devices that have been disconnected from the bus. The driver will delete the PDO of disconnected devices when PnP sends a request with minor code IRP_MN_REMOVE_DEVICE. For the driver, this request means that the device is no longer used by anyone, and it can be safely removed.

    More information on the WDM driver model can be found at:https://msdn.microsoft.com/en-us/library/windows/hardware/ff548158(v=vs.85).aspx .

    Essence of the decision


    The essence of the solution is to create a top class USB bus filter. Reserved classes can be found at: https://msdn.microsoft.com/en-us/library/windows/hardware/ff553419(v=vs.85).aspx . We are interested in a USB class with a GUID of 36fc9e60-c465-11cf-8056-444553540000. According to MSDN, this class is used for USB host controllers and hubs. However, this is practically not the case, the same class is used, for example, by flash-drives. This adds a bit of work to us. The AddDevice handler code is presented below.

    NTSTATUS UsbCreateAndAttachFilter( 
    	PDEVICE_OBJECT PhysicalDeviceObject, 
    	bool UpperFilter 
    ) {
    	SUSBDevice*		USBDevice;
    	PDEVICE_OBJECT		USBDeviceObject = nullptr;
    	ULONG			Flags;
    	NTSTATUS		Status = STATUS_SUCCESS;
    	PAGED_CODE();
    	for ( ;; ) {
    		// если нижний фильтр уже прикреплен, тогда здесь больше делать нечего
    		if ( !UpperFilter ) {
    			USBDeviceObject = PhysicalDeviceObject;
    			while ( USBDeviceObject->AttachedDevice ) {
    				if ( USBDeviceObject->DriverObject == g_DriverObject ) {
    					return STATUS_SUCCESS;
    				}
    				USBDeviceObject = USBDeviceObject->AttachedDevice;
    			}
    		}
    		// создаем фильтр
    		Status = IoCreateDevice(
    			g_DriverObject,
    			sizeof( SUSBDevice ),
    			nullptr,
    			PhysicalDeviceObject->DeviceType,
    			PhysicalDeviceObject->Characteristics,
    			false,
    			&USBDeviceObject
    		);
    		if ( !NT_SUCCESS( Status ) ) {
    			break;
    		}
    		// инициализируем флаги созданного устройства, копируем их из объекта к 
    		// которому прикрепились
    		Flags = PhysicalDeviceObject->Flags & 
    		 (DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE);
    		USBDeviceObject->Flags |= Flags;
    		// получаем указатель на нашу структуру
    		USBDevice = (SUSBDevice*)USBDeviceObject->DeviceExtension;
    		// инициализируем деструктор
    		USBDevice->DeleteDevice = DetachAndDeleteDevice;
    		// инициализируем обработчики
    		for ( ULONG i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++ ) {
    			USBDevice->MajorFunction[i] = UsbDispatchCommon;
    		}
    		USBDevice->MajorFunction[IRP_MJ_PNP] = UsbDispatchPnp;
    		USBDevice->MajorFunction[IRP_MJ_POWER] = UsbDispatchPower;
    		// инициализируем семафор удаления устройства
    		IoInitializeRemoveLock( 
    			&USBDevice->Lock, 
    			USBDEVICE_REMOVE_LOCK_TAG, 
    			0, 
    			0 
    		);
    		// заполняем структуру
    		USBDevice->SelfDevice = USBDeviceObject;
    		USBDevice->BaseDevice = PhysicalDeviceObject;
    		USBDevice->UpperFilter = UpperFilter;
    		// инициализируем paging семафор
    		USBDevice->PagingCount = 0;
    		KeInitializeEvent( &USBDevice->PagingLock, SynchronizationEvent, true );
    		// прикрепляем устройство к PDO
    		USBDevice->LowerDevice = IoAttachDeviceToDeviceStack( 
    			USBDeviceObject, 
    			PhysicalDeviceObject 
    		);
    		if ( !USBDevice->LowerDevice ) {
    			Status = STATUS_NO_SUCH_DEVICE;
    			break;
    		}
    		break;
    	}
    	// в зависимости от результата делаем
    	if ( !NT_SUCCESS( Status ) ) {
    		// отчистку
    		if ( USBDeviceObject ) {
    			IoDeleteDevice( USBDeviceObject );
    		}
    	} else {
    		// или сбрасываем флаг инициализации
    		USBDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
    	}
    	return Status;
    }
    static NTSTATUS DispatchAddDevice( 
    	PDRIVER_OBJECT DriverObject, 
    	PDEVICE_OBJECT PhysicalDeviceObject 
    ) {
    	UNREFERENCED_PARAMETER( DriverObject );
    	return UsbCreateAndAttachFilter( PhysicalDeviceObject, true );
    }

    As the example shows, we create DEVICE_OBJECT and attach it to the PDO. Thus, we will intercept all requests directed to the USB bus.

    Our task is to intercept requests with minor code IRP_MN_START_DEVICE. The handler code for this request is shown below.

    static NTSTATUS UsbDispatchPnpStartDevice( SUSBDevice* USBDevice, PIRP Irp ) {
    	bool		HubOrComposite;
    	NTSTATUS	Status;
    	PAGED_CODE();
    	for ( ;; ) {
    		// проверить, позволено ли устройству работать, также обновить
    		// информацию об устройстве, является ли оно хабом или композитным
    		Status = UsbIsDeviceAllowedToWork( &HubOrComposite, USBDevice );
    		if ( !NT_SUCCESS( Status ) ) {
    			break;
    		}
    		USBDevice->HubOrComposite = HubOrComposite;
    		// продвинуть запрос
    		Status = ForwardIrpSynchronously( USBDevice->LowerDevice, Irp );
    		if ( !NT_SUCCESS( Status ) ) {
    			break;
    		}
    		break;
    	}
    	// завершаем запрос
    	Irp->IoStatus.Status = Status;
    	IoCompleteRequest( Irp, IO_NO_INCREMENT );
    	// и освобождаем устройство
    	IoReleaseRemoveLock( &USBDevice->Lock, Irp );
    	return Status;
    }

    As shown in the figure, the handler calls the UsbIsDeviceAllowedToWork function. This function performs all the necessary checks to determine if the device is allowed to work. First of all, the function allows you to always work with hubs and composite devices, keyboards and mice. And also to those devices that are in the list of allowed. If the function returns an unsuccessful return code, then the request fails. Thus, the operation of the device will be blocked.

    Please note: the function determines whether the device is a hub or a composite device. This is necessary because, as already mentioned, the class of devices that is used for hubs and host controllers is used not only by these devices. And first of all, we need to control the daughter devices of only the hubs, host controllers and composite devices. Those. for hubs and composite devices, the request for listing child devices is additionally intercepted; at this stage, it is also important to attach a filter to all child devices, and this filter will be the bottom one. Otherwise, control over child devices will be lost.

    All of these definitions are based on device identifiers.

    Conclusion


    Despite its simplicity, in my case, this driver quite effectively solves the task. Although one of the drawbacks is the obligatory reboot after the list of allowed devices is updated. To eliminate this drawback, the driver will need to complicate a little. An even bigger drawback is the complete blocking of the device, and not partial. The description above does not disclose all implementation details. This was done intentionally, and the emphasis was placed more on the concept itself. Those who want to understand everything to the end can familiarize themselves with the source code.

    Also popular now: