控制台显示Windows串口过滤的内容
2天前 1366
记录自己学习《Windows 内核驱动》章节中的串口过滤驱动。
先看下最终效果吧
图中并没有展示对串口1的读写监控,但是已经绑定成功了。
程序分为驱动程序和用户态程序ControlSerialPort.exe两部分
用户态程序主要有两个功能:
1、使用-L参数会遍历注册表枚举串口。
2、以管理员权限使用-C参数来监控过滤串口,形式为ControlSerialPort.exe -C COM1 COM2 可以监控多个串口,也可以只监控一个串口,程序会根据串口号得到注册表Hardware\DeviceMap\SerialComm下的串口信息:设备名称如\Device\Serial0,使用DeviceIoControl将设备名称传入驱动程序中进行绑定。绑定需要绑定的串口后,如果存在串口可以绑定成功,那么会调用ReadFile读取过滤到的串口信息,包括打开串口、发送信息、收到信息、关闭串口这四种类型的消息。
需要注意的是用户态程序是32位的,而驱动程序是32位或者64位的,所以有的数据结构需要进行调整。
驱动程序使用WDM框架编写,下面介绍下驱动程序的功能逻辑:
1、在DriverEntry函数中创建一个控制设备,使用IoCreateDeviceSecure(WdmlibIoCreateDeviceSecure)函数,经过测试IoCreateDevice创建的设备,传入参数FILE_DEVICE_SECURE_OPEN或者传入0时,普通用户也可以打开此设备创建的链接,这样是不安全的,所以使用IoCreateDeviceSecure函数,使用的sddl为L"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GR;;;WD)",并且设置独占性为TRUE,只允许用户态控制程序打开一次此设备。
2、在驱动的回调函数中处理控制设备的IRP_MJ_DEVICE_CONTROL消息,使用DeviceIoControl函数时,传入参数为如下类型:
1 2 3 4 5 |
|
包括要过滤的串口号和对应的设备名(是用户态通过注册表查找出来的),因为要考虑到驱动运行在64位,为了数据结构的统一就使用UNICODE_STRING32来表示需要绑定的串口设备的名称。调用自己写的genFilterAndBind函数传入PUNICODE_STRING32和ULONG,在此函数中先调用IoGetDeviceObjectPointer函数获取需要绑定串口的指针,注意此函数的参数1类型为PUNICODE_STRING,所以此时要对genFilterAndBind传入的PUNICODE_STRING32数据赋值指针(Buffer)和长度(Length、MaximumLength)到一份临时变量PUNICODE_STRING中,使用临时变量获取设备指针,调用IoCreateDevice创建一个过滤设备,调用时传入DeviceExtensionSize不为0,用来保存串口ID、以及之后的过滤设备之下的设备指针,用于将请求交给下层,接着设置此过滤设备的属性Flags,调用IoAttachDeviceToDeviceStack将此过滤设备绑定到刚才得到的串口设备对象上,这样就过滤成功了。
调用IoCreateDevice时传入sizeof(TagFilterDevice)大小,使用设备的DeviceExtension指针存放如下结构体:
1 2 3 4 5 |
|
调用IoAttachDeviceToDeviceStack将过滤设备绑定到硬件设备上时,可以将硬件设备想象成一个栈,其中最底层的可能就是硬件设备,最上层的是其它设备,将此过滤设备添加到最顶层后,此函数的返回值是原来的是顶层设备,使用_topDev来保存。
需要注意的是在调用IoGetDeviceObjectPointer函数时除了设备指针,还获取到一个文件对象fileobj,需要调用ObDereferenceObject减少文件对象的引用计数,如果在过滤设备已经绑定到了硬件设备之上后调用此函数,那么会触发过滤设备的IRP_MJ_CLOSE例程。在绑定之前调用ObDereferenceObject不会触发过滤设备的IRP_MJ_CLOSE例程。
3、接下来在分发例程中处理IRP。来到此函数的IRP设备分为两种,一个是在DriverEntry中申请的设备,称之为控制设备,另一个是自己生成的过滤设备,控制设备只有一个,过滤设备可能有多个。
判断是过滤设备:就处理IRP_MJ_CREATE、IRP_MJ_CLEANUP、IRP_MJ_WRITE 、IRP_MJ_READ对应的IRP,将内容插入到一个链表中,只有当handlecount为0即所有用户态句柄都关闭了才会触发IRP_MJ_CLEANUP,当pointcount为0时会触发IRP_MJ_CLOSE。链表操作时用锁来保护链表操作。打开、关闭、写串口、读串口数据保存在如下结构中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
如果是打开关闭操作,那么不需要操作_buffer,如果是读写串口,用户态地址保存在_buffer._user_buffer,内核态地址保存在_buffer._kernel_buffer,用户态程序和驱动程序共用这个结构体,因为用户态程序使用的是32位的,而驱动程序不管是32位还是64位,使用此结构体都可以满足需求,链表数据使用如下结构体表示:
1 2 3 4 5 6 |
|
打开、关闭、写都比较简单,将信息保存在链表中,并转发下层驱动即可,如下:
1 2 3 4 |
|
读串口信息需要交给下层驱动读完串口之后,才能得到读到的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
上面代码等待下层驱动完成IRP,调用我们传入的completeEvent函数,在此函数中触发IoSetCompletionRoutine传入的第三个参数,为KEVENT类型:
1 2 3 4 5 6 |
|
这样KeWaitForSingleObject函数就可以返回了,处理读到的串口内容。
4、接下来处理控制设备的请求:
将串口的各项动作信息保存到链表后,还需要处理控制设备的读请求IRP_MJ_READ,因为我是通过在用户态调用ReadFile传入控制设备句柄来读过滤到的串口动作信息,在IRP_MJ_READ中判断链表是否为空(记得使用锁),为空则等待
1 2 3 4 5 6 7 |
|
等待到数据后,获取信息,如果_infoLength不为0,那么进行一些判断,比如用户态缓冲区是否足够等,之后拷贝内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
上面代码中先对用户态传入的缓冲区infoUser->_buffer._user_buffer调用ProbeForWrite检查缓冲区是否可写,调用memcpy_s时可以看到第一个参数和第三个参数分别表示_user_buffer和_kernel_buffer,如果是32位驱动,那么这两个数据都是32位的,ProbeForWrite函数可能会触发异常,使用异常处理机制进行处理。
5、如果是控制设备的IRP_MJ_CLEANUP请求,那么代表用户态控制程序调用CloseHandle关闭了控制设备的句柄,handlecount为0。在这个请求中通过驱动对象找到DeviceObject,再循环遍历NextDevice,如果不是控制设备,那就属于过滤设备,此时因为用户态程序已经不需要过滤串口了,那么就调用IoDetachDevice传入_topDev解除串口绑定,然后删除过滤设备。接着循环遍历链表,看看是否还有串口数据在链表中,调用ExFreePool释放PMySerialAction,还有如果是读写串口类型的数据,那么还需要释放_buffer。
控制设备的IRP_MJ_DEVICE_CONTROL请求处理前面已经有说过了。
驱动程序需要调用IoCreateDeviceSecure(WdmlibIoCreateDeviceSecure),所以需要在项目->属性->配置属性->链接器->输入中附加依赖项链接Wdmsec.lib。
用户态程序代码此处就不做介绍了,比较简单,参见附件源码即可。
问题:假如在处理串口信息的过程中,用户关闭了用户态控制端程序,那么会触发控制设备的IRP_MJ_CLEANUP请求,此时会解绑设备,接着删除设备,此时是否会影响到正在读串口或者正在写串口的IRP请求导致蓝屏,应该怎么测试复现这个问题?