Driver development note 4 - serial port filtering


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]);
		}
	}

}

Posted by LuckyLucy on Wed, 27 Oct 2021 07:35:52 -0700