📜 ⬆️ ⬇️

We write a simple driver under Windows to lock USB devices.

It is unlikely that a user of a home PC will be interested in blocking devices on his PC. But when it comes to the corporate environment, everything becomes different. There are users who can be trusted in absolutely everything, there are those who can delegate something, and there are those who cannot be trusted at all. For example, you blocked access to the Internet to one of the users, but did not block the devices of this PC. In this case, the user simply enough to bring a USB-modem, and he will have the Internet. Those. it is not limited to simply blocking access to the Internet.

Once about this task and stood 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 built, and I will tell the principle of the decision itself.

Also, complete source codes can be found in the USBLock folder of the git repository at: https://github.com/anatolymik/samples.git .
')

DRIVER_OBJECT structure


For each loaded driver, the system forms the DRIVER_OBJECT structure. The system actively uses this structure when it monitors the state of the driver. The driver is also responsible for its initialization, in particular for initializing the MajorFunction array. This array contains the addresses of handlers for all requests for which the driver can respond. 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 the specific request. Below is an example of initialization of 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; 

Such initialization is usually performed when the system calls the driver entry point, the 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 query types than in the example. Therefore, the entire array is first initialized so that requests that are not supported by the driver are processed correctly. For example, completed with an error. After array initialization, handlers are usually initialized for those requests for which the driver is responsible.

It also initializes the DriverUnload field of the structure, which contains the address of the handler responsible for terminating the driver. This field can be left non-initialized, in which case the driver becomes non-paged.

Please note that in the DriverExtension-> AddDevice field, 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 handle 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 a particular 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 which functionality is requested from it. If you do not use such a model, then the driver can only process any one functionality, but in the modern world this is unacceptable. A prototype of the function that processes a particular request is shown 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 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 it follows from the above-presented prototype. Also, the prototype takes the second parameter, which 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 by other drivers.

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

The DEVICE_OBJECT structure 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 install such a filter, you need to create another instance of DEVICE_OBJECT and attach it to DEVICE_OBJECT, whose requests should be intercepted. Attaching the filter 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 promotion request. First, the request will be processed by the top DEVICE_OBJECT driver, then the medium driver, and eventually the target DEVICE_OBJECT driver will receive control over the processing of the request. Also, the lower DEVICE_OBJECT is called the bottom of the stack, since it is not attached to anyone.

The presence of such a mechanism allows you to add functionality that is not initially in the drivers. For example, in this way, without modifying the FAT file system shipped in Windows, you can add file access permissions to this file system.

Pnp manager


The PnP manager is responsible for scheduling devices throughout the system. Its tasks include discovering devices, collecting information about them, loading their drivers, calling these drivers, managing hardware resources, starting and stopping devices, and deleting them.

When a driver of a bus detects devices on its interfaces, it creates a DEVICE_OBJECT for each child device. This DEVICE_OBJECT is also called a Physical Device Object or PDO. Then, using the IoInvalidateDeviceRelations function, it notifies the PnP manager that there have been changes on the bus. In response, the PnP manager sends a request with the minor code IRP_MN_QUERY_DEVICE_RELATIONS 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 stack of this hub is not shown for brevity and to preserve the sequence of explanations.

As soon as the PnP manager receives a list of all PDOs, it will separately collect all the necessary information about these devices. For example, a request will be sent with the minor code IRP_MN_QUERY_ID. Through this request, the PnP manager will obtain 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. And so on.

After all the necessary information has been collected, the PnP manager will create a so-called DevNode, which will reflect the state of the device. Also, PnP will create a registry branch for a specific device instance 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 previously mentioned that the DriverExtension-> AddDevice field of the DRIVER_OBJECT structure contains the address of the handler that is called whenever the system detects a new device. The prototype of this handler is shown below.

 NTSTATUS DispatchAddDevice( PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT PhysicalDeviceObject ); 

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

The task of the handler is to create a DEVICE_OBJECT and attach it to the PDO. Attached DEVICE_OBJECT is also called 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. Below is an example of when PnP has finished calling the driver responsible for the operation of the device.


As reflected in the example, in addition to the device driver itself, lower and upper device class filters can also be registered. Consequently, if any, PnP will also load their drivers and call their AddDevice handlers. Those. The order of calling the drivers is as follows: first, the registered lower filters are loaded and called, then the device driver is loaded and called, and in the end, the upper filters are loaded and called. The lower and upper filters are the usual DEVICE_OBJECT, which create drivers and attach them to the PDO 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 the 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 is completed 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 changes have occurred on the bus, it notifies PnP using the IoInvalidateDeviceRelations function that it should reassemble information about the connected devices. At this point, the driver does not delete the previously created PDO. Just when receiving a request with a minor code IRP_MN_QUERY_DEVICE_RELATIONS, he will not include this PDO in the list. Then, based on the received list, PnP identifies new devices and devices that have been disconnected from the bus. The PDO of the disabled devices will be deleted by the driver when the PnP sends a request with the minor code IRP_MN_REMOVE_DEVICE. For a driver, this request means that the device is not used by anyone else, and it can be safely removed.

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

The essence of the decision


The essence of the solution itself 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. As MSDN says, this class is used for USB host controllers and hubs. However, in practice this is not the case; the same class is used, for example, by flash-drives. This adds a bit of work to us. Handler code AddDevice 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 follows from the example, we create a 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 the 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 necessary checks to determine if the device is allowed to operate. First of all, the function allows you to always work hubs and composite devices, keyboards and mice. As well as those devices that are on the allowed list. If the function returns an unsuccessful return code, then the request fails. Thus, the operation of the device will be blocked.

Note: the function determines whether the device is a hub or a composite device. This is necessary because, as already mentioned, the device class that is used for hubs and host controllers is not only used by these devices. And we first need to control the child devices of only hubs, host controllers and composite devices. Those. for hubs and composite devices, the enumeration request for 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 lower. Otherwise, control over the child devices will be lost.

All the definitions mentioned are based on device identifiers.

Conclusion


Despite its simplicity in my case, this driver effectively solves the problem. Although of the shortcomings should be made a mandatory reboot after the list of authorized devices will be updated. To eliminate this drawback, the driver will need some complication. Another major disadvantage is the complete blocking of the device, and not partial. The description presented 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 get acquainted with the source code.

Source: https://habr.com/ru/post/330844/


All Articles