[原创]windows内核编程之[键盘信息过滤]
由于前几次发帖没有注明,很多网友在问我,于是我就注明一下:本文参考《windows内核安全与驱动开发》,是一篇读书后的学习笔记。
概念:键盘过滤,和上次我发过的串口过滤是一样的。同样也是绑定键盘设备,然后拦截过往信息,就能够实现对键盘的监控了。
技术原理:首先我们需要知道如何绑定设备,绑定设备,在上次串口绑定时是直接通过设备名字找到了设备指针,然后绑定。这次我们的思路是,通过设备栈最顶层的驱动对象名字来打开驱动对象,然后绑定此驱动对象之下的所有设备。一般针对于PS/2键盘,最顶层的驱动对象名字是“KbdClass”。
那么,如何根据驱动对象名字来打开驱动对象呢?使用API:
NTSTATUS ObReferenceObjectByName( IN PUNICODE_STRING ObjectName, IN ULONG Attributes, IN PACCESS_STATE PassedAccessState OPTIONAL, IN ACCESS_MASK DesiredAccess OPTIONAL, IN POBJECT_TYPE ObjectType, IN KPROCESSOR_MODE AccessMode, IN OUT PVOID ParseContext OPTIONAL, OUT PVOID *Object );
具体就不一一介绍了,这里关注两点,第一个与最后一个参数,分别是驱动对象的名字和用于接收返回的驱动对象指针,当此函数执行完毕时,会将打开的驱动对象地址写入最后一个参数里。
那么,现在我们知道了如何打开一个驱动对象,在驱动对象的结构中,有一个“DeviceObject”成员,这个成员指向了这个驱动对象设备链的第一个设备。而在设备对象结构中,又有一个成员“NextDevice”,这指向下一个设备,通过这样的结构,构成了一个设备链表。通过链表,我们可以遍历驱动对象之下的所有设备,并绑定。
好了,我们也知道怎么绑定了,下面我们还需要知道怎么储存绑定设备的信息,比如被绑定的设备指针,过滤设备的下一层设备指针还有本身过滤设备的指针。在这里可以使用设备扩展,关于设备扩展,他是由设备对象下的一个成员“DeviceExtension”指向的一片内存空间,这片内存空间的大小由创建这个设备对象的时候指定。我们可以随意编辑这片空间,让它储存我们想要的信息。比如在里面保存一个结构体等等。每个设备的设备扩展都是这个设备独有的。
以上我们介绍了如何通过驱动名字找到设备并绑定,还谈了如何保存单个设备的特有信息,虽然没有涉及具体代码,但思路已经很清晰了,我们遍历驱动对象下的所有设备,对每一个设备,我们都创建一个新的设备来绑定,其中信息储存在“DeviceExtension”指向的设备扩展里。
那么,有了以上步骤,就可以针对发往键盘的消息进行拦截了。如何处理拦截的消息,我们需要再添加一个分发函数。这个分发函数通过几个if语句来判断不同的信息并进行针对性处理。如果我们需要拦截键盘上的按键信息,那么我们就要针对读请求进行特别处理,应用发来读取键盘的请求,但这个时候我们并不会知道键盘会读取到什么信息,所以我们需要等待键盘读取完毕之后,在拦截已经读取到的信息。这个时候,我们可以使用IoSetCompletionRoutine来设置某个irp的回调函数,当这个irp请求处理完毕的时候,就会调用我们设置的回掉函数,我们在给回调函数传入的irp缓冲区里可以找到按下键盘的扫描码。然后要怎么处理就看你自己了。因为时间篇幅等诸多原因,这里不介绍如何把扫描码转换成ascii码,不过也挺简单。
至此,所有的技术原理就讲解完了,下面来谈谈设计思路。
设计思路:
在整个程序开始,我们需要先把分发函数填写了
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
KS_driver->MajorFunction[i] = KS_Dispatch;
}
通过一个简单的for循环。
之后我们直接开始绑定设备,绑定设备之前,我们先打开驱动对象,找到设备链头,开始循环遍历。每一个循环,我们都创建一个新的设备:
IoCreateDevice(KS_driver, sizeof(KS_C2P_DEV_EXT), NULL, KS_Ldevice->DeviceType,KS_Ldevice->Characteristics, FALSE, &KS_Ndevice);
注意参数中的sizeof(KS_C2P_DEV_EXT),其中KS_C2P_DEV_EXT是我定义的一个结构体,把结构体大小当作参数传入,表示你想要开辟这么大的一块空间作为这个设备的设备扩展。
然后我们将创建的设备与设备链中的设备绑定,之后在设备扩展里储存一些必要的信息:过滤设备地址,被绑定设备地址,绑定API返回的下一层设备地址。最后我们再拷贝一些重要的属性,整个绑定就算完成了。
至于分发函数,个人觉得没什么可以讲的,因为并没有复杂的思路与新知识点。于是我就直接放出完整代码了,代码功能:拦截按键扫描码并通过dbgprint打印出来。
#include <wdm.h>
#include <ntddkbd.h>
//未文档化的函数,全局变量声明。
NTKERNELAPI
NTSTATUS
ObReferenceObjectByName(
IN PUNICODE_STRING ObjectName,
IN ULONG Attributes,
IN PACCESS_STATE PassedAccessState OPTIONAL,
IN ACCESS_MASK DesiredAccess OPTIONAL,
IN POBJECT_TYPE ObjectType,
IN KPROCESSOR_MODE AccessMode,
IN OUT PVOID ParseContext OPTIONAL,
OUT PVOID *Object
);
extern POBJECT_TYPE IoDeviceObjectType;
//KdbClass设备名
#define KS_KDBCLASS_NAME L"\\Driver\\Kbdclass"
//全局变量,便于随时调用驱动对象
PDRIVER_OBJECT KS_gDriver;
//键盘计数,用于标记是否irp完成
ULONG KS_KeyCount;
//设备扩展结构
typedef struct KS_DEV_EXT
{
ULONG Size;
//过滤设备指针
PDEVICE_OBJECT KS_Ndevice;
//保护锁
KSPIN_LOCK KS_Lock;
//事件,用于通信
KEVENT KS_Event;
//被绑定设备指针
PDEVICE_OBJECT KS_Ldevice;
//绑定前顶端设备指针
PDEVICE_OBJECT KS_Rdevice;
}KS_C2P_DEV_EXT,*KS_PC2P_DEV_EXT;
//卸载函数
VOID KS_driverUnload(PDRIVER_OBJECT KS_driver)
{
PRKTHREAD KS_pThread;
LARGE_INTEGER KS_Delay;
PDEVICE_OBJECT KS_device;
KS_PC2P_DEV_EXT KS_dev;
DbgPrint("Let's begin to unload\r\n");
KS_Delay = RtlConvertLongToLargeInteger(-10 * 3000000);
//获取当前线程
KS_pThread = KeGetCurrentThread();
//低实时模式,待会还需进行等待
KeSetPriorityThread(KS_pThread, LOW_REALTIME_PRIORITY);
KS_device = KS_driver->DeviceObject;
while (KS_device)
{
__try
{
KS_dev = (KS_PC2P_DEV_EXT)KS_device->DeviceExtension;
IoDetachDevice(KS_dev->KS_Ldevice);
KS_dev->KS_Ldevice = NULL;
IoDeleteDevice(KS_device);
KS_dev->KS_Ndevice = NULL;
}
__except (EXCEPTION_EXECUTE_HANDLER) {}
KS_device = KS_device->NextDevice;
}
while (KS_KeyCount)
{
KeDelayExecutionThread(KernelMode, FALSE, &KS_Delay);
}
DbgPrint("Unload finish!\r\n");
return;
}
//设备扩展结构构造函数
VOID KS_pDEV_Init(KS_PC2P_DEV_EXT pDEV, PDEVICE_OBJECT KS_Ndevice,
PDEVICE_OBJECT KS_Ldevice, PDEVICE_OBJECT KS_Rdevice)
{
pDEV->KS_Ndevice = KS_Ndevice;
pDEV->KS_Rdevice = KS_Rdevice;
pDEV->KS_Ldevice = KS_Ldevice;
pDEV->Size = sizeof(KS_C2P_DEV_EXT);
KeInitializeSpinLock(&(pDEV->KS_Lock));
//初始化一个用于通信的未激发对象
KeInitializeEvent(&(pDEV->KS_Event), NotificationEvent, FALSE);
}
//KbdClass设备对象绑定
NTSTATUS KS_deviceAttach(PDRIVER_OBJECT KS_driver)
{
UNICODE_STRING KS_kbdName;
PDRIVER_OBJECT KS_KbdDriver = NULL;
NTSTATUS status;
//原始设备指针,临时变量
PDEVICE_OBJECT KS_Ldevice = NULL;
//过滤设备指针,临时变量
PDEVICE_OBJECT KS_Ndevice = NULL;
//用于接收绑定函数返回的顶端设备
PDEVICE_OBJECT KS_Rdevice = NULL;
//设备扩展
KS_PC2P_DEV_EXT pExtension = NULL;
DbgPrint("I will attach the device...\r\n");
RtlInitUnicodeString(&KS_kbdName, KS_KDBCLASS_NAME);
//通过设备名字获取打开驱动对象
status = ObReferenceObjectByName(&KS_kbdName, OBJ_CASE_INSENSITIVE, NULL,
FILE_ALL_ACCESS, IoDeviceObjectType, KernelMode, NULL, &KS_KbdDriver);
if (!NT_SUCCESS(status))
{
DbgPrint("Open driver object failed!");
return status;
}
else
{
ObDereferenceObject(KS_driver);
}
//通过打开的驱动对象获得他的第一个设备对象
KS_Ldevice = KS_KbdDriver->DeviceObject;
//开始遍历,并绑定驱动对象之下的所有设备
while (KS_Ldevice)
{
//打开一个过滤设备,并让KS_Ndevice指向它
status = IoCreateDevice(KS_driver, sizeof(KS_C2P_DEV_EXT), NULL, KS_Ldevice->DeviceType,
KS_Ldevice->Characteristics, FALSE, &KS_Ndevice);
if (!NT_SUCCESS(status))
{
DbgPrint("Create device failed");
return status;
}
//将过滤设备与之前得到的KS_Ldevice设备绑定起来
KS_Rdevice = IoAttachDeviceToDeviceStack(KS_Ndevice, KS_Ldevice);
if (!KS_Rdevice)
{
DbgPrint("Attach faild!");
//删除新创建的设备
IoDeleteDevice(KS_Ndevice);
KS_Ndevice = NULL;
return status;
}
//将设备扩展结构指针pExtension指向KS_Ndevice的设备扩展
pExtension = (KS_PC2P_DEV_EXT)KS_Ndevice->DeviceExtension;
//初始化
KS_pDEV_Init(pExtension, KS_Ndevice, KS_Ldevice, KS_Rdevice);
//关键属性拷贝,将原来设备链中的顶端设备属性拷贝到过滤设备中
KS_Ndevice->Characteristics = KS_Rdevice->Characteristics;
KS_Ndevice->DeviceType = KS_Rdevice->DeviceType;
KS_Ndevice->StackSize = KS_Rdevice->StackSize + 1;
KS_Ndevice->Flags |= KS_Rdevice->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE);
KS_Ldevice = KS_Ldevice->NextDevice;
}
return status;
}
//读请求回调函数
NTSTATUS KS_Completiom(PDEVICE_OBJECT KS_device, PIRP Irp, PVOID KS_text)
{
PIO_STACK_LOCATION KS_sIrp = IoGetCurrentIrpStackLocation(Irp);
PUCHAR KS_buf = NULL;
ULONG len,i;
if (NT_SUCCESS(Irp->IoStatus.Status))
{
//获取缓冲区
KS_buf = Irp->AssociatedIrp.SystemBuffer;
len = Irp->IoStatus.Information;
for (i = 0; i < len; i++)
{
//打印扫描码
DbgPrint("KS_the keyboard:%2x\r\n", KS_buf[i]);
}
}
KS_KeyCount--;
//如果还没有处理完,则标记pending
if (Irp->PendingReturned)
{
IoMarkIrpPending(Irp);
}
return Irp->IoStatus.Status;
}
//分发函数
NTSTATUS KS_Dispatch(PDEVICE_OBJECT KS_deviceObj, PIRP Irp)
{
KS_PC2P_DEV_EXT KS_pDEV;
PIO_STACK_LOCATION KS_sIrp = IoGetCurrentIrpStackLocation(Irp);
NTSTATUS status;
//获得设备扩展地址
KS_pDEV = (KS_PC2P_DEV_EXT)KS_deviceObj->DeviceExtension;
//如果是电源操作
if (KS_sIrp->MajorFunction == IRP_MJ_POWER)
{
PoStartNextPowerIrp(Irp);
IoSkipCurrentIrpStackLocation(Irp);
return PoCallDriver(KS_pDEV->KS_Rdevice, Irp);
}
if (KS_sIrp->MajorFunction == IRP_MJ_PNP)
{
//如果设备被拔出,我们需要解除绑定,删除过滤设备
if (KS_sIrp->MinorFunction == IRP_MN_REMOVE_DEVICE)
{
IoSkipCurrentIrpStackLocation(Irp);
IoCallDriver(KS_pDEV->KS_Rdevice, Irp);
IoDetachDevice(KS_pDEV->KS_Rdevice);
IoDeleteDevice(KS_deviceObj);
return STATUS_SUCCESS;
}
else
{
IoSkipCurrentIrpStackLocation(Irp);
return IoCallDriver(KS_pDEV->KS_Rdevice, Irp);
}
}
//如果是读操作
if (KS_sIrp->MajorFunction == IRP_MJ_READ)
{
KEVENT KS_waitEvent;
KeInitializeEvent(&KS_waitEvent, NotificationEvent, FALSE);
//如果已经到达底层,则返回错误。
if (Irp->CurrentLocation == 1)
{
status = STATUS_INVALID_DEVICE_REQUEST;
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}
KS_KeyCount++;
KS_pDEV = (KS_PC2P_DEV_EXT)KS_deviceObj->DeviceExtension;
IoCopyCurrentIrpStackLocationToNext(Irp);
IoSetCompletionRoutine(Irp, KS_Completiom, KS_deviceObj, TRUE, TRUE, TRUE);
return IoCallDriver(KS_pDEV->KS_Rdevice, Irp);
}
}
NTSTATUS DriverEntry(PDRIVER_OBJECT KS_driver, PUNICODE_STRING KS_ustr)
{
ULONG i;
#if DBG
__asm int 3
#endif
//填写分发函数
DbgPrint("Begin...\r\n");
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
KS_driver->MajorFunction[i] = KS_Dispatch;
}
KS_driver->DriverUnload = KS_driverUnload;
KS_gDriver = KS_driver;
return KS_deviceAttach(KS_driver);
}
注释里面也说的很详细了。
最后说点题外话,这篇文章确实不能作为教程,因为很多细节并没有讲清楚,但它适合作为一篇用于复习的文章,对于复习来说,重要的是思路而非细节与代码,所以在思路方面我也特意花了些时间写了写,希望对大家有帮助。
qq37794802,希望能和各路大佬一起交流学习
