键盘的过滤
8.1 技术原理
8.1.1 预备知识
何为符号链接?符号链接其实就是一个“别名”,可以用一个不同的名字来代表一个设备对象。
ZwCreateFile 是很重要的函数。同名的函数实际上有两个: 一个在内核中,一个在应用层。所以在应用程序中直接调用 CreateFile,就可以引发对这个函数的调用。它不但可以打开文件,而且可以打开设备对象(返回得到一个类似于文件句柄的句柄)。所以后面按常常会看到应用程序为了与内核交互而调用这个函数,这个函数最终调用 NtCreateFile。
何为 PDO? DO 是 Driver Object 的简称, PDO 是 Phsiycal Device Object 的简称,字面上意义是物理设备对象。我们暂时可以理解为设备栈下最下面的那个设备对象。
此外 如果看到
nt!IoGetAttachedDevice
nt!ObpCreateHandle
这是在调试工具 WinDbg 中常常出现的表示方法。!之前的内容标识模块名,而之后的内容标识函数名或者变量名。
8.1.2 Windows 中从击键到内核
这一小节专门讲述 Windows 是如何获得按键,然后传递给各个应用程序的。
打开任务管理器,可以看到一个名为 Csrss.exe 的进程。该进程十分关键,它有一个线程叫做 win32!RawInputThread ,这个线程通过一个GUID 来获得键盘设备栈的 PDO 的符号链接名。
应用程序是不能直接根据设备名字来打开设备的,一般都通过符号链接名来打开。
win32k!RawInputThread 执行到函数 win32!OpenDevice ,它的一个参数可以找到键盘设备栈的 PDO 的符号链接名。 win32!OpenDevice 有一个 OBJECT_ATTRIBUES 结构的局部变量,它自己初始化这个局部变量,用传入参数中的键盘设备栈的 PDO 的符号链接名赋值 OBJECT_ATTRIBUTES+0X8 处的 PUNICODE_STRING Object_name 。
…… 整个过程比较复杂,后续也不继续跟进了。详细可以再去网上查一下,我感觉书上写的有一些不容易理解。有流程图就更好了。不过暂时没有必要了解这些细节。我们只需要知道我们要去绑定的那个设备就是驱动 KdbClass 的设备对象就可以了。
8.1.3 键盘硬件原理
一个字符并不代表一个键,每个键只是有自己的扫描码。键盘和 CPU 的交互方式是中断和读取端口,这个操作是串行的。键盘每给 CPU 一个通知,就会发生一次中断。这个通知只能通知一个事件: 某个键被按下了,某个键弹起了。
因此,一个键实际上需要两个扫描码: 一个表示按下;另一个表示键弹起。CPU 只接受通知并读取端口的扫描码,从不主动查看任何键。
8.2 键盘过滤的框架
8.2.1 找到所有的键盘设备
要过滤一种设备,首先要绑定它。现在需要找到所有代表键盘的设备。从前面的原理来看,如果绑定了驱动 KdbClass 的所有设备对象,那么代表键盘的设备一定在其中。那么如何找到一个驱动下的所有对象呢? 联想一下第二章中对驱动对象结构的介绍。一个 DRIVER_OBJECT 下有一个域叫作 DeviceObject ,这看似是一个设备对象的指针,但是由于每个 DeviceObject 中又有一个域叫做 NextDevice, 指向同一个驱动中的下一个设备,所以这里实际上是一个设备链。
除了使用上面描述的设备链以外,还有一种获得驱动ia的所有设备对象的方法是调用函数 IoEnumerateDeviceObjectList ,这个函数也可以枚举出一个驱动下的所有设备。
现在来写代码。这些代码来自一个开源的键盘过滤例子 ctrl2cap,在github上应该可以搜到。我们首先打开驱动对象 KbdClass ,然后绑定它下面的所有设备。这里用到一个新的函数——ObReferenceObjectByName , 它用于通过一个名字来获得一个对象的指针。该函数的解释这里有较详细的
// IoDriverObjectTyp 实际上是一个全局变量,但是头文件中没有,只要声名之后就可以使用了
extern POBJECT_TPYE IoDriverObjectType;
//KbdClass 驱动的名字
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"
//这个函数是事实存在的,只是文档中没有公开
//声名一下就可以使用了
NTSTATUS ObReferenceObjectByName(
PUNICODE_STRING ObjectName,
ULONG Attributes,
PACCESS_STATE AccessState,
ACCESS_MASK DesiredAccess,
POBJET_TYPE ObjectType,
KPROCESSOR_MODE AccessMode,
PVOID ParseContext,
PVOID *Object
);
//打开驱动对象KdbClass,然后绑定它下面的所有设备
NTSTATUS c2pAttachDevices(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
NTSTATUS status = 0;
UNICODE_STRING uniNtNameString;
PC2P_DEV_EXT devExt;
PDEVICE_OBJECT pFilterDeviceObject = NULL;
PDEVICE_OBJECT pTargetDeviceObject = NULL;
PDEVICE_OBJECT pLowerDeviceObject = NULL;
PDEVICE_OBJECT KbdDriverObject = NULL;
KdPrint(("My Attach\n"));
//初始化一个字符串,即 KbdClass 驱动的名字
RtlInitUnicodeString(&uniNtNameString,KBD_DRIBER_NAME);
//参照前面打开设备对象的例子,这是这里打开的是驱动对象
status = ObReferenceObjectByName(
&uniNtNameString,
OBJ_CASE_INSENSITIVE,
NULL,
0,
IoDriverObjectType,
KernelMode,
NULL,
&KdbDriverObject
);
//如果失败了就返回
if(!NT_SUCCESS(status))
{
KdPrint(("couldn't get the KdbDriverObject!!\n"));
return status;
}
else
{
//调用 ObReferenceObjectByName 会导致对驱动对象的引用计数增加
//必须相应的调用 ObDereferenceObject 进心解引用
ObDereferenceObject(&DriverObject);
}
//这是设备链中的第一个设备
pTargetDeviceObject = KbdDirverObject->DeviceObject;
//开始遍历这个设备链并逐个绑定
while(pTargetDeviceObject)
{
//生成一个过滤设备
status = IoCreateDevice(
IN DriverObject,
IN sizeof(C2P_DEV_EXT),//指定要为设备对象的设备扩展分配的驱动程序确定的字节数。
0,
IN pTargetDeviceObject->DeviceType,
IN pTargetDeviceObject->Characteristics,
IN FALSE,
OUT &pFilterDeviceObject
);
//如果失败了就退出
if(!NT_SUCCESS(status))
{
KdPrint(("Create filterDeviceObject fail!\b"));
return status;
}
//绑定,其中 pLowerDeviceObject 就是绑定之后得到的下一个设备,也就是前面说的所谓的真实设备
pLowerDeviceObject = IoAttachDeviceToDeviceStack(&pFilterDeviceObject,&pTargetDeviceObject);
//如果绑定失败了,就放弃之前的操作,退出。
if(!pLowerDeviceObject)
{
KdPrint(("Attach fail!!"));
IoDeleteDevice(pFilterDeviceObject);
pFilterDeviceObject = NULL;
return status;
}
//设备拓展。下面要详细讲述设备拓展的应用。并且在上述创建过滤设备的时候预留了设备拓展的空间。
devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension);
c2pDevExtInit(
devExt,
pFilterDeviceObject,
pTargetDeviceObject,
pLowerDeviceObject
);
//下面的操作是为了保持一致,在串口过滤时候讲过
pFilterDeviceObject->DeviceType = pLowerDeviceObject->DeviceTpye;
pFilterDeviceObject->Characteristic = pLowerDeviceObject->Characteristics;
pFilterDeviceObject->StackSize = pLowerDeviceObject->StackSize+1;
pFilterDeviceObject->Flags |= pLowerDeviceObject->Flags & (DO_BUFFERED_IO | DO_DIRECT_IO | DO_POWER_PAGABLE);
//继续遍历下一个目标设备
pTargetDeviceObject = pTargetDeviceObject->NextDevice;
}
return status;
}
8.2.2 应用设备拓展
前面用到了设备拓展,联系前面串口过滤的例子,实际上我们用了两个数组: 一个用于保存所有的过滤设备;另一个用于保存所有的真实设备。两个数组起到一一映射的表的作用;拿到过滤设备的指针马上就可以找到真实设备的指针。
但是实际上是没有必要这样做的。在生成一个过滤设备时,我们可以给这个设备指定一个任意长度的”设备扩展“,这个扩展中的内容可以任意填写,作为一个自定义的数据结构。
这样就可以把真实设备的指针保存在过滤设备对象里了。就没有必要用两个数组分别保存它们了。
在这个键盘过滤中,作者定义的一个专门的结构作为设备拓展如下:
typedef struct _C2P_DEV_EXT
{
//这个设备的大小
ULONG NodeSize;
//过滤设备对象
PDEVICE_OBJECT pFilterDeviceObject;
//同时调用时的保护所
KSPIN_LOCK IoRequestesSpinLock;
//进程间同步处理
KEVENT IoInProgressEvent;
//绑定的设备对象
PDEVICE_OBJECT TargetDeviceObject;
//绑定前底层设备对象
PDEVICE_OBJECT LowerDeviceObject;
}C2P_DEV_EXT,*PC2P_DEV_EXT;
这里保存的除了 LowerDeviceObject 还有其他一些信息,暂时不用了解。
注意在调用 IoCreateDevice 时,第二个参数的填写
status = IoCreateDevice(
IN DriverObject,
IN sizeof(C2P_DEV_EXT),//指定要为设备对象的设备扩展分配的驱动程序确定的字节数。
0,
IN pTargetDeviceObject->DeviceType,
IN pTargetDeviceObject->Characteristics,
IN FALSE,
OUT &pFilterDeviceObject
);
接下来就可以在预留的设备拓展的空间里填写我需要的信息。相关代码如下:
//设备拓展。下面要详细讲述设备拓展的应用。并且在上述创建过滤设备的时候预留了设备拓展的空间。
devExt = (PC2P_DEV_EXT)(pFilterDeviceObject->DeviceExtension);
c2pDevExtInit(
devExt,
pFilterDeviceObject,
pTargetDeviceObject,
pLowerDeviceObject
);
NTSTATUS c2pDevExtInit(
IN PC2P_DEV_EXT devExt;
IN PDEVICE_OBJECT pFilterDeviceObject,
IN PDEVICE_OBJECT pTargetDeviceObject,
IN PDEVICE_OBJECT pLowerDeviceObject
){
memset(devExt,0,sizeof(C2P_DEV_EXT));
devExt->NodeSize = sizeof(C2P_DEV_EXT)
devExt->pFilterDeviceObject = pFilterDeviceObject;
devExt->pTargetDeviceObject = pTargetDeviceObject;
devExt->pLowerDeviceObject = pLowerDeviceObject;
KeInitializeSpinLock(&(devExt->IoRequestsSpinLock));
KeInitializeEvent(&(devExt->IoInProgressEvent),NotifiacationEvent,FALSE);//不自动复位的等待事件
return STATUS_SUCCESS;
}
8.2.3 键盘过滤模块的 DriverEntry
下面是 DriverEntry 函数的代码。这个函数就相当于应用编程里的 main 函数。下面的函数一进入就直接去找 KdbClass 下所有的设备进行绑定。调用了之前所完成的 c2pAttachDevices。
NTSTATUS DriverEntry(
IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
ULONG i;
NTSTATUS statusl
KdPrint(("c2p.SYS: entering DriverEntry"));
// 填写所有分发函数的指针
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
{
DriverObject->MajorFunction[i] = c2pDispatchGeneral;
}
// 单独地填写一个 Read 分发函数。因为重要的过滤就是读取来的按键信息。而其他的暂时都不考虑
DriverObject->MajorFunction[IPR_MJ_READ] = c2pDispatchRead;
//单独填写一个 IRP_MJ_POWER 函数。这是因为这类请求中间要调用一个
// PoCallDriver 和一个 PoStartNextPowerIrp,比较特殊
DriverObject->MajorFunction[IRP_MJ_POWER] = c2pPower;
//我们想知道什么时候绑定过的一个设备被卸载了(比如从机器上拔掉了),所以专门写一个PNP(即插即用)分发函数
DriverObject->MajorFunction[IRP_MJ_PNP] = c2pPnP;
//卸载函数
DriverObject->DriverUnload = c2pUnload;
gDriverObject = DriverObject;
//绑定所有的键盘设备
status = c2pAttachDevices(DriverObject,RegistryPath);
return status;
}
8.2.4 键盘过滤模块的动态卸载
键盘过滤模块的动态卸载和前面的串口过滤稍有不同。这是因为键盘总是处于”有一个读请求没有完成“的状态。
”“当键盘上有键被按下时,将出发键盘的那个中断,引起中断服务例程的执行,键盘中断的中断服务例程由键盘驱动提供。键盘驱动从端口读取扫描码,经过一系列的处理之后,把键盘得到的数据交给 IRP,然后结束这个 IRP。这个 IRP 的结束,将导致 win32k!RawInputThread 线程对这个读操作的等待结束。win32k!RawInputThread 将会对得到的数据做出处理,分发给合适的进程。一旦把输入数据处理完之后,win32k!RawInputThread 线程会立刻再调用一个 nt!ZwReadFile ,向键盘驱动要求读入数据,于是又一个等待开始,等待键盘上的键被按下。”
换句话说,就算类似串口驱动一样等待5秒,这个请求未必会完成。这样如果卸载了所有的过滤驱动,那么下一次一按键,这个请求就被处理,很可能马上蓝屏崩溃。
下面是对实际中动态卸载的处理。
VOID c2pUnload(In PDRIVER_OBJECT DriverObject)
{
PDEVICE_OBJECT DevictObject;
PDEVICE_OBJECT OldDeviceObject;
PC2P_DEV_EXT devExt;
LARGE_INTEGER lDelay;
PRKTHREAD CurretThread;
//延迟一些时间
lDelay = RtlConvertLongToLargeInteger(100 * DELAY_ONE_MILLISECOND);
CurrentThread = KeGetCurrentThread();
//把当前线程设置为低实时模式,以便让它的运行尽量少影响其他程序
KeSetPriorityThread(CurrentThread,LOW_REALTIME_PRIORITY);
UNREFERENCE_PARAMETER(DriverObject);
KdPrint(("DriverObject unloading...\n"));
//遍历所有设备并一律解除绑定
DeviceObject = DriverObject->DeviceObject;
while(DeviceObject)
{
//解除绑定并删除所有的设备
c2pDetach(DeviceObject);
DeviceObject = DeviceObject->nextDevice;
}
ASSERT(NULL == DriverObject->DeviceObject);
while(gC2pKeyCount)
{
KeDelayExecutionThread(KernelMode,FALSE,&lDelay);
}
KdPrint("DriverEntry unload OK!\n");
return;
}
这里防止请求没有完成的方法就是使用 gC2pKeyCount。在上述代码里 gC2pKeyCount是一个全局变量,每次有一个读请求到来时,gC2pKeyCount被加一;每次完成时候则减1。于是只有所有请求被完成的时候,才能结束等待。否则无休止的等待下去。
实际上,只有一个键被按下时,这卸载过程才结束。在下一节介绍 gC2pKeyCount 的生成。
明日计划
毕业设计的内容补充
继续学习驱动编程第八章
转载:https://blog.csdn.net/qq_38025365/article/details/105739341