The filtering of serial port is very simple and has little significance in security development. It is only used as learning to lay the foundation for later keyboard filtering and file system filtering.
Serial port
Serial port is a device in Windows. It is relatively simple and has a fixed name. The first serial port is named "\ Device\Serial0", the second is "\ Device\Serial1", and so on.
filter
Filtering: add a new layer to the Windows system kernel without affecting the upper and lower interfaces, so as to add new functions without modifying the upper software or the real driver of the lower layer.
Implement filtering: first, generate a filtering device (virtual device object) and bind it to a real device. Once bound, the original request sent by the operating system to the real device will be sent to the filtering device first. We can capture the data in advance by analyzing the request.
Generate filter device object
The function IoCreateDevice is used to create a device object.
NTSTATUS IoCreateDevice( IN PDRIVER_OBJECT DriverObject, // Drive object IN ULONG DeviceExtensionSize, // Device extension, 0 IN PUNICODE_STRING DeviceName, // Device name, not required for filtering device, NULL IN DEVICE_TYPE DeviceType, // The device type should be consistent with the bound device IN ULONG DeviceCharacteristics, // Device characteristics, 0 IN BOOLEAN Exclusive, // FALSE OUT PDEVICE_OBJECT *DeviceObject); // Generated device object
Get target device object
When the device name is known, you can use the IoGetDeviceObjectPointer function.
When you get the device object, you will also get a file object. Note that you must dereference this file object later, otherwise memory leakage will occur.
NTSTATUS IoGetDeviceObjectPointer( IN PUNICODE_STRING ObjectName, // Device name IN ACCESS_MASK DesiredAccess, // Access rights OUT PFILE_OBJECT *FileObject, // File object OUT PDEVICE_OBJECT *DeviceObject); // Device object
Binding device
Windows provides a series of kernel API s to implement device binding.
1. IoAttachDevice
Many device objects in Windows have names, but not all of them. Only devices with names can be bound with IoAttachDevice.
If a device is already bound by other devices, they form a device stack. At this time, IoAttachDevice will bind the device at the top of the device stack.
NTSTATUS IoAttachDevice( IN PDEVICE_OBJECT SourceDevice, // Filter device objects IN PUNICODE_STRING TargetDeviceName, // Target device name OUT PDEVICE_OBJECT *AttachedDevice); // Bound device object
2. IoAttachDeviceToDeviceStackSafe
Devices without names can be bound with IoAttachDeviceToDeviceStackSafe, which is also the device at the top of the bound device stack.
NTSTATUS IoAttachDeviceToDeviceStackSafe( IN PDEVICE_OBJECT SourceDevice, // Filter device objects IN PDEVICE_OBJECT TargetDevice, // Device object to bind IN OUT PDEVICE_OBJECT *AttachedToDeviceObject); // Finally bound device object
Note that during device binding, multiple sub domains of the filtering device object should be set to be consistent with the target, including flags and features.
Serial port filtering
After the filter device is created and bound to the serial port, when the operating system wants to send an IRP request to the serial port, it will be sent to our filter device first, and we can filter it.
There are many kinds of IRPs, such as read request (IRP_MJ_READ) and write request (IRP_MJ_WRITE).
There are three filtering results:
- Allow request to pass: the filtering device simply obtains the requested information without any processing. The request is issued normally.
- Prohibit request from passing: the request ends with an error, and the application will receive a failure message.
- Completion request: the request ends and returns success.
Test code
#include "Ring0.h" #include <ntstrsafe.h> PDEVICE_OBJECT FltDevices[32] = { 0 }; PDEVICE_OBJECT AttachedDevices[32] = { 0 }; NTSTATUS OpenDevice(ULONG Index, PDEVICE_OBJECT *DeviceObject) { NTSTATUS Status = STATUS_UNSUCCESSFUL; PFILE_OBJECT FileObject = NULL; UNICODE_STRING DeviceName; WCHAR Name[32] = { 0 }; RtlStringCchPrintfW(Name, 32, L"\\Device\\Serial%d", Index); RtlInitUnicodeString(&DeviceName, Name); Status = IoGetDeviceObjectPointer(&DeviceName, FILE_ALL_ACCESS, &FileObject, DeviceObject); if (!NT_SUCCESS(Status)) { return Status; } ObDereferenceObject(FileObject); return Status; } NTSTATUS AttachDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT TargetDevice, PDEVICE_OBJECT *FltDevice, PDEVICE_OBJECT *AttachedDevice) { NTSTATUS Status = STATUS_UNSUCCESSFUL; // Create filter device Status = IoCreateDevice(DriverObject, 0, NULL, TargetDevice->DeviceType, 0, FALSE, FltDevice); if (!NT_SUCCESS(Status)) { return Status; } // Set important flag bit if (TargetDevice->Flags & DO_BUFFERED_IO) { (*FltDevice)->Flags |= DO_BUFFERED_IO; } if (TargetDevice->Flags & DO_DIRECT_IO) { (*FltDevice)->Flags |= DO_DIRECT_IO; } if (TargetDevice->Characteristics & FILE_DEVICE_SECURE_OPEN) { (*FltDevice)->Characteristics |= FILE_DEVICE_SECURE_OPEN; } (*FltDevice)->Flags |= DO_POWER_PAGABLE; // Binding device Status = IoAttachDeviceToDeviceStackSafe(*FltDevice, TargetDevice, AttachedDevice); if (!NT_SUCCESS(Status)) { return Status; } // Setup device started (*FltDevice)->Flags &= DO_DEVICE_INITIALIZING; return Status; } NTSTATUS ControlThroughDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PIO_STACK_LOCATION IoStackLocation; //Current IRP call stack space //Obtain the stack space of the current IRP (i.e. io_stack_location corresponding to the device of this layer) IoStackLocation = IoGetCurrentIrpStackLocation(Irp); for (ULONG i = 0; i < 32; i++) { if (FltDevices[i] == DeviceObject) { // All power operations are directly let go if (IoStackLocation->MajorFunction == IRP_MJ_POWER) { // Send directly, and then return that it has been processed PoStartNextPowerIrp(Irp); IoSkipCurrentIrpStackLocation(Irp); return PoCallDriver(AttachedDevices[i], Irp); } // Write request if (IoStackLocation->MajorFunction == IRP_MJ_WRITE) { ULONG Length = IoStackLocation->Parameters.Write.Length; PUCHAR Buffer = NULL; if (Irp->MdlAddress != NULL) { Buffer = (PUCHAR)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority); } else { Buffer = (PUCHAR)Irp->UserBuffer; } if (Buffer == NULL) { Buffer = (PUCHAR)Irp->AssociatedIrp.SystemBuffer; } for (ULONG j = 0; j < Length; j++) { DbgPrint("comcap: Send Data: %2x\r\n", Buffer[j]); } } // Send request directly IoSkipCurrentIrpStackLocation(Irp); return IoCallDriver(AttachedDevices[i], Irp); } } //Construct data packets and send them to the user layer Irp->IoStatus.Status = 0; Irp->IoStatus.Information = STATUS_INVALID_PARAMETER; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegisterPath) { // Dereference UNREFERENCED_PARAMETER(RegisterPath); NTSTATUS Status = STATUS_SUCCESS; PDEVICE_OBJECT TargetDevice = NULL; UNICODE_STRING ObjectName; // Bind all serial ports, assuming a total of 32 serial ports for (ULONG i = 0; i < 32; i++) { Status = OpenDevice(i, &TargetDevice); if (!NT_SUCCESS(Status) || TargetDevice == NULL) { continue; } AttachDevice(DriverObject, TargetDevice, &FltDevices[i], &AttachedDevices[i]); } // Set dispatch function routine for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) { DriverObject->MajorFunction[i] = ControlThroughDispatch; } // Set driver unloading routine DriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; } void DriverUnload(PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); // Unbind for (ULONG i = 0; i < 32; i++) { if (AttachedDevices[i] != NULL) { IoDetachDevice(AttachedDevices[i]); } } #define DELAY_ONE_MICROSECOND (-10) #define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND * 1000) #define DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND * 1000) // Sleep for 5 seconds and wait for all IRP processing to end LARGE_INTEGER Interval; Interval.QuadPart = (5000 * DELAY_ONE_MILLISECOND); KeDelayExecutionThread(KernelMode, FALSE, &Interval); // Delete filter device for (ULONG i = 0; i < 32; i++) { if (FltDevices[i] != NULL) { IoDeleteDevice(FltDevices[i]); } } }