转 Windows串口过滤驱动程序的开发

在Windows系统上与安全软件相关的驱动开发过程当中,“过滤(filter)”是极其重要的一个概念。过滤是在不影响上层和下层接口的状况下,在Windows系统内核中加入新的层,从而不须要修改上层的软件和下层的真实驱动,就加入了新的功能。算法

 

过滤的概念和基础编程

 

1.设备绑定的内核API之一数组

进行过滤的最主要方法是对一个设备对象(Device Object)进行绑定。经过编程生成一个虚拟设备,并“绑定”(Attach)在一个真实的设备上。一旦绑定,则原本操做系统发送给真实设备的请求,就会首先发送的这个虚拟设备。安全

在WDK中,有多个内核API能实现绑定功能。下面是其中一个函数的原型:数据结构

[cpp]  view plain  copy
 
  1. NTSTATUS   
  2.    IoAttachDevice(  
  3.            IN PDEVICE_OBJECT SourceDevice,  
  4.            IN PUNICODE_STRING TargetDevice,  
  5.            OUT PDEVICE_OBJECT *AttachedDevice  
  6.           );  


IoAttachDevice的参数以下:函数

SouceDevice是调用者生成的用来过滤的虚拟设备;而TargetDevice是要被绑定的目标设备。请注意这里的TargetDevice并非一个PDEVICE_OBJECT,而是设备的名字。测试

若是一个设备被其余设备绑定了,他们在一块儿的一组设备,被称为设备栈。实际上,IoAttachDevice总会绑定设备栈最上层的那个设备。spa

AttachedDevice是一个用来返回的指针的指针。绑定成功后,被绑定的设备的指针返回到这个地址。操作系统

下面这个例子绑定串口1。之因此这里绑定很方便,是由于在Windows中,串口设备有固定的名字。第一个串口名字为“\Device\Serial0”,第二个为“\Device\Serial1”,以此类推。请注意实际编程时C语言中的“\”要写成“\\”。.net

[cpp]  view plain  copy
 
  1. UNICODE_STRING com_name = RLT_CONSTANT_STRING(L"\\Device\\Serial0");  
  2. NTSTATUS status = IoAttachDevice(  
  3.     com_filter_device,   //生成的过滤设备  
  4.     &com_device_name,    //串口的设备名  
  5.     &attached_device     //被绑定的设备指针返回到这里  
  6.     );  


2.绑定设备的内核API之二

并非全部设备都有设备名字,因此依靠IoAttachDevice没法绑定没有名字的设备。另外还有两个API:一个是IoAttachDeviceToDeviceStack,另外一个是IoAttachDeivceToDeviceStackSafe。这两个函数功能同样,都是根据设备对象的指针(而不是名字)进行绑定;区别是后者更加安全,并且只有在Windows2000SP4和Windows XP以上的系统中才有。

[cpp]  view plain  copy
 
  1. NTSTATUS  
  2.   IoAttachDeviceToDeviceStackSafe(   
  3.                                       IN PDEVICE_OBJECT SourceDevice,     //过滤设备  
  4.                   IN PDEVICE_OBJECT TargetDevice,     //要被绑定的设备  
  5.                   IN OUT PDEVICE_OBJECT *AttachedToDeviceObject  //返回最终绑定的设备  
  6.                   );  


和第一个API相似,只是TargetDevice换成了一个指针。另外,AttachedToDeviceObject一样也是返回最终被绑定的设备,实际上也就是以前设备栈上最顶端的那个设备。

 

3.生成过滤设备并绑定

在绑定一个设备以前,先要知道如何生成一个用于过滤的过滤设备。函数IoCreateDevice被用于生成设备:

[cpp]  view plain  copy
 
  1. NTSTATUS  
  2.   IoCreateDevice(  
  3.                    IN PDRIVER_OBJECT DriverObject,  
  4.          IN ULONG DeviceExtensionSize,  
  5.          IN PUNICODE_STRING DeviceName,  
  6.          IN DEVICE_TYPE DeviceType,   
  7.          IN ULONG DeviceCharacteristics,  
  8.          IN BOOLEAN Exclusive,   
  9.          OUT PDEVICE_OBJECT *DeviceObject  
  10.          );  

DriverObject:输入参数,每一个驱动程序中会有惟一的驱动对象与之对应,但每一个驱动对象会有若干个设备对象。DriverObject指向的就是驱动对象指针。

DeviceExtensionSize:输入参数,指定设备扩展的大小,I/O管理器会根据这个大小,在内存中建立设备扩展,并与驱动对象关联。

DeviceName:输入参数,设置设备对象的名字。一个规则是,过滤设备通常不须要设备名,传入NULL便可

DeviceType:输入参数,设备类型,保持和被绑定的设备类型一致便可。

DeviceCharacterristics:输入参数,设备对象的特征。

Exclusive:输入参数,设置设备对象是否为内核模式下使用,通常设置为TRUE。

DeviceObject:输出参数,I/O管理器负责建立这个设备对象,并返回设备对象的地址。

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

[cpp]  view plain  copy
 
  1. NTSTATUS  
  2.  ccpAttachDevice(  
  3.      PDRIVER_OBJECT driver,  
  4.      PDEVICE_OBJECT oldobj,  
  5.      PDEVICE_OBJECT *fltobj,  
  6.      PDEVICE_OBJECT *next)  
  7. {  
  8.     NTSTATUS status;  
  9.     PDEVICE_OBJECT topdev = NULL;  
  10.   
  11.     //生成设备而后绑定  
  12.     status = IoCreateDevice(driver,  
  13.                             0,  
  14.                   NULL,  
  15.                           oldobj->DeviceType,  
  16.                   0,  
  17.                   FALSE,  
  18.                   fltobj);  
  19.     if (status != STATUS_SUCCESS)  
  20.     {  
  21.         return status;  
  22.     }  
  23.   
  24.     //拷贝重要标志位  
  25.     if (oldobj->Flags & DO_BUFFERED_IO)  
  26.     {  
  27.         (*fltobj)->Flags |= DO_BUFFERED_IO;  
  28.     }  
  29.     if(oldobj->Flags & DO_DIRECT_IO)  
  30.     {  
  31.         (*fltobj)->Flags |= DO_DIRECT_IO;  
  32.     }  
  33.     if (oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN)  
  34.     {  
  35.         (*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN;  
  36.     }  
  37.     (*fltobj)->Flags |= DO_POWER_PAGABLE;  
  38.     //将一个设备绑定到另外一个设备  
  39.     topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj);  
  40.     if (topdev == NULL)  
  41.     {  
  42.         //若是绑定失败了,销毁设备,返回错误  
  43.         IoDeleteDevice(*fltobj);  
  44.         *float = NULL;  
  45.         status = STATUS_UNSUCCESSFUL;  
  46.         return status;  
  47.     }  
  48.     *next = topdev;  
  49.   
  50.     //设置这个设备已经启动  
  51.     (*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;  
  52.     return STATUS_SUCCESS;  
  53. }  


 

4.从名字得到设备对象

在知道一个设备名字的状况下,使用IoGetDeviceObjectPointer能够得到这个设备对象的指针。这个函数的原型以下:

[cpp]  view plain  copy
 
  1. NTSTATUS   
  2.   IoGetDeviceObjectPointer(   
  3.                    IN PUNICODE_STRING ObjectName,  
  4.          IN ACCESS_MASK DesiredAccess,  
  5.          OUT PFILE_OBJECT *FileObject,  
  6.          OUT PDEVICE_OBJECT *DeviceObject   
  7.         );  


其中ObjectName就是设备名字。

DesireAccess是指望访问的权限。实际使用时不要顾虑那么多,直接填写FILE_ACCESS_ALL便可。

FileObject是一个返回参数,即得到设备对象的同时会获得一个文件对象(File Object)。就打开串口这件事而言,这个文件对象没有什么用处。但必须注意:在使用这个函数以后必须把这个文件对象“解除引用”,不然会引发内存泄露。

 

要获得的设备对象就返回在参数DeviceObject中了。

[cpp]  view plain  copy
 
  1. //打开一个端口设备  
  2. PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status)  
  3. {  
  4.     //外面输入的是串口的id,这里会改写成字符串的形式  
  5.     UNICODE_STRING name_str;  
  6.     static WCHAR name[32] = {0};  
  7.     PFILE_OBJECT fileObj = NULL;  
  8.     PDEVICE_OBJECT devObj = NULL;  
  9.   
  10.     //根据id转换成串口的名字  
  11.     memset(name,0,sizeof(WCHAR)*32);  
  12.     RtlStringCchPrintfW(  
  13.                   name,32,  
  14.                   L"\\Device\\Serial%d",id);  
  15.     RtlInitUnicodeString(&name_str,name);  
  16.   
  17.     //打开设备  
  18.     *status = IoGetDeviceObjectPointer(  
  19.                         &name_str,  
  20.                    FILE_ALL_ACCESS,  
  21.                    &fileObj,&devObj);  
  22.   
  23.     //若是打开成功了,记得必定要把文件对象解除引用  
  24.     if (*status == NT_SUCCESS)  
  25.     {  
  26.         ObDereferenceObject(fileObj);  
  27.     }  
  28.   
  29.     //返回设备对象  
  30.     return devObj;  
  31. }  

 

 

5.绑定全部端口

下面是一个简单的函数,实现了绑定本机上全部串口的功能。这个函数用到了前面提供的ccpOpenCom和ccpAttachDevice这两个函数

[cpp]  view plain  copy
 
  1. //计算机上最多只有32个串口,这里是笔者的假定  
  2. #define CCP_MAX_COM_IO 32  
  3. //保存全部过滤设备指针  
  4. static PDEVICE_OBJECT s_fltObj[CCP_MAX_COM_IO] = {0};  
  5. //保存全部真实设备指针  
  6. static PDEVICE_OBJECT s_nextObj[CCP_MAX_COM_IO] = {0};  
  7.   
  8. //这个函数绑定全部的串口  
  9. void ccpAttachAllComs(PDRIVER_OBJECT driver)  
  10. {  
  11.     ULONG i;  
  12.     PDEVICE_OBJECT com_ob;  
  13.     NTSTATUS status;  
  14.     for(i = 0 ; i < CCP_MAX_COM_IO ; i++)  
  15.     {  
  16.         //得到object引用  
  17.         com_ob = ccpOpenCom(i,&status);  
  18.         if (com_ob == NULL)  
  19.         {  
  20.             continue;  
  21.         }  
  22.         //在这里绑定,并无论绑定是否成功  
  23.         ccpAttachDevice(driver,com_ob,&s_fltObj[i],&s_nextObj[i]);  
  24.     }  
  25. }  


不必关心这个绑定是否成功,就算失败,看下一个s_fltObj便可。这个数组中不为NULL的成员表示已经绑定,为NULL的成员则是没有绑定成功或者绑定失败的。这个函数须要一个DRIVER_OBJECT的指针。

 

得到实际数据

 

1.请求的区分

Windows的内核开发者们肯定了不少数据结构,例如:驱动对象(DriverObject),设备对象(DeviceObject),文件对象(FileObject)等,须要了解的是:

(1)每一个驱动程序只有一个驱动对象

(2)每一个驱动程序能够生成若干个设备对象,这些设备对象从属于一个驱动对象

(3)若干个设备(他们能够属于不一样的驱动)依次绑定造成一个设备栈,老是最顶端的设备先接收到请求。

(4)IRP是上层设备之间传递请求的常见数据结构,但不是惟一的数据结构

 

串口设备接收到的都是IRP,所以只要对全部IRP进行过滤,就能够获得串口流过的数据。请求能够经过IRP的主功能号区分。例以下面代码:

[cpp]  view plain  copy
 
  1. PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);  
  2. if (irpsp->MajorFunction == IRP_MJ_WRITE)  
  3. {  
  4.     //若是是写....  
  5. }  
  6. else if (irpsp->MajorFunction == IRP_MJ_READ)  
  7. {  
  8.     //若是是读....  
  9. }  


 

 

2.请求的结局

对请求的过滤,最终的结局有3种:

(1)请求被经过了,过滤不作任何事情,或者简单的获取请求的一些信息。可是请求自己不受干扰。

(2)请求直接被否决了,下层驱动根本收不到这个请求。

(3)过滤完成了这个请求。

 

串口过滤要捕获两种数据:一种是发送出的数据(也就是写请求的数据),另外一种是接收的数据(也就是读请求的数据)。为了简单起见,咱们只捕获发送出去的请求。这样,只须要采起第一种处理方法便可。

这种处理最为简单。首先调用IoSkipCurrentIrpStackLocation跳到当前栈空间;而后调用IoCallDriver把这个请求发送给真实的设备。请注意:由于真实的设备已经被过滤设备绑定,因此首先接收到IRP的是过滤设备对象。代码以下:

[cpp]  view plain  copy
 
  1. //跳到当前栈空间  
  2. IoSkipCurrentIrpStackLocation(irp);  
  3. status = IoCallDriver(s_nextObj[i],irp);  


3.写请求的数据

那么,一个写请求(也就是串口一次发送的数据)保存在哪呢?IRP的结构中有三个地方能够描述缓冲区:一个是irp->MDLAddress,一个是irp->UserBuffer,一个是irp->AssociatedIrp.SystemBuffer.三种结构的具体区别参见(Windows驱动技术开发详解__派遣函数)。

回到串口的问题,那么串口的写请求究竟是用哪一种方式呢?咱们不知道,可是能够用下面方法得到:

[cpp]  view plain  copy
 
  1. PBYTE buffer = NULL;  
  2. if (IRP->MdlAddress != NULL)  
  3.    buffer = (PBYTE)MmGetSystemAddressForMdlSafe(IRP->MdlAddress);  
  4. else  
  5.    buffer = (PBYTE)IRP->UserBuffer;  
  6. if (buffer == NULL)  
  7.    buffer = (PBYTE)IRP->AssociatedIrp.SystemBuffer;  


 

完整的代码

 

1.完整的分发函数(派遣函数)

[cpp]  view plain  copy
 
  1. NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp)  
  2. {  
  3.     PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);  
  4.     NTSTATUS status;  
  5.     ULONG i,j;  
  6.   
  7.     //首先得知道发送给哪一个设备,设备一共最多CCP_MAX_COM_ID个  
  8.     //是前面的代码保存好的你都在s_fltObj中  
  9.     for (i = 0 ; i < CCP_MAX_COM_ID ; i++)  
  10.     {  
  11.         if (s_fltObj[i] == device)  
  12.         {  
  13.             //全部电源操做所有直接放过  
  14.             if (irpsp->MajorFunction == IRP_MJ_POWER)  
  15.             {  
  16.                 //直接发送,而后返回说已被处理  
  17.                 PoStartNextPowerIrp(irp);  
  18.                 IoSkipCurrentIrpStackLocation(irp);  
  19.                 return PoCallDriver(s_nextObj[i],irp);  
  20.             }  
  21.   
  22.         }  
  23.         //此外咱们只过滤写请求  
  24.         if (irpsp->MajorFunction == IRP_MJ_WRITE)  
  25.         {  
  26.             //若是是写,先得到长度  
  27.             ULONG len = irpsp->Parameters.Write.Length;  
  28.             //而后得到缓冲区  
  29.             PUCHAR buf = NULL;  
  30.             if(irp->MdlAddress != NULL)  
  31.                 buf = (PUCHAR)  
  32.                 MmGetSystemAddressForMdlSafe(irp->MdlAddress,NormalPagePriority);  
  33.             else  
  34.                 buf = (PUCHAR)irp->UserBuffer;  
  35.             if(buf == NULL)  
  36.                 buf = (PUCHAR)irp->AssociatedIrp.SystemBuffer;  
  37.   
  38.             //打印内容  
  39.             for (j = 0 ; j < len ; j++)  
  40.             {  
  41.                 DbgPrint("comcap:Send Data:%2x\r\n",buf[j]);  
  42.             }  
  43.         }  
  44.         //这些请求直接下发便可  
  45.         IoSkipCurrentIrpStackLocation(irp);  
  46.         return IoCallDriver(s_nextObj[i],irp);  
  47.   
  48.     }  
  49.     //若是根本就不在被绑定的设备中,那是有问题的,直接返回参数错误  
  50.     irp->IoStatus.Information = 0;  
  51.     irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;  
  52.     IoCompleteRequest(irp,IO_NO_INCREMENT);  
  53.     return STATUS_SUCCESS;  
  54.   
  55. }  


2.动态卸载

前面说了如何绑定,可是没说如何解除绑定。若是要把这个模块作成能够动态卸载的模块,则必须提供一个卸载函数。咱们应该在卸载函数中完成解除绑定的功能,不然,一旦卸载必定会蓝屏。

[cpp]  view plain  copy
 
  1. #define  DELAY_ONE_MICROSECOND  (-10)  
  2. #define  DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)  
  3. #define  DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)  
  4.   
  5. VOID ccpUnload(PDRIVER_OBJECT drv)  
  6. {  
  7.     ULONG i;  
  8.     LARGE_INTEGER interval;  
  9.   
  10.     //首先解除绑定  
  11.     for (i = 0 ; i < CCP_MAX_COM_ID ; i++)  
  12.     {  
  13.         if(s_nextObj[i] != NULL)  
  14.             IoDeleteDevice(s_nextObj[i]);  
  15.     }  
  16.   
  17.     //睡眠5秒,等待全部IRP处理结束  
  18.     interval.QuadPart = (5*1000 *DELAY_ONE_MICROSECOND);  
  19.     KeDelayExecutionThread(KernelMode,FALSE,&interval);  
  20.   
  21.     //删除这些设备  
  22.     for (i = 0 ; i < CCP_MAX_COM_ID ; i++)  
  23.     {  
  24.         if(s_fltObj[i] != NULL)  
  25.             IoDeleteDevice(s_fltObj[i]);  
  26.     }  
  27. }  


DriverEntry函数代码:

[cpp]  view plain  copy
 
  1. NTSTATUS DriverEntry(  
  2.     IN OUT PDRIVER_OBJECT   DriverObject,  
  3.     IN PUNICODE_STRING      RegistryPath  
  4.     )  
  5. {  
  6.     DbgPrint("Enter Driver\r\n");  
  7.     size_t i;  
  8.     //全部分发函数都设置成同样的  
  9.     for (i = 0 ; i < IRP_MJ_MAXIMUM_FUNCTION ; i++)  
  10.     {  
  11.         DriverObject->MajorFunction[i] = ccpDispatch;  
  12.     }  
  13.     //支持动态卸载  
  14.     DriverObject->DriverUnload = ccpUnload;  
  15.   
  16.     //绑定全部的串口  
  17.     ccpAttachAllComs(DriverObject);  
  18.   
  19.     return STATUS_SUCCESS;  
  20. }  


测试效果:

相关文章
相关标签/搜索