自己写的改键器——用过滤驱动实现

环境:Windows XP SP3,  WDK 7600.16385.0

        看了这么长时间驱动,这还是头一回写一个能用的东西。玩游戏的时候经常会用到改键,用过滤驱动实现改键还是比较方便的,而且可以了解一下用户层的程序是怎么得到键盘输入的。

        首先,看一下用户的程序是怎么搞到键盘输入的。以前,我们都知道,像键盘这样的慢速设备,系统是用中断对其I/O操作的,其实就是这样的,键盘是中断号是 01,这个是固定的,也就是说,现在电脑的键盘都是接在中断控制器的IRQ1的那个腿儿上,不过,我们写过滤驱动和这个中断没有关系,知道一下就行了。还 有一个问题,现在常用键盘有PS/2键盘和USB键盘两种,硬件的实现肯定是不一样的,但是在操作系统中,有一个键盘类驱动KbdClass,这个驱动在 上面两种键盘的驱动的上层,也就是说,不管是从哪种键盘得到的数据,都要经过KbdClass,所以,从KbdClass就可以得到所有键盘的输入。

         windows中有个进程叫csrss.exe,里面有个线程叫win32!RawInputThread,这个线程在开始时,会根据键盘PDO的 GUID(可以当成这个PDO的符号链接)产生一个设备对象,并打开之。然后,这个线程不停得对键盘的PDO进行读操作,当然,这个读请求不是每次可以顺 利完成的,因为你不是一直都在用键盘,也不是以系统的最高速在用键盘,所以这个线程在发出一个读请求后会等待这个请求的完成,在等待中,PDO会一直等到 它的缓冲区中被填好值才会完成IRP,请求完成后,线程把这个扫描码发给必要的进程。

        当你按下键盘中的一个键时,会引发键盘中断,系统会调用相应的中断服务例程,键盘的中断服务例程在键盘驱动里,这个中断服务例程会 把得到的输入数据放在一个结构体中,并把结构体放在一个端口驱动的缓冲区里,在这个端口驱动里,会调用KbdClass提供的一个处理输入的回调函数,把 得到的结构体放到KbdClass的一个输入数据队列里,这样,那个等待中的读请求就可以完成了。然后,用户层的程序就知道键盘的哪个键按下了,还是松开 了。

        其次,看看我们怎么实现对键盘的过滤。从上面的叙述,我们不难看到,所有对键盘读的IRP都会从KbdClass上走一回,而且对键盘的读是操作系统主动 的,所以,我们只要在KbdClass上加一层过滤驱动,就可以截获基本上所有的键盘按键(这里说基本上的原因是,如果有一个驱动在键盘输入数据还没到 KbdClass时,就把数据截走了,那我们就得不到了,比如QQ的防盗号功能,所以我们这个驱动只能用来改键,不能用于盗号木马),在截到数据以后,把 我们想要改的键盘扫描码改为我们想改的扫描码,这样,就实现了改键。

 

        下面,我们看一下这个驱动是怎样实现的:

        首先,实现我们的设备扩展对象:

   typedef struct _KBDFIL_DEV_EXT

  {

   ULONG NodeSize;                 //本结构体大小

   PDEVICE_OBJECT pFilterDeviceObject;        //我们产生的过滤驱动设备对象

   PDEVICE_OBJECT pTargetDeviceObject;      //我们要挂载的KbdClass的设备对象

   PDEVICE_OBJECT pLowerDeviceObject;       //挂载完后,我们的过滤驱动在设备栈中下面的那个设备对象

  USHORT SrcMakeCode;                                     //要改的键的扫描码,由应用程序提供

   USHORT DestMakeCode;                                  //要改为的键的扫描码,由应用程序提供

  }KBDFIL_DEV_EXT, *PKBDFIL_DEV_EXT;

        第二,我们这个驱动是一个过滤驱动,所以在DriverEntry中,把我们自己实现的读派遣函数、写派遣函数、电源派遣函数、即插即用派遣函数注册到产 生的驱动对象,还需要产生我们的设备对象,并把该设备对象挂载到KbdClass的所有设备对象上。要注意的是,一般情况下,为了安全,过滤驱动是不需要 名字的,而我们这个驱动需要应用程序把要改的键和改为的键告诉它,所以它需要一个名字和一个符号链接。

        这里要提一下的是挂载的过程,首先,我们产生一个设备对象以后要把它挂载到一个KbdClass的设备对象上,这个设备对象是怎么得到的呢?我们知道这 个驱动的名字叫KbdClass,这样,我们就可以通过这个名字得到驱动对象,然后遍历这个驱动对象所链的所有设备对象,把我们的过滤驱动挂载到这个链表 中的每个设备对象上,驱动对象可以通过一个没有文档化的函数得到,至于那些高人是怎么知道这个函数,和函数的参数及其作用的,我还有点儿想不通,不过这个 函数确实好用,原型如下:

  NTSTATUS ObReferenceObjectByName(PUNICODE_STRING ObjectName, 

                    ULONG Attributes, 

                    PACCESS_STATE AccessState, 

                    ACCESS_MASK DesiredAccess, 

                    POBJECT_TYPE ObjectType, 

                    KPROCESSOR_MODE AccessMode, 

                    PVOID ParseContext, 

                    PVOID *Object);

我 们只用关心其中的几个参数就行了,ObjectName,我们可以传L"//Driver//KbdClass",ObjectType,因为我们要得到 的是驱动对象,所以我们传IoDriverObjectType,最后Object,这是个输入参数,我们传入驱动对象指针的地址。还要注意的是,如果这 个函数返回值是STATUS_SUCCESS的话,就表示获取成功了,成功以后,系统会你获得的内核对象的引用值加1,此时,你必须用函数:

   VOID ObDereferenceObject(

      IN PVOID  Object

      );

来使你刚刚获得的对象引用减1。

        在挂载完后,要产生符号链接,以便应用程序可以对我们的驱动程序进行写操作,产生符号链接时,本来没什么好说的,但是,我在这里却遇到了一个比较无语问 题。开始时,我把设备对象的名字打错了,但是,后来我改了,不过我没有删以前的符号链接,我以为再产生同名的符号链接时,会把以前的覆盖掉,其实不是这样 的,所以在后来我每次创建符号链接时都失败。最后,我发现,在创建符号链接前,先把这个名字的符号链接删掉,如果本来就没有这个符号链接的话,删除会出 错,不过,这也没有什么,但是,这样以后,可以保证每次产生符号链接都成功,当然,这也要求我们,在命名符号链接时,千万不要和系统的符号链接名重了。

        第三,我们刚才已经说了,在初始化时,我们应该做的事情。下面看一下,在我们的驱动被卸载时,应该做的事,也就是DriverUnload应该怎么写。 如果是比较简单的驱动,也就是一般驱动教材上刚开始就介绍的驱动,在DriverUnload中只需要把我们开始时产生的设备对象从驱动对象的设备链表中 拿掉并且删除就可以了。其实,我们的DriverUnload做的主要工作也是这个,只是在清除时要注意一些东西。在刚开始时,我们就知道了,操作系统总 是主动得去读键盘的输入,所以在大部分时候,总是有一个IRP在等待键盘的输入,也就是说,当我们要删一个设备对象时,应该会有一个IRP已经从我们的这 个设备对象传下去,如果我们在这个IRP还没有返回的时候删掉了这个设备对象,那么,在这个IRP返回时,它会找不到这个设备对象,结果就是蓝屏。所以, 我们要等待,等从我们的设备对象下去的IRP都返回后,才能退出DriverUnload,完成驱动的卸载。但是,这个是怎么实现的呢?其实,我设了一个 全局变量,初始化为0,在IRP_MJ_READ的派遣函数里每次加1,在IRP_MJ_READ的完成例程里每次减1,在DriverUnload退出 前,不停得检查这个全局变量,为0时退出。在我们删除设备对象时,我们要把它从设备栈上拿下来才能删。

        第四,上面是初始化工作,以及最后的清理工作,下面我们要看一下改键的功能是怎么实现的,也就是说各个派遣函数是怎么写的。我们并不用关心所有的派遣函 数该如何实现,我们只关心其中的一小部分就行了,剩下的我们可以写一个通用的派遣函数,简单的得把IRP转发向下层的设备对象。我们要自己的写的派遣函数 的主功能号有以下几个:

IRP_MJ_CREATE:本来过滤驱动是不会得到这个功能号的IRP的。但是,因为我的应用程序要向这个过滤驱动写东西,        所以要打开这个过滤驱动,而这个函数只是简单得完成IRP,并返回一个成功就可以了。

IRP_MJ_CLOSE:原因和上面的一样

IRP_MJ_READ:用于读键盘输入

IRP_MJ_WRITE:应用程序得告诉驱动改哪两个键,就是通过它完成的

IRP_MJ_POWER:因为键盘是即插即用的,所以要实现这个和下面那个的派遣函数

IRP_MJ_PNP:即插即用,当去掉一个键盘时,应该删除对应的设备对象

下面,我们看一下这几个派遣函数是怎么实现的:

IRP_MJ_CREATE IRP_MJ_CLOSE 是一样的:

  NTSTATUS KbdFilDispatch(IN PDEVICE_OBJECT DeviceObject, 

              IN PIRP Irp)

  { //只是简单的完成

    NTSTATUS status = STATUS_SUCCESS;

    Irp->IoStatus.Status = status;

    Irp->IoStatus.Information = 0;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);

    return status;

  }

IRP_MJ_READ : 我们知道操作系统在读键盘一直都处于一个等待的状态,所以我们要想从这个IRP的派遣函数中直接得到键盘的输入,是不可能的,所以我们得设定一个完成例 程,在IRP在底层完成以后回卷时,系统调用我们的完成例程,在完成例程中,我们可以得到键盘的输入,在这里,我们可以实现改键,所以 IRP_MJ_READ的派遣函数实现的只是设定一个完成例程,然后把IRP转发到设备栈的下一层:

  NTSTATUS KbdFilDispatchRead(IN PDEVICE_OBJECT DeviceObject, 

                 IN PIRP Irp)

  {

    NTSTATUS status = STATUS_SUCCESS;

    PKBDFIL_DEV_EXT devExt = NULL;

    if(Irp->CurrentLocation == 1)

    { //这个只是一个对错误的判断,可以忽略

      ULONG ReturnedInformation = 0;

      KdPrint(("KbdFilterDriver.sys: Dispatch encountered bogus current location./n"));

      status = STATUS_INVALID_DEVICE_REQUEST;

      Irp->IoStatus.Status = status;

      Irp->IoStatus.Information = ReturnedInformation;

      IoCompleteRequest(Irp, IO_NO_INCREMENT);

      return status;

    }

    gC2pKeyCount++;   //为0时,DriverUnload才退出

    devExt = (PKBDFIL_DEV_EXT)DeviceObject->DeviceExtension;

    IoCopyCurrentIrpStackLocationToNext(Irp);   //在设置完成例程时,必须调用这个函数把IRP复制到栈的下一层,                        不能用IoSkipCurrentIrpStackLocationToNext(...)

    IoSetCompletionRoutine(Irp, KbdFilReadComplete, DeviceObject, TRUE, TRUE, TRUE);

    return IoCallDriver(devExt->pLowerDeviceObject, Irp);   //调用下层驱动

  }

在完成例程中,我们就可以得到当前键盘的输入了,不过我们得到的是一个结构体,这个结构体的定义可以在WDK的头文件的找到,我们现在只用知道,在这个结构体的第三个字节处存的是键盘按键的扫描码,要改的话改它就行了,下面是完成例程:

  NTSTATUS KbdFilReadComplete(IN PDEVICE_OBJECT DeviceObject, 

                 IN PIRP Irp, 

                 IN PVOID Context)

  {

    PIO_STACK_LOCATION stack = NULL;

    ULONG buf_len = 0;

    PUCHAR buf = NULL;

    PKBDFIL_DEV_EXT devExt = NULL;

    devExt = (PKBDFIL_DEV_EXT)(DeviceObject->DeviceExtension);

    stack = IoGetCurrentIrpStackLocation(Irp);

    if(NT_SUCCESS(Irp->IoStatus.Status))

    {

      buf = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;     //关于键盘的一系列驱动用的都是DO_BUFFERED_IO

      buf_len = Irp->IoStatus.Information;

      if(devExt->SrcMakeCode && devExt->DestMakeCode)

      {//如果都不为0,才改键

        if(buf[2] == devExt->SrcMakeCode)

        {

          buf[2] = devExt->DestMakeCode;   //这就是那第三个字节,改成我们想要的扫描码就行了

        }

      }

    }

    gC2pKeyCount--;     //为0时,DriverUnload才退出

    if(Irp->PendingReturned)

    {

      IoMarkIrpPending(Irp);     //设置IRP为挂起状态

    }

    return Irp->IoStatus.Status;

  }

IRP_MJ_WRITE : 按理说,一个键盘过滤驱动不应该对这个IRP进行处理,但是,我必须通过应该程序把要改的键和要改为的键,以及改键还是恢复改键等信息告诉驱动,所以得写 这个驱动。我产生一个下面这样的结构体,应用程序按这个结构把值填好后,用WriteFile(hFile, ...)发给驱动,驱动得到数据以后,设置相应的值:

  typedef struct _KBDFIL_PARAM

  {

    USHORT SrcMakeCode;          //要改的键

    USHORT DestMakeCode;        //要改为的键

    UCHAR Flag;        //Flag=0x01时,为改键,Flag=0x02时,为恢复改键

  }KBDFIL_PARAM, *PKBDFIL_PARAM;

下面看一下派遣函数的实现:

  NTSTATUS KbdFilDispatchWrite(IN PDEVICE_OBJECT DeviceObject, 

                 IN PIRP Irp)

  {

    NTSTATUS status;

    KBDFIL_PARAM param;

    PKBDFIL_DEV_EXT devExt = (PKBDFIL_DEV_EXT)(DeviceObject->DeviceExtension);

    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);

    ULONG WriteLength = stack->Parameters.Write.Length;

    if(WriteLength > sizeof(KBDFIL_PARAM))

    { //如果长度不对头,则返回个错误

      KdPrint(("KbdFilterDriver.sys: Too large buffer to wirte./n"));

      status = STATUS_FILE_INVALID;

      WriteLength = 0;

    }

    else

    { //因为DO_BUFFERED_IO

      memcpy(&param, Irp->AssociatedIrp.SystemBuffer, WriteLength);

      if(param.Flag == CHANGE_KEY)

      { //CHANGE_HEY的值为0x01

        devExt->SrcMakeCode = param.SrcMakeCode;

        devExt->DestMakeCode = param.DestMakeCode;

      }

      else

      {

        KdPrint(("Clear/n"));

        devExt->SrcMakeCode = 0;

        devExt->DestMakeCode = 0;

      }

      status = STATUS_SUCCESS;

    }

    Irp->IoStatus.Status = status;

    Irp->IoStatus.Information = WriteLength;

    IoCompleteRequest(Irp, IO_NO_INCREMENT);     //完成就行了,不用再向下层转发了

    KdPrint(("KbdFilterDriver.sys: Leave KbdFilterDispatchWrite./n"));

    return status;

  }


        至此,大家应该清楚整个过程是个什么样的了,在应用程序中,只要打开驱动,并设置相应的值,就可以实现改键了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值