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

过滤的概念和基础

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

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

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

NTSTATUS
IoAttachDevice(
IN PDEVICE_OBJECT SourceDevice,
IN PUNICODE_STRING TargetDevice,
OUT PDEVICE_OBJECT *AttachedDevice
);

IoAttachDevice的参数如下:

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

如果一个设备被其他设备绑定了,他们在一起的一组设备,被称为设备栈。实际上,IoAttachDevice总会绑定设备栈最上层的那个设备。

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

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

 UNICODE_STRING com_name = RLT_CONSTANT_STRING(L"\\Device\\Serial0");
NTSTATUS status = IoAttachDevice(
com_filter_device,   //生成的过滤设备
&com_device_name,    //串口的设备名
&attached_device     //被绑定的设备指针返回到这里
);

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

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

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

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

3.生成过滤设备并绑定

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

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

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

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

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

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

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

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

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

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

NTSTATUS
ccpAttachDevice(
PDRIVER_OBJECT driver,
PDEVICE_OBJECT oldobj,
PDEVICE_OBJECT *fltobj,
PDEVICE_OBJECT *next)
{
NTSTATUS status;
PDEVICE_OBJECT topdev = NULL;
//生成设备然后绑定
status = IoCreateDevice(driver,
0,
NULL,
oldobj->DeviceType,
0,
FALSE,
fltobj);
if (status != STATUS_SUCCESS)
{
return status;
}
//拷贝重要标志位
if (oldobj->Flags & DO_BUFFERED_IO)
{
(*fltobj)->Flags |= DO_BUFFERED_IO;
}
if(oldobj->Flags & DO_DIRECT_IO)
{
(*fltobj)->Flags |= DO_DIRECT_IO;
}
if (oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN)
{
(*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN;
}
(*fltobj)->Flags |= DO_POWER_PAGABLE;
//将一个设备绑定到另一个设备
topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj);
if (topdev == NULL)
{
//如果绑定失败了,销毁设备,返回错误
IoDeleteDevice(*fltobj);
*float = NULL;
status = STATUS_UNSUCCESSFUL;
return status;
}
*next = topdev;
//设置这个设备已经启动
(*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}

4.从名字获得设备对象

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

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

其中ObjectName就是设备名字。

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

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

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

//打开一个端口设备
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;
//根据id转换成串口的名字
memset(name,0,sizeof(WCHAR)*32);
RtlStringCchPrintfW(
name,32,
L"\\Device\\Serial%d",id);
RtlInitUnicodeString(&name_str,name);
//打开设备
*status = IoGetDeviceObjectPointer(
&name_str,
FILE_ALL_ACCESS,
&fileObj,&devObj);
//如果打开成功了,记得一定要把文件对象解除引用
if (*status == NT_SUCCESS)
{
ObDereferenceObject(fileObj);
}
//返回设备对象
return devObj;
}

5.绑定所有端口

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

//计算机上最多只有32个串口,这里是笔者的假定
#define CCP_MAX_COM_IO 32
//保存所有过滤设备指针
static PDEVICE_OBJECT s_fltObj[CCP_MAX_COM_IO] = {0};
//保存所有真实设备指针
static PDEVICE_OBJECT s_nextObj[CCP_MAX_COM_IO] = {0};
//这个函数绑定所有的串口
void ccpAttachAllComs(PDRIVER_OBJECT driver)
{
ULONG i;
PDEVICE_OBJECT com_ob;
NTSTATUS status;
for(i = 0 ; i < CCP_MAX_COM_IO ; i++)
{
//获得object引用
com_ob = ccpOpenCom(i,&status);
if (com_ob == NULL)
{
continue;
}
//在这里绑定,并不管绑定是否成功
ccpAttachDevice(driver,com_ob,&s_fltObj[i],&s_nextObj[i]);
}
}

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

获得实际数据

1.请求的区分

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

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

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

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

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

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

PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
if (irpsp->MajorFunction == IRP_MJ_WRITE)
{
//如果是写....
}
else if (irpsp->MajorFunction == IRP_MJ_READ)
{
//如果是读....
}

2.请求的结局

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

(1)请求被通过了,过滤不做任何事情,或者简单的获取请求的一些信息。但是请求本身不受干扰。

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

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

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

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

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

3.写请求的数据

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

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

PBYTE buffer = NULL;
if (IRP->MdlAddress != NULL)
buffer = (PBYTE)MmGetSystemAddressForMdlSafe(IRP->MdlAddress);
else
buffer = (PBYTE)IRP->UserBuffer;
if (buffer == NULL)
buffer = (PBYTE)IRP->AssociatedIrp.SystemBuffer;

完整的代码

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

NTSTATUS ccpDispatch(PDEVICE_OBJECT device,PIRP irp)
{
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status;
ULONG i,j;
//首先得知道发送给哪个设备,设备一共最多CCP_MAX_COM_ID个
//是前面的代码保存好的你都在s_fltObj中
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 PoCallDriver(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++)
{
DbgPrint("comcap:Send Data:%2x\r\n",buf[j]);
}
}
//这些请求直接下发即可
IoSkipCurrentIrpStackLocation(irp);
return IoCallDriver(s_nextObj[i],irp);
}
//如果根本就不在被绑定的设备中,那是有问题的,直接返回参数错误
irp->IoStatus.Information = 0;
irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
IoCompleteRequest(irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}

2.动态卸载

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

#define  DELAY_ONE_MICROSECOND  (-10)
#define  DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define  DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)
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]);
}
//睡眠5秒,等待所有IRP处理结束
interval.QuadPart = (5*1000 *DELAY_ONE_MICROSECOND);
KeDelayExecutionThread(KernelMode,FALSE,&interval);
//删除这些设备
for (i = 0 ; i < CCP_MAX_COM_ID ; i++)
{
if(s_fltObj[i] != NULL)
IoDeleteDevice(s_fltObj[i]);
}
}

DriverEntry函数代码:

NTSTATUS DriverEntry(
IN OUT PDRIVER_OBJECT   DriverObject,
IN PUNICODE_STRING      RegistryPath
)
{
DbgPrint("Enter Driver\r\n");
size_t i;
//所有分发函数都设置成一样的
for (i = 0 ; i < IRP_MJ_MAXIMUM_FUNCTION ; i++)
{
DriverObject->MajorFunction[i] = ccpDispatch;
}
//支持动态卸载
DriverObject->DriverUnload = ccpUnload;
//绑定所有的串口
ccpAttachAllComs(DriverObject);
return STATUS_SUCCESS;
}

测试效果:

寒江独钓Windows内核安全编程__一个简单的Windows串口过滤驱动程序的开发相关推荐

  1. 《寒江独钓》内核学习笔记

    <寒江独钓>内核学习笔记(1)-- IRP - .Little Hann 时间 2013-11-30 15:40:00  博客园_.Little Hann原文  http://www.cn ...

  2. ETHREAD APC 《寒江独钓》内核学习笔记(4)

    继续学习windows 中和线程有关系的数据结构: ETHREAD.KTHREAD.TEB 1. 相关阅读材料 <windows 内核原理与实现> --- 潘爱民 2. 数据结构分析 我们 ...

  3. 《寒江独钓》内核学习笔记(1)-- IRP - .Little Hann

     原文  http://www.cnblogs.com/LittleHann/p/3450436.html 在学习内核过滤驱动的过程中,遇到了大量的涉及IRP操作的代码,这里有必要对IRP的数据结 ...

  4. THREAD APC 《寒江独钓》内核学习笔记(4)

    继续学习windows 中和线程有关系的数据结构: ETHREAD.KTHREAD.TEB 1. 相关阅读材料 <windows 内核原理与实现> --- 潘爱民 2. 数据结构分析 我们 ...

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

    在Windows系统上与安全软件相关的驱动开发过程中,"过滤(filter)"是极其重要的一个概念.过滤是在不影响上层和下层接口的情况下,在Windows系统内核中加入新的层,从而 ...

  6. Windows程序设计学习笔记(1):一个简单的windows程序

    <Windows程序设计>(第五版)(美Charles Petzold著) 1 #include<windows.h> 2 3 LRESULT CALLBACK WndProc ...

  7. 寒江独钓-Windows内核安全编程(完整版).pdf

    寒江独钓-Windows内核安全编程(完整版).pdf   编写Windows内核程序,就意味着这个程序可以执行任意指令,可以访问计算机所有的软件.硬件资源.因此,稍有不慎就有可能将系统变得不稳定.W ...

  8. 寒江独钓Windows内核编程-双机调试1

    今天总结一下关于双机调试,前面一直使用的是DDK包进行NT式与WDM式驱动入门,至今已进入使用WDK包进行编程了,DDK包早已落后我只作为入门因为大体内容变化不大.我使用的书是<寒江独钓Wind ...

  9. 驱动开发专家解读 寒江独钓 Windows内核安全编程

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 驱动开发 ...

最新文章

  1. 硬解析优化_解析!解析!598元的山灵动圈耳机大杀器,横评对比心慌慌
  2. Web 前端,易学难精,没有拿手的实战项目,怎么办?
  3. jpg 神经网络 手势识别_在STM32上跑神经网络做手势识别
  4. 终于收到HacktoberFest的奖品啦
  5. OpenGL中的二维编程——从简单的矩形开始
  6. C++之C/C++内存对齐
  7. call和apply的作用和不同
  8. Flutter 基础篇-所有知识点架构
  9. 支付:在线支付功能的概述
  10. 企业财务分析方法-杜邦分析法、沃尔评分法、Z值模型
  11. 报错Found existing installation: tensorflow 1.2.1
  12. 【小样本基础】N-way K-shot 模式和训练策略
  13. element 问号提示_点击HTML页面问号出现提示框(附源码)
  14. Mybatis + mysql获取元数据时出现问题以及解决
  15. java项目中数据查询慢问题
  16. 答疑中小白酒企业,爱码物联助力市场营销!
  17. 关于土地补偿费归谁所有
  18. 360浏览器扩展插件离线安装方法
  19. Ubuntu系统的备份与恢复
  20. 组合数的计算方法(Combinatorial Number)

热门文章

  1. Ubuntu磁盘扩展
  2. idea一键生成实体类
  3. adobe acrobat提取pdf中某个区域为单独的pdf或者eps文件
  4. JS事件与操作元素--世纪佳缘(用户名、显示隐藏内容)--黑马程序员pink老师JS第P25-操作元素总结及作业1
  5. 帝国理工学院计算机专业排名,帝国理工学院专业排名一览及最强专业推荐(上交世界排名)...
  6. Revit二次开发-资源汇总(书籍、网站、案例...)
  7. Wnt 信号通路介导胆碱通路抵御感染
  8. Niushop B2C 使用教程目录
  9. 企鹅FM音频下载器V1.0 企鹅FM下载器
  10. 淘客分佣系统(代理+群管控+分销)