首页 > 代码库 > 寒江独钓Windows内核编程——串口过滤
寒江独钓Windows内核编程——串口过滤
一、过滤的概念:
过滤是在不影响上层和下层接口的情况下,在Windows系统内核中加入新的层,从而不需要修改上层的软件或者下层的真是驱动程序,就加入了新的功能。
1.1 设备绑定的内核API
进行过滤的最主要的方法是对一个设备对象(Device Object)进行绑定。通过编程可以生成一个虚拟设备对象,并“绑定”(Attach)在一个真实的设备上。一旦绑定,则本来操作系统发送给真实设备的请求,就会首先发送到这个虚拟设备。
在WDK中,有多个内核API能实现绑定的功能。以下三个绑定API是从WDK帮助文档中获得的,需要进一步了解可以查看帮助文档。
(1)IoAttachDevice
The IoAttachDevice routine attaches the caller‘s device object to a named target device object, so that I/O requests bound for the target device are routed first to the caller.
NTSTATUS IoAttachDevice( IN PDEVICE_OBJECT SourceDevice, IN PUNICODE_STRING TargetDevice, OUT PDEVICE_OBJECT *AttachedDevice );
Parameters
- SourceDevice
- Pointer to the caller-created device object.
- TargetDevice
- Pointer to a buffer containing the name of the device object to which the specified SourceDevice is to be attached.
- AttachedDevice
- Pointer to caller-allocated storage for a pointer. On return, contains a pointer to the target device object if the attachment succeeds.
- (2)IoAttachDeviceToDeviceStack
- The IoAttachDeviceToDeviceStack routine attaches the caller‘s device object to the highest device object in the chain and returns a pointer to the previously highest device object.
PDEVICE_OBJECT IoAttachDeviceToDeviceStack( IN PDEVICE_OBJECT SourceDevice, IN PDEVICE_OBJECT TargetDevice );
Parameters
- SourceDevice
- Pointer to the caller-created device object.
- TargetDevice
- Pointer to another driver‘s device object, such as a pointer returned by a preceding call to IoGetDeviceObjectPointer.
Return Value
IoAttachDeviceToDeviceStack returns a pointer to the device object to which the SourceDevice was attached. The returned device object pointer can differ from TargetDevice if TargetDevice had additional drivers layered on top of it.
IoAttachDeviceToDeviceStack returns NULL if it could not attach the device object because, for example, the target device was being unloaded.
(3)IoAttachDeviceToDeviceStackSafe
The IoAttachDeviceToDeviceStackSafe routine attaches the caller‘s device object to the topmost device object in a driver stack.
NTSTATUS IoAttachDeviceToDeviceStackSafe( IN PDEVICE_OBJECT SourceDevice, IN PDEVICE_OBJECT TargetDevice, IN OUT PDEVICE_OBJECT *AttachedToDeviceObject );
Parameters
- SourceDevice
- Pointer to a caller-created device object.
- TargetDevice
- Pointer to the device object in the stack to which the SourceDevice object is to be attached.
- AttachedToDeviceObject
- On input, this parameter specifies the address of SourceDevice->DeviceExtension->AttachedToDeviceObject, which must contain a NULL pointer. On output, this parameter receives a pointer to the device object to which the SourceDevice was attached.
Return Value
IoAttachDeviceToDeviceStackSafe returns STATUS_SUCCESS if SourceDevice is successfully attached above the TargetDevice; otherwise it returns STATUS_NO_SUCH_DEVICE.
1.2 生成过滤设备并绑定
在绑定一个设备之前,先要知道如何生成一个用于过滤的过滤设备。函数IoCreateDevice被用于生成设备。函数的参数可以占时根据源码中的进行设置,需要进一步了解可以查看WDK帮助文档。此外还需要注意的就是,在绑定一个设备之前,应该把这个设备对象的多个子域设置成和要绑定的目标对象一致,包括标志和特征。实现代码参见下文源码中的ccpAttachDevice函数。
IoCreateDevice 定义,来自WDK帮助文档:
Call IoCreateDevice to create a filter device object to attach to a volume or file system stack. In the FileSpy sample, this is done as follows:
status = IoCreateDevice(
gFileSpyDriverObject, //DriverObject
sizeof(FILESPY_DEVICE_EXTENSION), //DeviceExtensionSize
NULL, //DeviceName
DeviceObject->DeviceType, //DeviceType
0, //DeviceCharacteristics
FALSE, //Exclusive
&newDeviceObject); //DeviceObject
In the above code snippet, DeviceObject is a pointer to the target device object to which the filter device object will be attached; newDeviceObject is a pointer to the filter device object itself.
Setting the DeviceExtensionSize parameter to sizeof(FILESPY_DEVICE_EXTENSION) causes a FILESPY_DEVICE_EXTENSION structure to be allocated for the filter device object. The newly created filter device object‘s DeviceExtension member is set to point to this structure. File system filter drivers usually define and allocate memory for a device extension for each filter device object. The structure and contents of the device extension are driver-specific. However, on Microsoft Windows XP and later, filter drivers should define a DEVICE_EXTENSION structure for filter driver objects that contains at least the following member:
PDEVICE_OBJECT AttachedToDeviceObject;
In the above call to IoCreateDevice, setting the DeviceName parameter to NULL specifies that the filter device object will not be named. Filter device objects are never named. Because a filter device object is attached to a file system or volume driver stack, assigning a name to the filter device object would create a system security hole.
The DeviceType parameter must always be set to the same device type as that of the target (file system or filter) device object to which the filter device object is being attached. It is important to propagate the device type in this way, because it is used by the I/O Manager and can be reported back to applications.
Note File systems and file system filter drivers should never set the DeviceType parameter to FILE_DEVICE_FILE_SYSTEM. This is not a valid value for this parameter. (The FILE_DEVICE_FILE_SYSTEM constant is intended only for use in defining FSCTL codes.)
Another reason why the DeviceType parameter is important is that many filters attach only to certain types of file systems. For example, a particular filter may attach to all local disk file systems, but not to CD-ROM file systems or remote file systems. Such filters determine the type of file system by examining the device type of the topmost device object in the file system or volume driver stack. In most cases, the topmost device object in the stack is a filter device object. Thus it is essential that all attached filter device objects have the same device type as that of the underlying file system or volume device object.
1.3 从名字获得设备对象
在知道一个设备名字的情况下,使用函数IoGetDeviceObjectPointer可以获得这个设备对象的指针。必须注意的是:在使用这个函数之后必须把这个文件对象“解除引用”,否则会引起内存泄露(请注意实现的代码)。运用此函数实现下文源码中的ccpOpenCom函数用于打开一个端口设备。
IoGetDeviceObjectPointer:
The IoGetDeviceObjectPointer routine returns a pointer to the top object in the named device object‘s stack and a pointer to the corresponding file object, if the requested access to the objects can be granted.
NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT *FileObject,
OUT PDEVICE_OBJECT *DeviceObject
);
Parameters
- ObjectName
- Pointer to a buffer that contains a Unicode string that is the name of the device object.
- DesiredAccess
- Specifies the ACCESS_MASK value that represents the desired access. Usually DesiredAccess is FILE_READ_DATA. Infrequently, the FILE_WRITE_DATA, or FILE_ALL_ACCESS access rights are specified.
- FileObject
- Pointer to the file object that represents the corresponding device object to user-mode code if the call is successful.
- DeviceObject
- Pointer to the device object that represents the named logical, virtual, or physical device if the call is successful.
- 1.4 绑定所有串口 实现一个函数,调用上文提供的ccpOpenCom和ccpAttachDevice这两个函数。其中可以假定计算机中拥有的最大串口数为32。
- 二、获得实际数据
- 这一章我们一直都在开发一个可以捕获串口上数据的过滤程序。现在虚拟设备已经绑定了真正的串口设备,那么,实际上如何从虚拟设备得到串口设备上流过的数据呢?答案是根据“请求”。操作系统将请求发送给串口设备,请求中就含有要发送的数据,请求的回答中则含有要接收的数据。下面分析这些“请求”,以便得到实际的串口数据流。
- 2.1 请求的区分
- Windows的内核开发者们确定了很多的数据结构,有DEVICE_OBJECT(设备对象)、FILE_OBJECT(文件对象)和DRIVER_OBJECT(驱动对象)。需要注意的是:
- (1)每个驱动程序只有一个驱动对象。
- (2)每个驱动程序可以生成若干个设备对象,这些设备对象从属于一个驱动对象。
- (3)若干个设备(它们从属于不同的驱动)依次绑定形成一个设备栈,总是最顶端的设备先接收到请求。
- 请注意:IRP是上层设备之间传递请求的常见数据结构,但是绝对不是唯一的数据结构。传递请求还有很多其他的方法,不同的设备也可能使用不同的结构来传递请求。但是在书中,90%的情况下,请求与IRP是等价的概念。
- 串口设备接收到的请求都是IRP,因此只要对所有的IRP进行过滤,就可以得到串口上流过的所有数据。串口过滤时只需要关心两种请求:读请求和写请求。对串口而言,读指接收数据,而写指发送数据。串口也还有其他的请求,比如打开或关闭、设置波特率等。但是我们的目标是获得串口上流过的数据,而不是关心打开关闭和波特率是多少这样的问题,这里可一概无视。
- 请求可以通过IRP的主功能号进行区分。IRP的主功能号是保存在IRP栈空间中的一个字节,用来标识这个IRP的功能大类。相应的,还有一个次功能号来标识这个IRP的功能细分小类。
- 读请求的功能号为IRP_MJ_READ,而写请求的功能号为IRP_MJ_WRITE。
- 2.2 请求的结局
- 对请求的过滤,最终结局有3种:
- (1)请求被允许通过了。过滤不做任何事情,或者简单的获取请求的一些信息。但是请求本身不受干扰,这样系统行为就不会有变化,皆大欢喜。
- (2)请求直接被否决了。过滤禁止这个请求通过,这个请求被返回了错误,下层驱动程序根本收不到这个请求。这样系统行为就变了,后果是常常看见上层应用程序弹出错误框提示权限错误或者读取文件失败之类信息。
- (3)过滤完成了这个请求。有时候有这样的需求,比如一个读请求,我们想记录读到了什么。如果读请求还没有完成,那么如何知道到底会读到什么呢?只有让这个请求先完成再去记录。过滤完成这个请求时不一定要原封不动地完成,这个请求的参数可以被修改(比如把数据加密一番)。
- 当过滤了一个请求时,就必须把这个请求按照上面3种方法之一进行处理。当然这些代码会写在一个处理函数中。这里先介绍这些处理方法的代码应该怎么写。
- 串口过滤要捕获两种数据:一种是发送出的数据(也就是写请求中的数据),另一种是接收的数据(也就是读请求的数据)。为了简单起见,我们只捕获发送出的数据,这样,只需要采取第1种处理方法即可。至于第2、3两种处理方法,读者会在后面的许多过滤程序中碰到。
- 这种处理最简单。首先调用IoSkipCurrentIrpStackLocation跳过当前栈空间;然后调用IoCallDriver把这个请求发送给真实的设备。请注意:因为真实的设备已经被过滤设备绑定,所以首先接收到IRP的是过滤设备的对象。
- 2.3 完整的分发函数
- 源码中实现的ccpDispatch分发函数处理所有串口的写请求,所有从串口输出的数据都用DbgPrint打印出来。也就是说,读者打开DbgView.exe就可以看到串口的输出数据了。
- 2.4 动态卸载
- 前面只说了如何绑定,但是没有说如何解除绑定。如果要把这个模块做成可以动态卸载的模块,则必须提供一个卸载函数。我们应该在卸载函数中完成解除绑定的功能;否则,一旦卸载一定会蓝屏。
- 这里涉及到3个内核API:一个是IoDetachDevice,负责将绑定的设备解除绑定;另一个是IoDeleteDevice,负责把我们前面用IoCreateDevice生成的设备删除掉以后回收内存;还有一个是KeDelayExecutionThread,纯粹负责延时。这三个函数的参数相对简单,这里就不详细介绍了,可以查看WDK帮助文档。
- 卸载过滤驱动有一个关键的问题:我们要终止这个过滤程序,但是一些IRP可能还在这个过滤程序的处理过程中。要取消这些请求非常的麻烦,而且不一定能成功。所以解决方案是等待5秒来保证安全地卸载掉。只能确信这些请求会在5秒内完成,同时等待之前我们已经解除了绑定,所以这5秒内不会有新请求发送过来处理。这对于串口而言是没问题的,但是并非所有的设备都如此。
- 三、源码和调试
- comcap.c文件
#include <ntddk.h>#define NTSTRSAFE_LIB#include <ntstrsafe.h>#ifndef SetFlag#define SetFlag(_F,_SF) ((_F) |= (_SF))#endif#ifndef ClearFlag#define ClearFlag(_F,_SF) ((_F) &= ~(_SF))#endif#define CCP_MAX_COM_ID 32// 过滤设备和真实设备static PDEVICE_OBJECT s_fltobj[CCP_MAX_COM_ID] = { 0 };static PDEVICE_OBJECT s_nextobj[CCP_MAX_COM_ID] = { 0 };// 打开一个端口设备PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status){ UNICODE_STRING name_str; static WCHAR name[32] = { 0 }; PFILE_OBJECT fileobj = NULL; PDEVICE_OBJECT devobj = NULL; // 输入字符串。 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 == STATUS_SUCCESS) ObDereferenceObject(fileobj); return devobj;}//生成并绑定一个设备NTSTATUSccpAttachDevice( 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->Flags & DO_BUFFERED_IO) (*fltobj)->Flags |= DO_BUFFERED_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); *fltobj = NULL; status = STATUS_UNSUCCESSFUL; return status; } *next = topdev; // 设置这个设备已经启动。 (*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING; return STATUS_SUCCESS;}// 这个函数绑定所有的串口。void ccpAttachAllComs(PDRIVER_OBJECT driver){ ULONG i; PDEVICE_OBJECT com_ob; NTSTATUS status; for(i = 0;i<CCP_MAX_COM_ID;i++) { // 获得object引用。 com_ob = ccpOpenCom(i,&status); if(com_ob == NULL) continue; // 在这里绑定。并不管绑定是否成功。 ccpAttachDevice(driver,com_ob,&s_fltobj[i],&s_nextobj[i]); // 取消object引用。 }}#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) IoDetachDevice(s_nextobj[i]); } // 睡眠5秒。等待所有irp处理结束 interval.QuadPart = (5*1000 * DELAY_ONE_MILLISECOND); KeDelayExecutionThread(KernelMode,FALSE,&interval); // 删除这些设备 for(i=0;i<CCP_MAX_COM_ID;i++) { if(s_fltobj[i] != NULL) IoDeleteDevice(s_fltobj[i]); }}//分发函数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_PARAMETER; IoCompleteRequest(irp,IO_NO_INCREMENT); return STATUS_SUCCESS; }NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path){ size_t i; // 所有的分发函数都设置成一样的。 for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++) { driver->MajorFunction[i] = ccpDispatch; } // 支持动态卸载。 driver->DriverUnload = ccpUnload; // 绑定所有的串口。 ccpAttachAllComs(driver); // 直接返回成功即可。 return STATUS_SUCCESS;}
- SOURCE文件
!IF 0Copyright (C) Microsoft Corporation, 1997 - 1999Module Name: sources.!ENDIFTARGETNAME=comcapTARGETPATH=objTARGETTYPE=DRIVERSOURCES =comcap.cTARGETLIBS= $(DDK_LIB_PATH)\ntstrsafe.lib
- 对文件进行编译后进行调试。编译方法可参考前面的相关文档。
- 此外可以通过以下方法使用串口:
- 打开”开始“菜单 ——》”所有程序“——》”附件“——》”通讯“——》”超级终端“,然后任意建立一个连接。如下图所示
- 注意选择COM1,然后在输入框中键入字符串。其中对话窗口默认是不回显输入的字符的,这个可以点击”文件“——》”属性“——》”设置”——》ASCII码设置,选择回显即可。
- DbgView输出:
寒江独钓Windows内核编程——串口过滤