欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]windows内核编程之[串口过滤]

[原创]windows内核编程之[串口过滤]

嗯,昨天发过一篇有关【应用-内核通信】的内核基础贴:http://bbs.pediy.com/showthread.php?t=212683 请大家多多提意见。
  今天的内容是串口过滤,都是很基础的,但我会认真写的。
  过滤概念:
  过滤,顾名思义,水流从A点流向B点,当我们想要去掉水中的泥沙,却又想不影响水的流向时,就可以在A,B两点间插入一个过滤网C。串口过滤其实也和这差不多,我们可以在串口设备的外面加一层过滤网,这样就保证了总是我们的程序率先收到应用发向串口的信息。如果是写串口操作,还可以截获写往串口的数据。当我们的“过滤网”对截获的信息操作完毕之后,为了不影响内核正常工作,还需要将截获的信息发出去(至于怎么发就是你们自己的事了)
  技术原理:
  上一篇帖子介绍了如何生成驱动程序自己的设备对象,那么现在我们可以用自己生成的“虚拟设备对象”来绑定到“串口设备对象”上。这样绑定过后,原本发给串口的信息,都会先发到我们自己的设备对象上。
  用于绑定的内核API很多,这里我使用的是IoAttachDeviceToDeviceStack,与这个相似的API还有一个:IoAttachDeviceToDeviceStackSafe,只不过有兼容性问题。贴出IoAttachDeviceToDeviceStack原型:


PDEVICE_OBJECT IoAttachDeviceToDeviceStack(

  _In_ PDEVICE_OBJECT SourceDevice,

  _In_ PDEVICE_OBJECT TargetDevice

);


  第一个参数是我们生成的过滤设备,第二个参数是要被绑定的设备,我们将设备绑定之后,就可以收到对串口的IRP请求了。那么如何获得需要绑定的设备对象呢?可以使用IoGetDeviceObjectPointer函数来获取设备对象。原型:

NTSTATUS IoGetDeviceObjectPointer(

  _In_  PUNICODE_STRING ObjectName,

  _In_  ACCESS_MASK     DesiredAccess,

  _Out_ PFILE_OBJECT    *FileObject,

  _Out_ PDEVICE_OBJECT  *DeviceObject

);


  这个API可以通过设备名称获取设备对象,由于在windows中,串口是有固定名字的,所以能很方便的获取设备对象。第一个参数是设备名称,第二个是访问权限,第三个参数用于返回文件对象(在这里我们不用管他,获得文件对象之后直接解除引用就好了),第四个参数用于返回设备对象。得到设备对象,就可以和自己生成的过滤设备进行绑定了。
  绑定之后,对消息处理完毕,我们需要把消息发往原始设备,使用以下API来完成此功能:
IoSkipCurrentIrpStackLocation
IoCallDriver
  这里就不具体讲解这两个API了。
  在卸载函数里,我们需要把绑定的设备进行解绑,然后删除掉。使用以下两个API:

VOID IoDetachDevice(

  _Inout_ PDEVICE_OBJECT TargetDevice

);

VOID IoDeleteDevice(

  _In_ PDEVICE_OBJECT DeviceObject

);


  以上两个函数,所需要提供的参数都是我们生成的过滤设备。
  至此,技术原理就讲解完毕。
  简单的串口过滤驱动设计思路:
  首先从第一个问题开始:获得设备对象并绑定。
  我们并不知道计算机上究竟有多少个串口,只能一个个的去尝试打开,判断是否成功,如果成功,就绑定

for (i = 0; i < 32; i++)

  {

    //首先将打开的端口保存到临时变量中

    KS_Ports = KS_OpenPort(i, &status);

                //判定是否打开成功

    if (KS_Ports == NULL)

      continue;

    //然后绑定

    KS_deviceAttach(KS_driver, KS_Ports, &KS_MyPorts[i], &KS_OtherPorts[i]);

  }


  正如大家所见,我这里粗略的指定了一个值:32,也就是说最多打开32个串口,最后调用的那个函数KS_deviceAttach是我自己编写的一个绑定函数。我申请了两个数组全局变量,分别是KS_MyPorts与KS_OtherPorts,前者用于保存程序自己的过滤设备指针,后者用于保存原始设备的指针。在KS_deviceAttach中,我们传入当前设备对象KS_driver,和我们打开的端口设备对象 KS_Ports。最后他会把绑定的过滤设备指针返回到KS_MyPorts[i]中,把原始设备返回到KS_OtherPorts[i]中。
  函数 KS_OpenPort是我编写的一个打开串口设备函数。传递一个整数i,表示尝试打开第几个串口,结果返回到status中。下面贴出KS_OpenPort代码:

//打开串口设备函数

PDEVICE_OBJECT KS_OpenPort(ULONG id, NTSTATUS *status)

{

  WCHAR Port_Name[32] = { 0 };

  UNICODE_STRING KS_Name;

  PFILE_OBJECT KS_FileObj = NULL;

  PDEVICE_OBJECT KS_Dev = NULL;

  RtlStringCchPrintfW(Port_Name, 32, L"\\Device\\Serial%d", id);

  RtlInitUnicodeString(&KS_Name, Port_Name);

  *status = IoGetDeviceObjectPointer(&KS_Name, FILE_ALL_ACCESS, &KS_FileObj, &KS_Dev);

  if (*status == STATUS_SUCCESS)

    ObDereferenceObject(KS_FileObj);

  return KS_Dev;

}


  如上所示:RtlStringCchPrintfW把串口号转换成设备名称,最后打开。
  绑定完毕之后,设置了分发函数,就可以截获信息了。
  在这里我们只截获对串口的写请求,其他的一律直接发送给下层设备。
  设计思路讲完了,下面贴出完整代码。

#include <ntddk.h>

#include <ntstrsafe.h>

//保存过滤设备指针

PDEVICE_OBJECT KS_MyPorts[32];

//保存原始设备指针

PDEVICE_OBJECT KS_OtherPorts[32];

//卸载函数,这里需要把绑定的设备都解绑,然后删除

VOID KS_dirverUnload(PDRIVER_OBJECT KS_driver)

{

  ULONG i;

  for (i = 0; i < 32; i++)

  {

    if (KS_MyPorts[i] != NULL)

      IoDetachDevice(KS_MyPorts[i]);

  }

  //这里我们直接删除设备,但这是不安全的,

  //因为有可能还会有未完成的irp

  for (i = 0; i < 32; i++)

  {

    if (KS_MyPorts[i] != NULL)

      IoDeleteDevice(KS_MyPorts[i]);

  }

  DbgPrint("My driver is unloading...");

}

//生成一个设备并绑定KS_Lobj指向的设备,

//KS_Nobj用于返回过滤设备指针,next用于返回原始设备指针

NTSTATUS KS_deviceAttach(

  PDRIVER_OBJECT driver,

  PDEVICE_OBJECT KS_Lobj,

  PDEVICE_OBJECT *KS_Nobj,

  PDEVICE_OBJECT *next)

{

  NTSTATUS status;

  PDEVICE_OBJECT KS_newDev;

  //生成设备

  status = IoCreateDevice(driver, 0, NULL, KS_Lobj->DeviceType, 0, FALSE, KS_Nobj);

  if (!NT_SUCCESS(status))

  {

    DbgPrint("Create device failed!\r\n");

    status = STATUS_UNSUCCESSFUL;

    return status;

  }

  if (KS_Lobj->Flags&DO_BUFFERED_IO)

    (*KS_Nobj)->Flags |= DO_BUFFERED_IO;

  if (KS_Lobj->Flags&DO_DIRECT_IO)

    (*KS_Nobj)->Flags |= DO_DIRECT_IO;

  if (KS_Lobj->Flags&FILE_DEVICE_SECURE_OPEN)

    (*KS_Nobj)->Flags |= FILE_DEVICE_SECURE_OPEN;

  (*KS_Nobj)->Flags |= DO_POWER_PAGABLE;

  //绑定设备

  KS_newDev = IoAttachDeviceToDeviceStack(*KS_Nobj, KS_Lobj);

  if (KS_newDev == NULL)

  {

    IoDeleteDevice(*KS_Nobj);

    *KS_Nobj = NULL;

    status = STATUS_UNSUCCESSFUL;

    return status;

  }

  *next = KS_newDev;

  //设备启动

  (*KS_Nobj)->Flags = (*KS_Nobj)->Flags & ~DO_DEVICE_INITIALIZING;

  return STATUS_SUCCESS;

}

//打开串口设备函数

PDEVICE_OBJECT KS_OpenPort(ULONG id, NTSTATUS *status)

{

  WCHAR Port_Name[32] = { 0 };

  UNICODE_STRING KS_Name;

  PFILE_OBJECT KS_FileObj = NULL;

  PDEVICE_OBJECT KS_Dev = NULL;

  RtlStringCchPrintfW(Port_Name, 32, L"\\Device\\Serial%d", id);

  RtlInitUnicodeString(&KS_Name, Port_Name);

  *status = IoGetDeviceObjectPointer(&KS_Name, FILE_ALL_ACCESS, &KS_FileObj, &KS_Dev);

  if (*status == STATUS_SUCCESS)

    ObDereferenceObject(KS_FileObj);

  return KS_Dev;

}

//分发函数,当串口有IRP请求时会被此函数截获。

NTSTATUS KS_Dispatch(PDEVICE_OBJECT dev, PIRP irp)

{

  PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(irp);

  NTSTATUS status;

  ULONG i, j;

  //遍历绑定的所有串口

  //判断究竟是发给哪个串口的

  for (i = 0; i < 32; i++)

  {

    //电源操作直接发送

    if (IrpStack->MajorFunction == IRP_MJ_POWER)

    {

      PoStartNextPowerIrp(irp);

      IoSkipCurrentIrpStackLocation(irp);

      return PoCallDriver(KS_OtherPorts[i], irp);

    }

    //过滤写请求

    if (IrpStack->MajorFunction == IRP_MJ_WRITE)

    {

      //在这里就可以对数据进行操作,这里我们

      //仅仅是打印出数据然后原封不动的发给下一层

      ULONG len = IrpStack->Parameters.Write.Length;

      PUCHAR buffer = NULL;

      //因为不知道用的什么储存方式,所以需要进行这样的判断

      if (irp->MdlAddress != NULL)

        buffer = (PUCHAR)MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);

      else

        buffer = (PUCHAR)irp->UserBuffer;

      if (buffer == NULL)

        buffer = (PUCHAR)irp->AssociatedIrp.SystemBuffer;

      for (j = 0; j < len; j++)

      {

        DbgPrint("The data:%2x\r\n", buffer[j]);

      }

      //当我们截获数据后,直接下发这些操作就可以了

      IoSkipCurrentIrpStackLocation(irp);

      return IoCallDriver(KS_MyPorts[i], irp);

    }

  }

  //如果这段代码被执行,表示没有找到设备,这里我们粗略的返回错误

  irp->IoStatus.Information = 0;

  irp->IoStatus.Status = STATUS_INVALID_PARAMETER;

  IoCompleteRequest(irp, IO_NO_INCREMENT);

  return STATUS_SUCCESS;

}

NTSTATUS DriverEntry(PDRIVER_OBJECT KS_driver, PUNICODE_STRING KS_UNS)

{

  ULONG i;

  NTSTATUS status;

  PDEVICE_OBJECT KS_Ports;

  //设置分发函数

  for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)

  {

    KS_driver->MajorFunction[i] = KS_Dispatch;

  }

  //设置卸载函数

  KS_driver->DriverUnload = KS_dirverUnload;

  

  //绑定所有串口

  for (i = 0; i < 32; i++)

  {

    //首先将打开的端口保存到临时变量中

    KS_Ports = KS_OpenPort(i, &status);

    if (KS_Ports == NULL)

      continue;

    //然后绑定

    KS_deviceAttach(KS_driver, KS_Ports, &KS_MyPorts[i], &KS_OtherPorts[i]);

  }

  return STATUS_SUCCESS;

}


  以上就是一个简单的串口过滤全部代码,对于截获的写操作数据,我们仅仅是简单的把他用dbgprint打印出来,然后不做任何修改的发送到下一层设备。
  全文除函数原型是从MSDN文档复制之外,其余的都是纯手打,难免有错,还请多多指教。
  
  最后,欢迎大家加我qq37794802,一起讨论学习内核编程

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功