读书笔记_键盘嗅探器(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,
 
补充:综合编程 , 安全编程 ,