有的公司采用技术手段禁止员工使用U盘,是为了防止员工通过U盘将敏感数据带出公司,本质上是禁止敏感数据通过USB接口流出。USB接口比较复杂。本章 讨论一个类似但是简单得多的设备:串口。要禁止使用串口非常容易(给串口贴上封条,或者写一个简单的程序始终占用串口),但是要区别处理,允许非机密数据 流出,而禁止机密数据;或者要记录串口上流过的数据,然而又不影响其他的程序使用串口,就有一定难度了。这一章中,我们通过给串口设备增加一个过滤层来实 现这个功能。
而下一章中,我们则把目标转移到键盘,除了过滤之外,我们将进一步探讨是否有方法保证从键盘输入的密码不被隐藏的黑客软件通过类似的过滤方式截取。
在Windows系统上与安全软件相关的驱动开发过程中,“过滤”(filter)是极其重要的一个概念。过滤是在不影响上层和下层接口的情况下,在Windows系统内核中加入新的层,从而不需要修改上层的软件或者下层的真实驱动程序,就加入了新的功能。
举 一个比较容易理解的例子:实时监控的反病毒程序。任何高层软件或者Windows的文件系统都没有考虑过应该什么时候去检查文件中是否含有某个病毒的特征 码,实际上也不应该要求某个软件或者Windows的文件系统去考虑这些。反病毒程序需要在不改变文件系统的上层和下层接口的情况下,在中间加入一个过滤 层,这样就可以在上层软件读取文件、下层驱动提供数据时,对这些数据进行扫描,看其中是否含有某个病毒的特征码。这是一个很典型的过滤过程。
本 章之所以从串口过滤出发,是因为串口在Windows中是非常简单的设备。对一个安全软件来说,串口过滤似乎没有什么意义。不过有一些特殊场合的计算机, 要求防止“信息外泄”,或者需要知道“哪些信息外泄”了。除了网络和可移动存储设备之外,有时串口也在考虑的范围之内。
此外,使用这种方法也可以绑定键盘,从而截获用户的击键。
3.1.1 设备绑定的内核API之一
进行过滤的最主要的方法是对一个设备对象(Device Object)进行绑定。读者可以想象,Windows系统之所以可以运作,是因为Windows中已经存在许多提供了各种功能的设备对象。这些设备对象接收请求,并完成实际硬件的功能。
我们可以首先认为:一个真实的设备对应一个设备对象(虽然实际对应关系可能复杂得多)。通过编程可以生成一个虚拟的设备对象,并“绑定”(Attach)在一个真实的设备上。一旦绑定,则本来操作系统发送给真实设备的请求,就会首先发送到这个虚拟设备。
下面结合代码进行讲解。读者可能希望编译执行这些代码,驱动的初学者请先阅读本书第1章,以便学会如何安装开发环境、编译代码和调试程序。
在WDK中,有多个内核API能实现绑定功能。下面是其中一个函数的原型:
NTSTATUS
IoAttachDevice(
IN PDEVICE_OBJECT SourceDevice,
IN PUNICODE_STRING TargetDevice,
OUT PDEVICE_OBJECT *AttachedDevice
);
IoAttachDevice参数如下:
SourceDevice 是调用者生成的用来过滤的虚拟设备;而TargetDevice是要被绑定的目标设备。请注意这里的TargetDevice并不是一个 PDEVICE_OBJECT(DEVICE_OBJECT是设备对象的数据结构,以P开头的是其指针),而是一个字符串(在驱动开发中字符串用 UNICODE_STRING来表示)。实际上,这个字符串是要被绑定的设备的名字。
Windows中许多设备对象是有名字的,但是并不是所有的设备对象都有名字。必须是有名字的设备,才能用这个内核API进行绑定。
这里有一个疑问:假设这个函数绑定一个名字所对应的设备,那么如果这个设备已经被其他的设备绑定了,会怎么样呢?
如果一个设备被其他设备绑定,它们在一起的一组设备,被称为设备栈(之所以称为栈,是由于和请求的传递方式有关)。实际上,IoAttachDevice总是会绑定设备栈上最顶层的那个设备。
AttachedDevice是一个用来返回的指针的指针。绑定成功后,被绑定的设备指针被返回到这个地址。
下面这个例子绑定串口1。之所以这里绑定很方便,是因为在Windows中,串口设备是有固定名字的。第一个串口名字为“/Device/Serial0”,第二个为“/Device/Serial1”,依次类推。请注意实际编码时C语言中的“/”要写成“//”。
UNICODE_STRING com_name = RLT_CONSTANT_STRING(L"//Device//Serial0");
NTSTATUS status = IoAttachDevice(
com_filter_device, // 生成的过滤设备
&com_device_name, // 串口的设备名
&attached_device); // 被绑定的设备指针返回到这里
当然,试图执行这段代码的读者可能会发现,这里没有提供如何生成一个过滤设备的代码。在接下来的第二个API介绍之后,读者会看到完整的例子。
3.1.2 设备绑定的内核API之二
前 面已经提到了并不是所有的设备都有设备名字,所以依靠IoAttachDevice无法绑定没有名字的设备。另外还有两个API:一个是 IoAttachDeviceToDeviceStack,另一个是IoAttachDeviceToDeviceStackSafe。这两个函数功能一 样,都是根据设备对象的指针(而不是名字)进行绑定;区别是IoAttachDeviceToDeviceStackSafe更加安全,而且只有在 Windows 2000SP4和Windows XP以上的系统中才有。一般都使用IoAttachDeviceToDeviceStackSafe,但当试图兼容较低版本的Windows 2000时,应该使用IoAttachDeviceToDeviceStack。
NTSTATUS
IoAttachDeviceToDeviceStackSafe(
IN PDEVICE_OBJECT SourceDevice, // 过滤设备
IN PDEVICE_OBJECT TargetDevice, // 要被绑定的设备栈中的设备
IN OUT PDEVICE_OBJECT *AttachedToDeviceObject// 返回最终被绑定的设备
);
和第一个API是类似的,只是TargetDevice换成了一个指针。另外,AttachedToDevice Object同样也是返回最终被绑定的设备,实际上也就是绑定之前设备栈上最顶端的那个设备。
在Window 2000下应该使用另外一个函数IoAttachDeviceToDeviceStack,这个函数除了缺少最后一个参数之外(实际上放到返回值里了),其他的和IoAttachDeviceTo DeviceStackSafe函数相同。
PDEVICE_OBJECT
IoAttachDeviceToDeviceStack(
IN PDEVICE_OBJECT SourceDevice,
IN PDEVICE_OBJECT TargetDevice
);
这个函数返回了最终被绑定的设备指针,这也就导致了它不能返回一个明确的错误码。但是如果为NULL,则表示绑定失败了。
读到这里,读者一定迫不及待地想试试如何绑定一个串口了,但问题是,这里还没有介绍如何打开串口设备(从名字获得设备对象指针)和如何生成一个串口。下面就给出绑定的完整例子。
3.1.3 生成过滤设备并绑定
在绑定一个设备之前,先要知道如何生成一个用于过滤的过滤设备。函数IoCreateDevice被用于生成设备:
NTSTATUS
IoCreateDevice(
IN PDRIVER_OBJECT DriverObject,
IN ULONG DeviceExtensionSize,
IN PUNICODE_STRING DeviceName OPTIONAL,
IN DEVICE_TYPE DeviceType,
IN ULONG DeviceCharacteristics,
IN BOOLEAN Exclusive,
OUT PDEVICE_OBJECT *DeviceObject
);
这 个函数看上去很复杂,但是目前使用时,还无须了解太多。DriverObject是本驱动的驱动对象。这个指针是系统提供,从DriverEntry中传 入,在最后完整的例子中再解释。DeviceExtensionSize是设备扩展,读者请先简单地传入0。DeviceName是设备名称。一个规则 是:过滤设备一般不需要名称,所以传入NULL即可。DeviceType是设备类型,保持和被绑定的设备类型一致即可。 DeviceCharacteristics是设备特征,在生成设备对象时笔者总是凭经验直接填0,然后看是否排斥,选择FALSE。
值得注意的是,在绑定一个设备之前,应该把这个设备对象的多个子域设置成和要绑定的目标对象一致,包括标志和特征。下面是一个示例的函数,这个函数可以生成一个设备,然后绑定在另一个设备上。
NTSTATUS
ccpAttachDevice(
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;
}
3.1.4 从名字获得设备对象
在知道一个设备名字的情况下,使用函数IoGetDeviceObjectPointer可以获得这个设备对象的指针。这个函数的原型如下:
NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT *FileObject,
OUT PDEVICE_OBJECT *DeviceObject
);
其 中的ObjectName就是设备名字。DesiredAccess是期望访问的权限。实际使用时可以不要顾忌那么多,直接填写 FILE_ALL_ACCESS即可。FileObject是一个返回参数,即获得这个设备对象的同时会得到的一个文件对象(File Object)。就打开串口设备这件事而言,这个文件对象并没有什么用处。但是必须注意:在使用这个函数之后必须把这个文件对象“解除引用”,否则会引起 内存泄漏(请注意后面的代码)。
要得到的设备对象就返回在参数DeviceObject中了。示例如下:
#include <ntddk.h>
// 因为用到了RtlStringCchPrintfW,所以必须使用头文件ntstrsafe.h
// 这里定义NTSTRSAFE_LIB是为了使用静态的ntstrsafe静态库。这样才能
// 兼容Windows2000。
#define NTSTRSAFE_LIB
#include <ntstrsafe.h>
……
// 打开一个端口设备
PDEVICE_OBJECT ccpOpenCom(ULONG id,NTSTATUS *status)
{
// 外面输入的是串口的id,这里会改写成字符串的形式
UNICODE_STRING name_str;
static WCHAR name[32] = { 0 };
PFILE_OBJECT fileobj = NULL;
PDEVICE_OBJECT devobj = NULL;
// 根据id转换成串口的名字
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;
}
3.1.5 绑定所有串口
计 算机上到底有多少个串口?笔者提供不出很好的方法来判定,除了依次去打开串口0、1、2、3…,目前还不知道如果串口2不存在,是否说明串口3、4…肯定 不存在?这是没有依据的,所以只好全部测试一次。不过有一个好处是,串口是焊死在计算机上的,很少见到能“即插即用”的串口(但是有一种用USB口来虚拟 串口的设备,不知道会不会产生动态生成串口的效果,在这里先忽略这一点)。那么绑定所有串口,就只需要做一次就可以了,不用去动态地追踪串口的诞生与消 亡。
下面是一个简单的函数,实现了绑定本机上所有串口的功能。这个函数用到了前面提供的ccpOpenCom和ccpAttachDevice这两个函数。
为 了后面的过滤,这里必须把过滤设备和被绑定的设备(后面暂且称为真实设备吧,虽然这些设备未必真实)的设备对象指针都保存起来。下面的代码用两个数组保 存。数组应该多大?这取决于一台计算机上最多能有多少个串口。读者应该去查阅IBM PC标准,这里笔者随意地写一个自认为足够大的数字。
// 计算机上最多只有32个串口,这是笔者的假定
#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 };
// 这个函数绑定所有的串口
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,&s_nextobj);
}
}
没 有必要关心这个绑定是否成功,就算失败了,看一下s_fltobj即可。这个数组中不为NULL的成员表示已经绑定了,为NULL的成员则是没有绑定或者 绑定失败的。这个函数需要一个DRIVER_OBJECT的指针,这是本驱动的驱动对象,是系统在DriverEntry中传入的。
-
顶
- 0
-
踩