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

过滤的概念和基础

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

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

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

  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总会绑定设备栈最上层的那个设备。

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

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

  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以上的系统中才有。

  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被用于生成设备:

  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管理器负责创建这个设备对象,并返回设备对象的地址。

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

  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. status = IoCreateDevice(driver,
  12. 0,
  13. NULL,
  14. oldobj->DeviceType,
  15. 0,
  16. FALSE,
  17. fltobj);
  18. if (status != STATUS_SUCCESS)
  19. {
  20. return status;
  21. }
  22. //拷贝重要标志位
  23. if (oldobj->Flags & DO_BUFFERED_IO)
  24. {
  25. (*fltobj)->Flags |= DO_BUFFERED_IO;
  26. }
  27. if(oldobj->Flags & DO_DIRECT_IO)
  28. {
  29. (*fltobj)->Flags |= DO_DIRECT_IO;
  30. }
  31. if (oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN)
  32. {
  33. (*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN;
  34. }
  35. (*fltobj)->Flags |= DO_POWER_PAGABLE;
  36. //将一个设备绑定到另一个设备
  37. topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj);
  38. if (topdev == NULL)
  39. {
  40. //如果绑定失败了,销毁设备,返回错误
  41. IoDeleteDevice(*fltobj);
  42. *float = NULL;
  43. status = STATUS_UNSUCCESSFUL;
  44. return status;
  45. }
  46. *next = topdev;
  47. //设置这个设备已经启动
  48. (*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;
  49. return STATUS_SUCCESS;
  50. }

4.从名字获得设备对象

在知道一个设备名字的情况下,使用IoGetDeviceObjectPointer可以获得这个设备对象的指针。这个函数的原型如下:

  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中了。

  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. //根据id转换成串口的名字
  10. memset(name,0,sizeof(WCHAR)*32);
  11. RtlStringCchPrintfW(
  12. name,32,
  13. L"\\Device\\Serial%d",id);
  14. RtlInitUnicodeString(&name_str,name);
  15. //打开设备
  16. *status = IoGetDeviceObjectPointer(
  17. &name_str,
  18. FILE_ALL_ACCESS,
  19. &fileObj,&devObj);
  20. //如果打开成功了,记得一定要把文件对象解除引用
  21. if (*status == NT_SUCCESS)
  22. {
  23. ObDereferenceObject(fileObj);
  24. }
  25. //返回设备对象
  26. return devObj;
  27. }

5.绑定所有端口

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

  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. void ccpAttachAllComs(PDRIVER_OBJECT driver)
  9. {
  10. ULONG i;
  11. PDEVICE_OBJECT com_ob;
  12. NTSTATUS status;
  13. for(i = 0 ; i < CCP_MAX_COM_IO ; i++)
  14. {
  15. //获得object引用
  16. com_ob = ccpOpenCom(i,&status);
  17. if (com_ob == NULL)
  18. {
  19. continue;
  20. }
  21. //在这里绑定,并不管绑定是否成功
  22. ccpAttachDevice(driver,com_ob,&s_fltObj[i],&s_nextObj[i]);
  23. }
  24. }

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

获得实际数据

1.请求的区分

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

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

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

(3)若干个设备(他们可以属于不同的驱动)依次绑定形成一个设备栈,总是最顶端的设备先接收到请求。

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

串口设备接收到的都是IRP,因此只要对所有IRP进行过滤,就可以得到串口流过的数据。请求可以通过IRP的主功能号区分。例如下面代码:

  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的是过滤设备对象。代码如下:

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

3.写请求的数据

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

回到串口的问题,那么串口的写请求到底是用哪种方式呢?我们不知道,但是可以用下面方法获得:

  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.完整的分发函数(派遣函数)

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

2.动态卸载

前面说了如何绑定,但是没说如何解除绑定。如果要把这个模块做成可以动态卸载的模块,则必须提供一个卸载函数。我们应该在卸载函数中完成解除绑定的功能,否则,一旦卸载一定会蓝屏。

  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. VOID ccpUnload(PDRIVER_OBJECT drv)
  5. {
  6. ULONG i;
  7. LARGE_INTEGER interval;
  8. //首先解除绑定
  9. for (i = 0 ; i < CCP_MAX_COM_ID ; i++)
  10. {
  11. if(s_nextObj[i] != NULL)
  12. IoDeleteDevice(s_nextObj[i]);
  13. }
  14. //睡眠5秒,等待所有IRP处理结束
  15. interval.QuadPart = (5*1000 *DELAY_ONE_MICROSECOND);
  16. KeDelayExecutionThread(KernelMode,FALSE,&interval);
  17. //删除这些设备
  18. for (i = 0 ; i < CCP_MAX_COM_ID ; i++)
  19. {
  20. if(s_fltObj[i] != NULL)
  21. IoDeleteDevice(s_fltObj[i]);
  22. }
  23. }

DriverEntry函数代码:

  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. ccpAttachAllComs(DriverObject);
  17. return STATUS_SUCCESS;
  18. }

测试效果:

转 Windows串口过滤驱动程序的开发的更多相关文章

  1. Windows内核开发之串口过滤

    学习了几个月的内核编程,现在对Windows驱动开发又了更加深入的认识,特别是对IRP的分层处理逻辑有了深入认识. 总结起来就几句话: 当irp下来的时候,你要根据实际情况,进行处理 1> 无处 ...

  2. 《Windows内核安全与驱动开发》 7.1&7.2&7.3 串口的过滤

    <Windows内核安全与驱动开发>阅读笔记 -- 索引目录 <Windows内核安全与驱动开发> 7.1&7.2&7.3 串口的过滤 一.设备绑定的内核API ...

  3. Windows内核安全与驱动开发

    这篇是计算机中Windows Mobile/Symbian类的优质预售推荐<Windows内核安全与驱动开发>. 编辑推荐 本书适合计算机安全软件从业人员.计算机相关专业院校学生以及有一定 ...

  4. 如何构造一个简单的USB过滤驱动程序

    本文分三部分来介绍如何构造一个简单的USB过滤驱动程序,包括"基本原理"."程序的实现"."使用INF安装".此文的目的在于希望读者了解基本 ...

  5. 《windows内核安全与驱动开发》ctrl2cap中的ObReferenceObjectByName疑问

    国内有关于windows内核驱动这块的书籍实在是甚少,不过好在<windows内核安全与驱动开发>这本书还算不错(内容方面),但是不得不说这本书在许多地方存在着一些细节上的问题.比如我今天 ...

  6. 《Windows内核安全与驱动开发》阅读笔记 -- 索引目录

    <Windows内核安全与驱动开发>阅读笔记 -- 索引目录 一.内核上机指导 二.内核编程环境及其特殊性 2.1 内核编程的环境 2.2 数据类型 2.3 重要的数据结构 2.4 函数调 ...

  7. 用Windows+VirtualBox搭建嵌入式Linux开发环境

    Windows+VirtualBox的嵌入式Linux开发环境的搭建 最近一直在学习Linux的设备驱动编写,一直是在物理机上安装的Ubuntu进行的,但是在Ubuntu12.04的系统中,已经不能用 ...

  8. 《Windows内核安全与驱动开发》 2.3 重要的数据结构

    <Windows内核安全与驱动开发>阅读笔记 -- 索引目录 <Windows内核安全与驱动开发> 2.3 重要的数据结构 一.驱动对象  Windows内核采用__的编程方式 ...

  9. Windows 文件过滤驱动经验总结

    Windows 文件过滤驱动经验总结作者:sinister 本文转载自驱动开发网 看了 ChuKuangRen 的第二版<文件过滤驱动开发教程>后,颇有感触.我想,交流都是建立在平等的基础 ...

随机推荐

  1. cmake命令 安装、用法简介

    前言 cmake是kitware公司以及一些开源开发者在开发几个工具套件(VTK)的过程中所产生的衍生品.后来经过发展,最终形成体系,在2001年成为一个独立的开放源代码项目.其官方网站是www.cm ...

  2. hdu 5441

    Travel Time Limit: 1500/1000 MS (Java/Others)    Memory Limit: 131072/131072 K (Java/Others)Total Su ...

  3. poj 1328 安雷达问题 贪心算法

    题意:雷达如何放置?在xoy二维平面坐标系里面,x轴上方的为岛屿,x轴下方的是雷达要放到位置,如何放使得雷达放的最少? 思路 肯定放在x轴上减少浪费是最好的选择 什么情况下,雷达无法到达呢?--以这个 ...

  4. gpg: signing failed: secret key not available

    1 使用png签名tag时报错“ jb@39:~/11$ git tag -s gpg -m "gpg"gpg: directory `/home/jb/.gnupg' creat ...

  5. poj2195 bfs+最小权匹配

    题意:给个矩阵,矩阵里有一些人和房子(人数和房子数相等),一个人只进一个房子(可以路过房子而不进),每走一步花费1美金,求所有人都进入房子的最小花费,这是典型的二分图带权匹配问题. 这题就是建图有点麻 ...

  6. poj1523赤裸裸的割点

    这题真是没什么好说的...赤裸裸的求割点直接模板上 #include<cstdio> #include<cstring> #include<iostream> #i ...

  7. 2019 study list

    分析工具: (1)SQL   select from   where   group by having   order by   limit   运算符(算数运算符+-*/.比较运算符>< ...

  8. 网络编程-TCP/IP各层介绍(5层模型讲解)

    1.TCP/IP五层协议讲解 物理层--数据链路层--网络层--传输层--应用层 我们将应用层,表示层,会话层并作应用层,从tcp/ip五层协议的角度来阐述每层的由来与功能,搞清楚了每层的主要协议 就 ...

  9. 用asp.net+Jquery+Ajax+sqlserver编写的 (英语六级记单词)

    开始(英语对程序员的重要性引用) 出处 英语的重要性已经毋庸置疑,对于程序员来说更甚,一些最新的技术资料是英文的,如果想进入外企英语也是一个很重要的条件.对于程序员来说怎样学习好英语,在此谈一下我的一 ...

  10. apache 虚拟主机配置(根据不同的域名映射到不同网站)

    最近弄了台香港服务器做测试,Web服务器软件用的是Apache2.2,机器只有一台,ip只有一个,但是想测试几个站点,于是尝试了下Apache的虚拟主机配置.之前已经写过一篇博文了——<Apac ...