Windows内核开发之串口过滤

学习了几个月的内核编程,现在对Windows驱动开发又了更加深入的认识,特别是对IRP的分层处理逻辑有了深入认识。

总结起来就几句话:

当irp下来的时候,你要根据实际情况,进行处理

1> 无处理,继续往下传

2> 处理之后 ,往下传

3> 处理之后, 往上传

4> 不做处理,直接丢弃

具体怎么理解,通过一个串口驱动过滤就可以深入理解。

一、串口过滤概念

串口过滤:平时我们看到的主机上的USB、网线口等都属于串口,那么设想一个环境,我去网吧上网,正在通过某宝付款,然后主机后面的USB插着一个串口监控器,把我的数据都获取了,然后我的钱就刷刷刷没了...何等悲哀的事情,所以,一个串口过滤,可以在不影响整个串口通信的功能上,拦截住用户层发送出去的信息,这个对于串口数据监控和安全有很大的作用。

二、串口过滤思路

首先是驱动过滤的原理:

“过滤”(filter)是极其重要的一个概念。过滤是在不影响上层和下层接口的情况下,在Windows系统内核中加入新的层,从而不需要修改上层的软件或者下层的真实驱动程序,就加入了新的功能。

也可以说,一个虚拟的设备Attach在一个真实的物理设备上,只要有消息包发送到真实物理设备的方向,则优先进入虚拟设备,而虚拟设备是我们绑定在真实设备上的,因此,优先拿到消息包,并对消息包的内容进行解析,过滤,这里的过滤可以说是对千万的IRP进行if选择,拿到需要的对应设备的IRP,从而,进行分析处理。

为了加深理解,我将逻辑思路做了简要的总结:

1>首先注册IRP的派遣函数(Dispathc Funtion),在这里函数内容做IRP的过滤操作,提取缓冲区,下传IRP等等

2>打开真实的串口设备,以获取到指向串口设备的对象指针,用于下一步的真实虚拟绑定

3>根据真实的串口设备,创建一个虚拟的串口过滤设备(IoCreateDevice)

4>把虚拟串口设备Attach在物理设备上

5>完善派遣函数:对IRP_MJ_WRITE进行过滤,完成过滤操作,将各类IRP做相关的分发处理

总之:OpenCom-----IoCreateFilterDevice-----AttachTo-----IRP_MJ_FUCTION

三、编码测试

一、内核API

1、绑定设备API

NTSTATUS
IoAttachDeviceToDeviceStackSafe(
IN PDEVICE_OBJECT SourceDevice, // 过滤设备
IN PDEVICE_OBJECT TargetDevice, // 要被绑定的设备栈中的设备
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject// 返回最终被绑定的设备
);

SourceDevice是调用者生成的用来过滤的虚拟设备;TargetDevice是要被绑定的目标设备。请注意这里的 TargetDevice并不是一个PDEVICE_OBJECT(DEVICE_OBJECT是设备对象的数据结构,以P开头的是其指针),而是一个字 符串(在驱动开发中字符串用UNICODE_STRING来表示)。实际上,这个字符串是要被绑定的设备的名字。Windows中许多设备对象是有名字的,但是并不是所有的设备对象都有名字。必须是有名字

的设备,才能用这个内核API进行绑定。在Windows中,串口设备是有固定名字的。这里有一个疑问:假设这个函数绑定一个名字所对应的设备,那么如果这个设备已经被其他的设备绑定了,会怎么样呢?如果一个设备被其他设备绑定,它们在一起的一组设备,被称为设备栈(之所以称为栈,是由于和请求的传递方式有关)。实际上,IoAttachDevice总是会绑定设备栈上最顶层的那个设备。

2、创建虚拟设备

NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);

这个函数看上去很复杂,但是目前使用时,还无须了解太多。DriverObject是本驱动的驱动对象。这个指针是系统提供,从DriverEntry中传入,在最后完整的例子中再解释。DeviceExtensionSize是设备扩展,读者请先简单地传入0。 DeviceName是设备名称。一个规则是:过滤设备一般不需要名称,所以传入NULL即可。DeviceType是设备类型,保持和被绑定的设备类型 一致即可。DeviceCharacteristics是设备特征,在生成设备对象时笔者总是凭经验直接填0,然后看是否排斥,选择FALSE。

值得注意的是,在绑定一个设备之前,应该把这个设备对象的多个子域设置成和要绑定的目标对象一致,包括标志和特征。下面是一个示例的函数,这个函数可以生成一个设备,然后绑定在另一个设备上。

3、绑定设备API

首先要获得真实设备对象的指针

NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT *FileObject,
OUT PDEVICE_OBJECT *DeviceObject
);

然后绑定串口,返回绑定之后的设备。比如topDev其实是指绑定之后的那一整个串口,可以说一个新的设备对象,通过对这个设备对象的操作,就可以完成对过滤设备的操作

topDev = IoAttachDeviceToDeviceStack(*fltObj, oldDev);

4、处理IRP,获取WRITE缓冲区内容

// 这里的irpsp称为IRP的栈空间,IoGetCurrentIrpStackLocation获得当前栈空间
// 栈空间是非常重要的数据结构
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
if(irpsp->MajorFunction == IRP_MJ_WRITE)
{
// 如果是写…
}
else if(irpsp->MajorFunction == IRP_MJ_READ)
{
// 如果是读…

Get到当前栈,也就是IRP的栈空间,然后通过,IRP->MajorFuction == IRP_MJ_WRITE来过滤出WRITE动作,同样,也能过滤到READ动作,因为这个IRP是从当前IRP来的,而这个IRP在物理层是串口数据发送,而我们的驱动刚好绑定的是真实物理串口设备,因此,能够拦截到这类IRP,也就是说,IRP虽然很多,但是,我们通过绑定设备完成了针对性的IRP拦截,从而达到了过滤作用。

5、请求完成

最终的结局有3种:

(1)请求被允许通过了。过滤不做任何事情,或者简单地获取请求的一些信息。但是请求本身不受干扰,这样系统行为不会有变化。

(2)请求直接被否决了。过滤禁止这个请求通过,这个请求被返回错误了,下层驱动程序根本收不到这个请求。这样系统行为就变了,后果是常常看见上层应用程序弹出错误框提示权限错误或者读取文件失败之类信息。

(3)过滤完成了这个请求。有时有这样的需求,比如一个读请求,我们想记录读到了什么。如果读请求还没有完成,那么如何知道到底会读到什么呢?只有让这个请求先完成再去记录。过滤完成这个请求时不一定要原封不动地完成,这个请求的参数可以被修改(比如把数据都加密一番)。

请求完成后,这里就说到IRP的处理:有四种,分别是

1> 无处理,继续往下传

2> 处理之后 ,往下传

3> 处理之后, 往上传

4> 不做处理,直接丢弃

诸如这样的处理,不做任何处理,直接往下传:

PoStartNextPowerIrp(irp);
IoSkipCurrentIrpStackLocation(irp);
return PoCallDriver(s_nextobj[i],irp);

完成IRP后,直接下传

IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextobj[i],irp);

四、模块代码

驱动入口:

#include "Driver.h"
#include <ntstrsafe.h> /////////////////////////////////////////////////
// 函数名:DriverEntry
// 功能:驱动入口
// 作者:Geons
// 时间:2016年4月14日22:02:54
///////////////////////////////////////////////// extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObj,
IN PUNICODE_STRING RegistryPath)
{
size_t i; // 注册派遣函数
for(i = 0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
{ DriverObj->MajorFunction[i] = ccpDispatch; } // 卸载驱动
DriverObj->DriverUnload = ccpUnload; // 绑定物理设备端口
ccpAttachAlloComs(DriverObj); return STATUS_SUCCESS; }

派遣函数:

// 分发函数
NTSTATUS ccpDispatch(PDEVICE_OBJECT device, PIRP irp)
{
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status;
ULONG i, j; for(i = 0; i< CCP_MAX_COM_ID;i++)
{
if(s_fltobj[i] == device)
{
if(irpsp->MajorFunction == IRP_MJ_POWER)
{
PoStartNextPowerIrp(irp);
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextobj[i], irp); } if(irpsp->MajorFunction == IRP_MJ_WRITE)
{
// 写请求分发处理 // 获得写请求的文字长度
ULONG len = irpsp->Parameters.Write.Length; // 开辟写缓冲区
PUCHAR buf = NULL; if(irp->MdlAddress != NULL)
{
buf = (PUCHAR)MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority); }
else
{
buf = (PUCHAR)irp->UserBuffer; }
if(buf == NULL)
{
buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer; } for(j = 0; j<len;j++)
{
KdPrint(("comcap:Send Data: %2x\r\n", buf[j]));
} } // 下发IRP
IoSkipCurrentIrpStackLocation(irp); return IoCallDriver(s_nextobj[i], irp); }
} irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_INVALID_PARAMETER;
IoCompleteRequest(irp, IO_NO_INCREMENT); // 返回IRP成功状态
return STATUS_SUCCESS; }

驱动卸载:

// 驱动卸载
void ccpUnload(PDRIVER_OBJECT drv)
{
ULONG i;
LARGE_INTEGER interval; for(i = 0; i<CCP_MAX_COM_ID;i++)
{
if(s_nextobj[i] != NULL)
{
IoDeleteDevice(s_nextobj[i]); } } interval.QuadPart = (5*1000*DELAY_ONE_MILLISECOND); KeDelayExecutionThread(KernelMode, FALSE, &interval); for(int i =0; i<CCP_MAX_COM_ID; i++)
{
if(s_nextobj[i] != NULL)
{ IoDeleteDevice(s_fltobj[i]); }
}
}

创建过滤设备

// 创建过滤设备,虚拟设备和真实设备的参数保持一致
NTSTATUS ccpAttachDevice(PDRIVER_OBJECT driverObj,
PDEVICE_OBJECT oldDev,
PDEVICE_OBJECT *fltObj,
PDEVICE_OBJECT *next)
{
NTSTATUS status;
PDEVICE_OBJECT topDev = NULL; status = IoCreateDevice(driverObj,
0,
NULL,
oldDev->DeviceType,
0,
FALSE,
fltObj); if(status != STATUS_SUCCESS)
{
return status; } if(oldDev->Flags & DO_BUFFERED_IO)
{
(*fltObj)->Flags |= DO_BUFFERED_IO; }
else if(oldDev->Flags & DO_DIRECT_IO)
{
(*fltObj)->Flags |= DO_DIRECT_IO;
}
else if(oldDev->Characteristics & FILE_DEVICE_SECURE_OPEN)
{
(*fltObj)->Characteristics |= FILE_DEVICE_SECURE_OPEN; }
else
{ } (*fltObj)->Flags |= DO_POWER_PAGABLE; topDev = IoAttachDeviceToDeviceStack(*fltObj, oldDev); if(topDev == NULL)
{
IoDeleteDevice(*fltObj);
*fltObj = NULL;
status = STATUS_SUCCESS;
return status; } *next = topDev; // 设置设备已经启动
(*fltObj)->Flags = (*fltObj)->Flags & ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS; }

打开串口,获得串口对象指针

// 打开Serial端口
PDEVICE_OBJECT ccpOpenCom(ULONG id, NTSTATUS *status)
{
// 外面输入的是串口id
UNICODE_STRING name_str;
static WCHAR name[32] = {0};
PFILE_OBJECT fileObj = NULL;
PDEVICE_OBJECT devObj = NULL; memset(name, 0, sizeof(WCHAR)*32);
RtlInitUnicodeString(&name_str, L"\\Device\\Serial0");
KdPrint(("the test serial is %w", name_str)); // 打开设备对象
*status = IoGetDeviceObjectPointer(&name_str, FILE_ALL_ACCESS, &fileObj, &devObj); // 打开文件成功后,解除文件对象的引用
if(*status == STATUS_SUCCESS)
{
ObDereferenceObject(fileObj); }
return devObj; }

绑定串口

// 绑定已有的串口
void ccpAttachAlloComs(PDRIVER_OBJECT driverObj)
{
ULONG i;
PDEVICE_OBJECT com_obj;
NTSTATUS status;
for(i =0; i<DPFLTR_TCPIP_ID;i++)
{
com_obj = ccpOpenCom(i, &status); if(com_obj == NULL)
{
continue; }
ccpAttachDevice(driverObj, com_obj, &s_fltobj[i], &s_nextobj[i]);
}
}

测试截图:

Windows内核开发之串口过滤

上一篇:你想知道的关于JavaScript作用域的一切(译)


下一篇:代码中使用StoryBoard和DoubleAnimation的方法