过滤驱动程序
过滤驱动程序可以修改已有驱动的功能,也可以对数据进行过滤加密。WDM驱动需要通过注册表记录
指定加裁的过滤驱动,OS会读取这些值完成加载,其可以是高层过滤,也可以是低层过滤。而NT较为灵
活,不用设置注册表,直接在内存中寻找设备对象,然后自行创建过滤驱动并将自己附加在这个驱动之
上。
过滤驱动的入口函数需要将所有的IRP都设置派遣例程,因为过滤驱动要保证所有上层传递下来的
IRP都能够接收到。如果想修改某个IRP的处理,例如键盘读取输入的键盘扫描码,可以在派遣例程中设
置完成例程,在完成例程中修改IRP的处理。
一般,将介于FDO与PDO间的过滤驱动程序称为低层过滤驱动程序,而将附加在FDO上面的称为高层过
滤驱动程序。
1、WDM式过滤示例
文件过滤驱动,它将挂载在磁盘驱动上,它将所有发往磁盘驱动的IRP全部拦截。一般都是将上层驱
动程序传进来的IRP进行拦截并不做处理,而转发给底层驱动程序,称为Pass-Through。特殊IRP,特殊
处理。将需要过滤的操作放在完成例程中。
wps_clip_image-8752
图示 过滤驱动程序 P507
代码
示例代码 P507
wps_clip_image-1252
图示U盘设备驱动拓扑 P509
对于过滤驱动程序的AddDevice例程,需要创建一个过滤设备对象,然后把这个对象附加在设备栈上
,并用设备扩展记录仅低于本层的驱动对象地址。对于磁盘过滤的关键在于IRP_MJ_SCSI这个IRP的捕获
,DISK.sys与USBSTOR.sys间传递的就是标准SCSI指令。如果想让设备变成只读,只要将SCSI命令中的
SCSIOP_WRITE请求设置为失败;还有另外一种方法是在SCSIOP_MODE_SENSE请求时,设置
MODE_DSP_WRITE_PROTECT位。
2、NT式过滤驱动
如将键盘中的CapsLock改成Ctrl,示例如下。
示例代码(Link)
========
Windows驱动开发WDM
之前用的驱动例子是一个功能型驱动,只不过它操作的是一个虚拟设备。这个驱动创建的FDO(功能设备
对象)是附在虚拟总线驱动创建的虚拟PDO之上的。这次来介绍一下不同于功能型驱动的过滤驱动。过滤
驱动可以在功能型驱动的上面,称之为上层过滤驱动,或者高层,反正就这个意思。过滤驱动在功能型
驱动下面,称之为下层过滤驱动。看示意图:
从名字上就可以知道过滤驱动是干啥用的。就是起过滤作用。比如:
1. 可以对写入硬盘的数据做加密,然后读取的时候解密,这样对于用户来说,根本不知道有加密解密的
过程,然后存在硬盘上的数据是加密的。
2. 可以对已有驱动做一些扩展,或者改变已有驱动的功能。比如已有驱动一次只能写1024字节的数据,
那么过滤驱动可以扩展到任何长度,然后分段调用已有驱动就是了。
我们来尝试给之前用的驱动加个过滤驱动。这个过滤驱动代码,我是从前面的驱动copy过来的,然后修
改一下。
其实过滤驱动跟功能型驱动从代码上看也没有太多不同,需要注意的是:
1. 因为过滤驱动是附在FDO上的,那么通常过滤驱动都不需要一个名字,因为caller不会直接调用过滤
驱动;
2. 因为不知道下层驱动是缓冲模式还是直接模式操作内存,那么通常过滤驱动会同时支持缓冲和直接方
式。
看一下AddDevice里面创建过滤驱动设备对象的代码:
01.NTSTATUS CreateFDO(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo)
02.{
03. NTSTATUS status;
04. PDEVICE_OBJECT fido;
05.
06. //创建FDO(Function Device Object)
07. status = IoCreateDevice(
08. DriverObject,
09. sizeof(DEVICE_EXTENSION),
10. NULL,//过滤驱动,无需名字
11. FILE_DEVICE_UNKNOWN,
12. 0,
13. FALSE,
14. &fido);
15. if( !NT_SUCCESS(status))
16. return status;
17.
18. KdPrint(("Create filter device\n"));
19.
20. PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fido->DeviceExtension;
21. pdx->fdo = fido;
22. //将FDO附加在PDO上面,并且将Extension中的NextStackDevice指向FDO的下层设备。如果PDO上
面有过滤驱动的话,NextStackDevice就是过滤驱动,如果没有就是PDO。
23. pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fido, pdo);
24.
25. if (pdx->NextStackDevice == NULL)
26. {
27. KdPrint(("attach failed\n"));
28. IoDeleteDevice(fido);
29. return STATUS_DEVICE_REMOVED;
30. }
31.
32.
33. fido->Flags |= DO_BUFFERED_IO | DO_DIRECT_IO| DO_POWER_PAGABLE;//
34. fido->Flags &= ~DO_DEVICE_INITIALIZING;//将Flag上的DO_DEVICE_INITIALIZING位清零,保
证设备初始化完毕,必须的。
35.
36. return STATUS_SUCCESS;
37.}
NTSTATUS CreateFDO(IN PDRIVER_OBJECT DriverObject, IN PDEVICE_OBJECT pdo)
{
NTSTATUS status;
PDEVICE_OBJECT fido;
//创建FDO(Function Device Object)
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION),
NULL,//过滤驱动,无需名字
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&fido);
if( !NT_SUCCESS(status))
return status;
KdPrint(("Create filter device\n"));
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fido->DeviceExtension;
pdx->fdo = fido;
//将FDO附加在PDO上面,并且将Extension中的NextStackDevice指向FDO的下层设备。如果PDO
上面有过滤驱动的话,NextStackDevice就是过滤驱动,如果没有就是PDO。
pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fido, pdo);
if (pdx->NextStackDevice == NULL)
{
KdPrint(("attach failed\n"));
IoDeleteDevice(fido);
return STATUS_DEVICE_REMOVED;
}
fido->Flags |= DO_BUFFERED_IO | DO_DIRECT_IO| DO_POWER_PAGABLE;//
fido->Flags &= ~DO_DEVICE_INITIALIZING;//将Flag上的DO_DEVICE_INITIALIZING位清零,保
证设备初始化完毕,必须的。
return STATUS_SUCCESS;
}看上去代码很简单,这就够了。主要就是创建一个设备,然后附在传进来的pdo之上。
我们在过滤驱动里面只做一件事情,把传进来的buffer的第一个字节改成x。
看code:
[cpp] view plain copy print?
01.NTSTATUS HelloWDMIOControl(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
02.{
03. KdPrint(("EX Enter HelloWDMIOControl\n"));
04.
05. PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
06. PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
07.
08.
09. //得到IOCTRL码
10. ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
11.
12. NTSTATUS status;
13. ULONG info = 0;
14. switch (code)
15. {
16. case IOCTL_ENCODE:
17. {
18. PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject;
19. do
20. {
21. pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension;
22. KdPrint(("EX Device: %x, PDX::NextStackDevice: %x\n", pFdo, pdx-
>NextStackDevice));
23. pFdo = pFdo->NextDevice;
24. } while (pFdo != NULL);
25.
26. //过滤驱动里面修改一个输入缓冲的数据。
27. char* inBuf = (char*)Irp->AssociatedIrp.SystemBuffer;
28. inBuf[0] = 'x';
29.
30. KdPrint(("Try to call lower driver Irp: %x\n", Irp));
31. pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
32. IoCopyCurrentIrpStackLocationToNext(Irp);
33. status = IoCallDriver(pdx->NextStackDevice, Irp);
34.
35. KdPrint(("Finished calling lower driver, Irp: %x", Irp));
36. }
37. break;
38. default:
39. IoSkipCurrentIrpStackLocation(Irp);
40. status = IoCallDriver(pdx->NextStackDevice, Irp);
41. break;
42. }
43.
44. KdPrint(("EX Leave HelloWDMIOControl\n"));
45. return status;
46.}
NTSTATUS HelloWDMIOControl(IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{
KdPrint(("EX Enter HelloWDMIOControl\n"));
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
//得到IOCTRL码
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
NTSTATUS status;
ULONG info = 0;
switch (code)
{
case IOCTL_ENCODE:
{
PDEVICE_OBJECT pFdo = fdo->DriverObject->DeviceObject;
do
{
pdx = (PDEVICE_EXTENSION)pFdo->DeviceExtension;
KdPrint(("EX Device: %x, PDX::NextStackDevice: %x\n", pFdo,
pdx->NextStackDevice));
pFdo = pFdo->NextDevice;
} while (pFdo != NULL);
//过滤驱动里面修改一个输入缓冲的数据。
char* inBuf = (char*)Irp->AssociatedIrp.SystemBuffer;
inBuf[0] = 'x';
KdPrint(("Try to call lower driver Irp: %x\n", Irp));
pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
IoCopyCurrentIrpStackLocationToNext(Irp);
status = IoCallDriver(pdx->NextStackDevice, Irp);
KdPrint(("Finished calling lower driver, Irp: %x", Irp));
}
break;
default:
IoSkipCurrentIrpStackLocation(Irp);
status = IoCallDriver(pdx->NextStackDevice, Irp);
break;
}
KdPrint(("EX Leave HelloWDMIOControl\n"));
return status;
}
如果是IOCTL_ENCODE的irp,那么把input buffer的第一个字节改成x,然后继续调用下层驱动。如果是
其他irp直接调用下层驱动。
代码就这么简单,现在来看看inf文件:
01.[Version]
02.Signature=$CHICAGO$
03.Provider=%MFGNAME%
04.
05.[DestinationDirs]
06.DefaultDestDir=10,system32\drivers
07.FiltJectCopyFiles=11
08.
09.[SourceDisksFiles]
10.HelloWDM_Ex.sys=1
11.
12.[SourceDisksNames]
13.1=%INSTDISK%,,,MyFilter_Check
14.
15.;------------------------------------------------------------------------------
16.; Windows 2000 Sections
17.;------------------------------------------------------------------------------
18.
19.[DefaultInstall.ntx86]
20.CopyFiles=DriverCopyFiles,FiltJectCopyFiles
21.
22.[DriverCopyFiles]
23.HelloWDM_Ex.sys,,,0x60 ; replace newer, suppress dialog
24.
25.[DefaultInstall.ntx86.services]
26.AddService=HelloWDM_Filter,,FilterService
27.
28.[FilterService]
29.ServiceType=1
30.StartType=3
31.ErrorControl=1
32.ServiceBinary=%10%\system32\drivers\HelloWDM_Ex.sys
33.
34.;------------------------------------------------------------------------------
35.; String Definitions
36.;------------------------------------------------------------------------------
37.
38.[Strings]
39.MFGNAME="Kevin filter"
40.INSTDISK="Kevin disk"
41.DESCRIPTION="Kevin - Sample Filter Driver"
[Version]
Signature=$CHICAGO$
Provider=%MFGNAME%
[DestinationDirs]
DefaultDestDir=10,system32\drivers
FiltJectCopyFiles=11
[SourceDisksFiles]
HelloWDM_Ex.sys=1
[SourceDisksNames]
1=%INSTDISK%,,,MyFilter_Check
;------------------------------------------------------------------------------
; Windows 2000 Sections
;------------------------------------------------------------------------------
[DefaultInstall.ntx86]
CopyFiles=DriverCopyFiles,FiltJectCopyFiles
[DriverCopyFiles]
HelloWDM_Ex.sys,,,0x60
; replace newer, suppress dialog
[DefaultInstall.ntx86.services]
AddService=HelloWDM_Filter,,FilterService
[FilterService]
ServiceType=1
StartType=3
ErrorControl=1
ServiceBinary=%10%\system32\drivers\HelloWDM_Ex.sys
;------------------------------------------------------------------------------
; String Definitions
;------------------------------------------------------------------------------
[Strings]
MFGNAME="Kevin filter"
INSTDISK="Kevin disk"
DESCRIPTION="Kevin - Sample Filter Driver"
比功能型驱动简单很多,没什么花头,就是要注意一下AddService=HelloWDM_Filter,,FilterService,
这里的HelloWDM_Filter后面会被用到。
这个inf文件,可以直接右键点击,然后安装。
安装这个还不够,我们还需要改一下注册表,来指明过滤驱动应该对哪些设备起作用。之前那个驱动的
ID是ClassGUID={EF2962F0-0D55-4bff-B8AA-2221EE8A79B0},那么我们可以在注册表里面找到
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Class\{EF2962F0-0D55-4BFF-B8AA-
2221EE8A79B0},给它加个子键,比如:
UpperFilters指上层过滤驱动(反之,下层驱动是LowerFilters),HelloWDM_Filter来自于inf文件的
AddService=HelloWDM_Filter,,FilterService
一旦我们创建的这个子键后,我们可以从设备管理器上看到:
重启电脑,用devicetree可以看到:
因为功能驱动HelloWDM创建了3个设备对象,那么过滤驱动将会附在再上层的FDO上面。从上图可以看到
附在MyWDMDevice3的上面。这个时候设备栈类似于:
filter device -> MyWDMDevice3 -> MyWDMDevice2 -> MyWDMDevice -> PDO(虚拟总线驱动创建的)
ok,测试一下。测试代码很简单,就是打开设备\\.\HelloWDM(MyWDMDevice),然后发一个IOCTL_ENCODE
请求(DeviceIoControl)。
01.// TestWDMDriver.cpp : Defines the entry point for the console application.
02.//
03.
04.#include "stdafx.h"
05.#include
06.#include
07.
08.#define DEVICE_NAME L"\\\\.\\HelloWDM"
09.#define DEVICE_NAME2 L"\\\\.\\HelloWDM2"
10.#define DEVICE_EX L"\\\\.\\HelloWDM_EX"
11.
12.void Test(void* pParam)
13.{
14. int index = (int)pParam;
15. //设置overlapped标志,表示异步打开
16. HANDLE hDevice;
17.
18. hDevice = CreateFile(DEVICE_NAME,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ |
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,
19. FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL);
20.
21. wprintf(L"CreateFile, name: %s, ret: %x\n", DEVICE_NAME, hDevice);
22.
23.
24.
25. if (hDevice != INVALID_HANDLE_VALUE)
26. {
27. char inbuf[100] = {0};
28.
29. sprintf(inbuf, "hello world %d", index);
30. char outbuf[100] = {0};
31. DWORD dwBytes = 0;
32.
33. DWORD dwStart = GetTickCount();
34.
35. printf("input buffer: %s\n", inbuf);
36.
37. OVERLAPPED ol = {0};
38. ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
39.
40. BOOL b = DeviceIoControl(hDevice, CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800,
METHOD_IN_DIRECT, FILE_ANY_ACCESS),
41. inbuf, strlen(inbuf), outbuf, 100, &dwBytes, &ol);
42.
43. printf("DeviceIoControl thread %d, returns %d, last error: %d, used: %d ms,
input: %s\n", index, b, GetLastError(), GetTickCount() - dwStart, inbuf);
44.
45.
46. WaitForSingleObject(ol.hEvent, INFINITE);
47.
48. DWORD dwEnd = GetTickCount();
49. //将输出buffer的数据和'm'亦或,看看是否能够得到初始的字符串。
50. for (int i = 0; i < strlen(inbuf); i++)
51. {
52. outbuf[i] = outbuf[i] ^ 'm';
53. }
54.
55. printf("Verify thread %d, outbuf: %s, used: %d ms\n", index, outbuf, dwEnd -
dwStart);
56.
57. CloseHandle(hDevice);
58.
59. }
60. else
61. printf("CreateFile failed, err: %x\n", GetLastError());
62.}
63.
64.
65.int _tmain(int argc, _TCHAR* argv[])
66.{
67. HANDLE t1 = (HANDLE)_beginthread(Test, 0, (void*)0);
68.
69. WaitForSingleObject(t1, INFINITE);
70.
71. printf("Test ends\n");
72. return 0;
73.}
// TestWDMDriver.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include
#include
#define DEVICE_NAME L"\\\\.\\HelloWDM"
#define DEVICE_NAME2 L"\\\\.\\HelloWDM2"
#define DEVICE_EX L"\\\\.\\HelloWDM_EX"
void Test(void* pParam)
{
int index = (int)pParam;
//设置overlapped标志,表示异步打开
HANDLE hDevice;
hDevice = CreateFile(DEVICE_NAME,GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ |
FILE_SHARE_WRITE,NULL,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,NULL);
wprintf(L"CreateFile, name: %s, ret: %x\n", DEVICE_NAME, hDevice);
if (hDevice != INVALID_HANDLE_VALUE)
{
char inbuf[100] = {0};
sprintf(inbuf, "hello world %d", index);
char outbuf[100] = {0};
DWORD dwBytes = 0;
DWORD dwStart = GetTickCount();
printf("input buffer: %s\n", inbuf);
OVERLAPPED ol = {0};
ol.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
BOOL b = DeviceIoControl(hDevice, CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800,
METHOD_IN_DIRECT, FILE_ANY_ACCESS),
inbuf, strlen(inbuf), outbuf, 100, &dwBytes, &ol);
printf("DeviceIoControl thread %d, returns %d, last error: %d, used: %d ms,
input: %s\n", index, b, GetLastError(), GetTickCount() - dwStart, inbuf);
WaitForSingleObject(ol.hEvent, INFINITE);
DWORD dwEnd = GetTickCount();
//将输出buffer的数据和'm'亦或,看看是否能够得到初始的字符串。
for (int i = 0; i < strlen(inbuf); i++)
{
outbuf[i] = outbuf[i] ^ 'm';
}
printf("Verify thread %d, outbuf: %s, used: %d ms\n", index, outbuf, dwEnd
- dwStart);
CloseHandle(hDevice);
}
else
printf("CreateFile failed, err: %x\n", GetLastError());
}
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE t1 = (HANDLE)_beginthread(Test, 0, (void*)0);
WaitForSingleObject(t1, INFINITE);
printf("Test ends\n");
return 0;
}
看输出:
输进去hello world 0, 输出变成了xello world 0,说明过滤驱动起作用了。看看debugview的输出:
嘿嘿,确实有过滤驱动的输出,成功。
总结,看起来过滤驱动还是蛮简单的,当然这只是一个极其简单的例子,实际情况怕是没那么简单了。
要点:
1. 代码和功能驱动没什么大分别,如果无需过滤,则直接调用下层驱动。
2. INF文件需要相应修改,看上面的例子。
3. 需要改一下注册表,找到相应的ID, 如HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
\Class\{EF2962F0-0D55-4BFF-B8AA-2221EE8A79B0},然后增加子键UpperFilters或者LowerFilters,看
上面的例子。(其实INF文件可以增加这个子键,在INF文件里面改下就好)
有关这个例子,如果我们设置为LowerFilters,那么过滤驱动就不会被调用到,为什么?很简单,因为这
个例子的功能驱动直接把IRP完成了,也就不会往下走了。
代码:http://download.csdn.net/detail/zj510/4906910
WDK(7600) 编译驱动, VS2008编译测试例子。过滤驱动可以直接右键inf文件安装。
========
过滤驱动的概念
有的公司采用技术手段禁止员工使用U盘,是为了防止员工通过U盘将敏感数据带出公司,本质上是禁止
敏感数据通过USB接口流出。USB接口比较复杂。本章讨论一个类似但是简单得多的设备:串口。要禁止
使用串口非常容易(给串口贴上封条,或者写一个简单的程序始终占用串口),但是要区别处理,允许
非机密数据流出,而禁止机密数据;或者要记录串口上流过的数据,然而又不影响其他的程序使用串口
,就有一定难度了。这一章中,我们通过给串口设备增加一个过滤层来实现这个功能。 `|:'f'2_
而下一章中,我们则把目标转移到键盘,除了过滤之外,我们将进一步探讨是否有方法保证从键盘输入
的密码不被隐藏的黑客软件通过类似的过滤方式截取。 o4WoyzX-"
在Windows系统上与安全软件相关的驱动开发过程中,“过滤”(filter)是极其重要的一个概念。过滤
是在不影响上层和下层接口的情况下,在Windows系统内核中加入新的层,从而不需要修改上层的软件或
者下层的真实驱动程序,就加入了新的功能。 U KKV{i5
举一个比较容易理解的例子:实时监控的反病毒程序。任何高层软件或者Windows的文件系统都没有考虑
过应该什么时候去检查文件中是否含有某个病毒的特征码,实际上也不应该要求某个软件或者Windows的
文件系统去考虑这些。反病毒程序需要在不改变文件系统的上层和下层接口的情况下,在中间加入一个
过滤层,这样就可以在上层软件读取文件、下层驱动提供数据时,对这些数据进行扫描,看其中是否含
有某个病毒的特征码。这是一个很典型的过滤过程。 G/Prs|ug<
本章之所以从串口过滤出发,是因为串口在Windows中是非常简单的设备。对一个安全软件来说,串口过
滤似乎没有什么意义。不过有一些特殊场合的计算机,要求防止“信息外泄”,或者需要知道“哪些信
息外泄”了。除了网络和可移动存储设备之外,有时串口也在考虑的范围之内。 5�dpS151
此外,使用这种方法也可以绑定键盘,从而截获用户的击键。 &m/bx"Oe�
3.1.1 设备绑定的内核API之一 $YBJ:GM{/
进行过滤的最主要的方法是对一个设备对象(Device Object)进行绑定。读者可以想象,Windows系统
之所以可以运作,是因为Windows中已经存在许多提供了各种功能的设备对象。这些设备对象接收请求,
并完成实际硬件的功能。 E aS.d-
我们可以首先认为:一个真实的设备对应一个设备对象(虽然实际对应关系可能复杂得多)。通过编程
可以生成一个虚拟的设备对象,并“绑定”(Attach)在一个真实的设备上。一旦绑定,则本来操作系
统发送给真实设备的请求,就会首先发送到这个虚拟设备。 YBk90/+
下面结合代码进行讲解。读者可能希望编译执行这些代码,驱动的初学者请先阅读本书第1章,以便学会
如何安装开发环境、编译代码和调试程序。 la2d"?R*
在WDK中,有多个内核API能实现绑定功能。下面是其中一个函数的原型: tO+`tj
NTSTATUS aR OLvu545
IoAttachDevice( -nHLc?:N2
IN PDEVICE_OBJECT SourceDevice, .&!�5 IN PUNICODE_STRING TargetDevice, orp)Nyy
OUT PDEVICE_OBJECT *AttachedDevice J2=N?>S>
); P z45h
IoAttachDevice参数如下: !jae"LuOm
SourceDevice 是调用者生成的用来过滤的虚拟设备;而TargetDevice是要被绑定的目标设备。请注意这
里的TargetDevice并不是一个 PDEVICE_OBJECT(DEVICE_OBJECT是设备对象的数据结构,以P开头的是其
指针),而是一个字符串(在驱动开发中字符串用 UNICODE_STRING来表示)。实际上,这个字符串是要
被绑定的设备的名字。 *~ZlG8jD
Windows中许多设备对象是有名字的,但是并不是所有的设备对象都有名字。必须是有名字的设备,才能
用这个内核API进行绑定。 M/n/=wbq2
这里有一个疑问:假设这个函数绑定一个名字所对应的设备,那么如果这个设备已经被其他的设备绑定
了,会怎么样呢? YzLPTvc
如果一个设备被其他设备绑定,它们在一起的一组设备,被称为设备栈(之所以称为栈,是由于和请求
的传递方式有关)。实际上,IoAttachDevice总是会绑定设备栈上最顶层的那个设备。 7_-(.nFjWh
AttachedDevice是一个用来返回的指针的指针。绑定成功后,被绑定的设备指针被返回到这个地址。 K
g!_9
下面这个例子绑定串口1。之所以这里绑定很方便,是因为在Windows中,串口设备是有固定名字的。第
一个串口名字为“/Device/Serial0”,第二个为“/Device/Serial1”,依次类推。请注意实际编码时C
语言中的“/”要写成“//”。 F"} U)
UNICODE_STRING com_name = RLT_CONSTANT_STRING(L"//Device//Serial0"); ?s+@tKAe
NTSTATUS status = IoAttachDevice( kc=*o=d(
com_filter_device, // 生成的过滤设备 "pf R
&com_device_name, // 串口的设备名 }fS~z
&attached_device); // 被绑定的设备指针返回到这里 [!j bz{(
当然,试图执行这段代码的读者可能会发现,这里没有提供如何生成一个过滤设备的代码。在接下来的
第二个API介绍之后,读者会看到完整的例子。 CfEw a@v
3.1.2 设备绑定的内核API之二 /&`m
前面已经提到了并不是所有的设备都有设备名字,所以依靠IoAttachDevice无法绑定没有名字的设备。
另外还有两个API:一个是 IoAttachDeviceToDeviceStack,另一个是
IoAttachDeviceToDeviceStackSafe。这两个函数功能一样,都是根据设备对象的指针(而不是名字)进
行绑定;区别是IoAttachDeviceToDeviceStackSafe更加安全,而且只有在 Windows 2000SP4和Windows
XP以上的系统中才有。一般都使用IoAttachDeviceToDeviceStackSafe,但当试图兼容较低版本的
Windows 2000时,应该使用IoAttachDeviceToDeviceStack。 gh/NI;6`
NTSTATUS ]!@9[|
IoAttachDeviceToDeviceStackSafe( s53M~(
IN PDEVICE_OBJECT SourceDevice, // 过滤设备 A^ pE#A%
IN PDEVICE_OBJECT TargetDevice, // 要被绑定的设备栈中的设备 FN@W]ZI,K
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject// 返回最终被绑定的设备 WKQ4(o0L
); sd_"}kbX
和第一个API是类似的,只是TargetDevice换成了一个指针。另外,AttachedToDevice Object同样也是
返回最终被绑定的设备,实际上也就是绑定之前设备栈上最顶端的那个设备。 >J86 -
在Window 2000下应该使用另外一个函数IoAttachDeviceToDeviceStack,这个函数除了缺少最后一个参
数之外(实际上放到返回值里了),其他的和IoAttachDeviceTo DeviceStackSafe函数相同。
K21aV6/$
PDEVICE_OBJECT ]db=oR9I
IoAttachDeviceToDeviceStack( khP- o8|(�
IN PDEVICE_OBJECT SourceDevice, jB/nN9j6
IN PDEVICE_OBJECT TargetDevice 58H|$dM{
); >gA@'/
这个函数返回了最终被绑定的设备指针,这也就导致了它不能返回一个明确的错误码。但是如果为NULL
,则表示绑定失败了。 8�b>6T
读到这里,读者一定迫不及待地想试试如何绑定一个串口了,但问题是,这里还没有介绍如何打开串口
设备(从名字获得设备对象指针)和如何生成一个串口。下面就给出绑定的完整例子。 ]^DGZ(]
3.1.3 生成过滤设备并绑定 POz)IQAy
在绑定一个设备之前,先要知道如何生成一个用于过滤的过滤设备。函数IoCreateDevice被用于生成设
备: [i@;/Uo[y
NTSTATUS NBw- 7hK IoCreateDevice( B!1F*I*
IN PDRIVER_OBJECT DriverObject, R6H'?qq9R
IN ULONG DeviceExtensionSize, �MVG1TTd
IN PUNICODE_STRING DeviceName OPTIONAL, T~/-DQLU
IN DEVICE_TYPE DeviceType, M) bp{7
IN ULONG DeviceCharacteristics, e]{gf
IN BOOLEAN Exclusive, %4 Z1lOwU
OUT PDEVICE_OBJECT *DeviceObject :iC'`* S
); :O%zvb9P
这个函数看上去很复杂,但是目前使用时,还无须了解太多。DriverObject是本驱动的驱动对象。这个
指针是系统提供,从DriverEntry中传入,在最后完整的例子中再解释。DeviceExtensionSize是设备扩
展,读者请先简单地传入0。DeviceName是设备名称。一个规则是:过滤设备一般不需要名称,所以传入
NULL即可。DeviceType是设备类型,保持和被绑定的设备类型一致即可。 DeviceCharacteristics是设
备特征,在生成设备对象时笔者总是凭经验直接填0,然后看是否排斥,选择FALSE。 -Y1Z,Gb
值得注意的是,在绑定一个设备之前,应该把这个设备对象的多个子域设置成和要绑定的目标对象一致
,包括标志和特征。下面是一个示例的函数,这个函数可以生成一个设备,然后绑定在另一个设备上。
&'yUXl)9Vm
NTSTATUS I-w=JGT/D
ccpAttachDevice( 7iyDBrWSI0
PDRIVER_OBJECT driver, /A6|6dzM
PDEVICE_OBJECT oldobj, Tt7kLOR
PDEVICE_OBJECT *fltobj, P< q�w %_
PDEVICE_OBJECT *next) PRR]=EoYu
{ T)[sh &
NTSTATUS status; 28i5 gK!
PDEVICE_OBJECT topdev = NULL; Fu4Q3D}%
// 生成设备,然后绑定 =yU!xz.Sr7
status = IoCreateDevice(driver, B[79%),
0, EGoEfNk0S
NULL, )zX7FE
oldobj->DeviceType, VEs.(o]|}
0, }s2E}&SO
FALSE, Jzehq7 %
fltobj); v, Nrn%
if (status != STATUS_SUCCESS) L?aemV/
return status; uIy! . // 拷贝重要标志位 y W}):Al?
if(oldobj->Flags & DO_BUFFERED_IO) }HVLLmQ
(*fltobj)->Flags |= DO_BUFFERED_IO; e fnQM6
if(oldobj->Flags & DO_DIRECT_IO) yVCz9*ybj#
(*fltobj)->Flags |= DO_DIRECT_IO; f Z/R
if(oldobj->Flags & DO_BUFFERED_IO) dm-iX;Q!
(*fltobj)->Flags |= DO_BUFFERED_IO; //)o!0 @
if(oldobj->Characteristics & FILE_DEVICE_SECURE_OPEN) yG]A _s'
(*fltobj)->Characteristics |= FILE_DEVICE_SECURE_OPEN; Ico1X>U
(*fltobj)->Flags |= DO_POWER_PAGABLE; {ca@Yi:B
// 将一个设备绑定到另一个设备上 >yxeH! 5
topdev = IoAttachDeviceToDeviceStack(*fltobj,oldobj); C sol ;.F
if (topdev == NULL) c:~,WC~PF
{ yY,*iEk
// 如果绑定失败了,销毁设备,返回错误。 1LaOYu
IoDeleteDevice(*fltobj); ;at:Z&
*fltobj = NULL; ;Z. Va2d
status = STATUS_UNSUCCESSFUL; `VHoCU+
return status; _.^OKm*
} ) >|/p(
*next = topdev; tamw4h{n
// 设置这个设备已经启动 ,K- Ne
(*fltobj)->Flags = (*fltobj)->Flags & ~DO_DEVICE_INITIALIZING; G7].p4E
return STATUS_SUCCESS; h5^w.;8/
} fDoeR&&Tv
3.1.4 从名字获得设备对象 V6/iu"+g&
在知道一个设备名字的情况下,使用函数IoGetDeviceObjectPointer可以获得这个设备对象的指针。这
个函数的原型如下: 6,!/x"d~8
NTSTATUS e1EH* D
IoGetDeviceObjectPointer( ipz/dJ.`m
IN PUNICODE_STRING ObjectName, i3*$VG^=
IN ACCESS_MASK DesiredAccess, qnI 7@P
OUT PFILE_OBJECT *FileObject, yOO3/NtI�V
OUT PDEVICE_OBJECT *DeviceObject >L5x<, 3;
); uLMjD.`U-8
其中的ObjectName就是设备名字。DesiredAccess是期望访问的权限。实际使用时可以不要顾忌那么多,
直接填写 FILE_ALL_ACCESS即可。FileObject是一个返回参数,即获得这个设备对象的同时会得到的一
个文件对象(File Object)。就打开串口设备这件事而言,这个文件对象并没有什么用处。但是必须注
意:在使用这个函数之后必须把这个文件对象“解除引用”,否则会引起内存泄漏(请注意后面的代码
)。 B&;Paa|=
要得到的设备对象就返回在参数DeviceObject中了。示例如下: m3}uDNJ]4
#include 4$/"kx1D!
// 因为用到了RtlStringCchPrintfW,所以必须使用头文件ntstrsafe.h 6)9TaEv.
// 这里定义NTSTRSAFE_LIB是为了使用静态的ntstrsafe静态库。这样才能 y}V}k,; Z
// 兼容Windows2000。 * NOg:fu
#define NTSTRSAFE_LIB Bx_4a
#include .+m1
…… (+[Dni15
// 打开一个端口设备 jI(C _;?
PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status) 2�1^ek
{ GfUKdURh=
// 外面输入的是串口的id,这里会改写成字符串的形式 7"%YIbK!)
UNICODE_STRING name_str; w".`f= }W
static WCHAR name[32] = { 0 }; 6V#v GJ$
PFILE_OBJECT fileobj = NULL; g4TCZ
PDEVICE_OBJECT devobj = NULL; H_Z=`7wy
// 根据id转换成串口的名字 ?oxpAD
memset(name,0,sizeof(WCHAR)*32); yVQ(EUfbE#
RtlStringCchPrintfW( |6A/jF
name,32, L"//Device//Serial%d",id); '?|lEkXb]-
RtlInitUnicodeString(&name_str,name); y*|fF"IAK
// 打开设备对象 4eVr1mgq
*status = IoGetDeviceObjectPointer( qYFKF%Sq7
&name_str, &]%W&a<
FILE_ALL_ACCESS, ..io1 f^
&fileobj, &devobj); x>p.U.S}0
uJ $t! -
// 如果打开成功了,记得一定要把文件对象解除引用 yM&iA+3
// 总之,这一句不要忽视 j�r_>mZ
if (*status == STATUS_SUCCESS) DMc{sS5
ObDereferenceObject(fileobj); E.911F
// 返回设备对象 !%2f~O!9$/
return devobj; (Kh+wDKcTx
} ,Kc=wRV=n
3.1.5 绑定所有串口 aGnN$]
计算机上到底有多少个串口?笔者提供不出很好的方法来判定,除了依次去打开串口0、1、2、3…,目
前还不知道如果串口2不存在,是否说明串口3、4…肯定不存在?这是没有依据的,所以只好全部测试一
次。不过有一个好处是,串口是焊死在计算机上的,很少见到能“即插即用”的串口(但是有一种用USB
口来虚拟串口的设备,不知道会不会产生动态生成串口的效果,在这里先忽略这一点)。那么绑定所有
串口,就只需要做一次就可以了,不用去动态地追踪串口的诞生与消亡。 7zv./9%
下面是一个简单的函数,实现了绑定本机上所有串口的功能。这个函数用到了前面提供的ccpOpenCom和
ccpAttachDevice这两个函数。 cS(v4,C
为了后面的过滤,这里必须把过滤设备和被绑定的设备(后面暂且称为真实设备吧,虽然这些设备未必
真实)的设备对象指针都保存起来。下面的代码用两个数组保存。数组应该多大?这取决于一台计算机
上最多能有多少个串口。读者应该去查阅IBM PC标准,这里笔者随意地写一个自认为足够大的数字。
xsRMPB
// 计算机上最多只有32个串口,这是笔者的假定 x6g-HiSkO
#define CCP_MAX_COM_ID 32 GZ~z%1og
// 保存所有过滤设备指针 3>A1C8
static PDEVICE_OBJECT s_fltobj[CCP_MAX_COM_ID] = { 0 }; // 保存所有真实设备指针 ^O:Mt//B
static PDEVICE_OBJECT s_nextobj[CCP_MAX_COM_ID] = { 0 }; kyh;~D;M
// 这个函数绑定所有的串口 n@DPW, }
void ccpAttachAllComs(PDRIVER_OBJECT driver) 7�dE;FI
{ {(s_dtA_
ULONG i; RETffF/
PDEVICE_OBJECT com_ob; HKz-Ok]vQ
NTSTATUS status; g44<@]J=E
for(i = 0;i { $@sPn#@ *
// 获得object引用 ! Oi]3bO
com_ob = ccpOpenCom(i,&status); BK?+&
if(com_ob == NULL) Xh1|6#i6l
continue; A F^LOA$*j
// 在这里绑定,并不管绑定是否成功 /ITFLs!3
ccpAttachDevice(driver,com_ob,&s_fltobj,&s_nextobj); !?Ojij.$gg
} 7']#0 +
} x=[A iF9 8
没有必要关心这个绑定是否成功,就算失败了,看一下s_fltobj即可。这个数组中不为NULL的成员表示
已经绑定了,为NULL的成员则是没有绑定或者绑定失败的。这个函数需要一个DRIVER_OBJECT的指针,这
是本驱动的驱动对象,是系统在DriverEntry中传入的。 D Kn21U
========