控制台显示Windows串口过滤的内容
2022-11-1 18:9:21 Author: bbs.pediy.com(查看原文) 阅读量:16 收藏

控制台显示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

typedef struct tagMonitorInfo

{

    ULONG _ulComId;

    UNICODE_STRING32 _deviceName;

}TagMonitorInfo, *PTagMonitorInfo;

包括要过滤的串口号和对应的设备名(是用户态通过注册表查找出来的),因为要考虑到驱动运行在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

typedef struct tagFilterDevice

{

    ULONG _comId;   

    PDEVICE_OBJECT _topDev;

}TagFilterDevice, *PTagFilterDevice;

调用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

typedef enum eSerialAction

{

    eComOpen,

    eComClose,

    eComWrite,

    eComReceive

}ESerialAction;

typedef struct serialActionInfo

{

    ULONG _ulComId;

    ULONG _ProcessId;

    ESerialAction _eAction;

    ULONG _infoLength; //如果是读写  此属性表示buffer中数据的长度

    union

    {

        LONG     _user_buffer;

        LONGLONG _kernel_buffer;

    }_buffer;

}SerialActionInfo, *PSerialActionInfo;

如果是打开关闭操作,那么不需要操作_buffer,如果是读写串口,用户态地址保存在_buffer._user_buffer,内核态地址保存在_buffer._kernel_buffer,用户态程序和驱动程序共用这个结构体,因为用户态程序使用的是32位的,而驱动程序不管是32位还是64位,使用此结构体都可以满足需求,链表数据使用如下结构体表示:

1

2

3

4

5

6

typedef struct mySerialAction

{

    SerialActionInfo _serialAction;

    LIST_ENTRY _listEntry;

}MySerialAction, *PMySerialAction;

LIST_ENTRY gloListHead = {0};

打开、关闭、写都比较简单,将信息保存在链表中,并转发下层驱动即可,如下:

1

2

3

4

PTagFilterDevice pDeviceExtension = device->DeviceExtension;

PDEVICE_OBJECT topdev = pDeviceExtension->_topDev;

IoSkipCurrentIrpStackLocation(irp);

return IoCallDriver(topdev, irp);

读串口信息需要交给下层驱动读完串口之后,才能得到读到的内容

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

if (irpsp->MajorFunction == IRP_MJ_READ)

{               

    DbgPrint("filterDevice IRP_MJ_READ default\r\n");

    //控制某个串口 拷贝irp 交由下一层驱动去读取完成后 再获取内容

    KEVENT completeEvent = { 0 };

    KeInitializeEvent(&completeEvent, SynchronizationEvent, FALSE);

    IoCopyCurrentIrpStackLocationToNext(irp);

    IoSetCompletionRoutine(irp, completeRead, &completeEvent, TRUE, TRUE, TRUE);

    NTSTATUS nextStatus = IoCallDriver(topdev, irp);               

    KeWaitForSingleObject(&completeEvent, Executive, KernelMode, TRUE, 0);

    if (NT_SUCCESS(nextStatus))

    {                                       

        //此处插入读到的内容 参见源码             

    }               

    return nextStatus;

}

上面代码等待下层驱动完成IRP,调用我们传入的completeEvent函数,在此函数中触发IoSetCompletionRoutine传入的第三个参数,为KEVENT类型:

1

2

3

4

5

6

NTSTATUS completeRead(_In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp, _In_reads_opt_(_Inexpressible_("varies")) PVOID Context)

{

    PKEVENT pcompleteEvent = Context;   

    KeSetEvent(pcompleteEvent, 0, FALSE);

    return STATUS_SUCCESS;

}

这样KeWaitForSingleObject函数就可以返回了,处理读到的串口内容。

4、接下来处理控制设备的请求:

将串口的各项动作信息保存到链表后,还需要处理控制设备的读请求IRP_MJ_READ,因为我是通过在用户态调用ReadFile传入控制设备句柄来读过滤到的串口动作信息,在IRP_MJ_READ中判断链表是否为空(记得使用锁),为空则等待

1

2

3

4

5

6

7

//读取链表是否为空

if (listInfoIsEmpty())

{

    status = KeWaitForSingleObject(&gloWaitEvent, Executive, KernelMode, TRUE, 0);                   

    //如果关闭用户态程序 那么等待也会返回  继续返回到返回到用户态之前KiServiceExit函数 检查到有需要执行的apc nt!KiDeliverApc 执行apc 关闭句柄退出

    //如果关闭程序 会返回STATUS_ALERTED 0n257  0x101

}

等待到数据后,获取信息,如果_infoLength不为0,那么进行一些判断,比如用户态缓冲区是否足够等,之后拷贝内存:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

//判断用户态缓冲区是否合法

__try

{            

    ProbeForWrite((void*)infoUser->_buffer._user_buffer, infoUser->_infoLength, 1);

    //拷贝内存

    memcpy_s((void*)infoUser->_buffer._user_buffer, infoUser->_infoLength, (void*)listInfo->_buffer._kernel_buffer, listInfo->_infoLength);                  

    infoUser->_infoLength = listInfo->_infoLength;

}

__except (EXCEPTION_EXECUTE_HANDLER)

{

    KdBreakPoint();

    status = STATUS_INVALID_USER_BUFFER;

    __leave;

}

上面代码中先对用户态传入的缓冲区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请求导致蓝屏,应该怎么测试复现这个问题?

看雪招聘平台创建简历并且简历完整度达到90%及以上可获得500看雪币~

上传的附件:
  • ControlSerialPort.cpp (7.13kb,5次下载)
  • MyFilterSerialPort.c (21.85kb,5次下载)

文章来源: https://bbs.pediy.com/thread-274956.htm
如有侵权请联系:admin#unsafe.sh