Windows驱动开发入门-键盘过滤驱动
Windows驱动开发入门-键盘过滤驱动
碎碎念
PDO是设备栈最下面的那个物理设备对象。
csrss.exe的进程有个线程叫做win32!RawInputThread,它通过一个GUID叫GUID_CLASS_KEYBOARD获得键盘设备栈PDO符号链接名。应用程序不能通过设备名字打开设备,一般通过符号链接名来打开。win32k!RawInputThread执行到win32k!OpenDevice,一个参数能找到键盘设备栈的PDO符号链接名。win32!OpenDevice有个OBJECT_ATTRIBUTES结构局部变量,它自己初始化这个局部变量,用传入参数中的符号链接名赋值OBJECT_ATTRIBUTES+0x8处的PUNICODE_STRING ObjectName。然后调用ZwCreateFile打开设备得到句柄。
ZwCreateFile
通过系统服务,调用内核NtCreateFile
,执行到nt!IopParseDevice调用nt!IoGetAttachedDevice,通过PDO获得键盘设备栈最顶端的设备对象,该对象+30 char StackSize作为参数调用IoAllocateIrp
创建IRP。调用nt!ObCreateObject创建文件对象,初始化这个文件对象。+04 struct _DEVICE_OBJECT *DeviceObject
赋值为键盘设备栈PDO。调用nt!IopfCallDriver将IRP发往驱动。一系列返回到nt!ObOpenObjectByName,调用nt!ObpCreateHandle在csrss.exe句柄表中创建一个新的句柄,对应的对象就是刚才创建并初始化的那个文件对象。
win32!RawInputThread在获得句柄之后,以这个为参数调用nt!ZwReadFile向键盘驱动要求读入数据,并创建一个IRP_MJ_READ的IRP发送给键盘驱动,告诉键盘驱动要求读入数据。键盘驱动一般让这个IRP Pending,该IRP不会被满足被一直放在那等待来自键盘的数据,win32k!RawInputThread也会等待这个读操作完成。
当键盘有键被按下时,触发键盘那个中断,引起中断服务例程执行(由键盘驱动提供),键盘驱动从端口读取扫描码,处理后把键盘得到的数据交给IRP并结束。
IRP结束导致win32k!RawInputThread线程将会对得到的数据做出处理并分发给合适的进程。处理后win32k!RawInputThread线程立刻调用下一个nt!ZwReadFile,向键盘驱动要求读入数据,开始另一个等待键盘键被按下。
PS/2键盘设备栈长这样:顶层设备对象是驱动KbdClass生成的设备对象,中间层设别对象是驱动i8042prt生成的设备对象,底层设备对象是驱动ACPI生成的设备对象。
中断号0x93端口0x60,每次扫描码1字节,2字节的必须分两次读。设按下扫描码为X,抬起为X+0x80。
找到所有键盘设备
尝试绑定KbdClass所有设备对象。每个DRIVER_OBJECT下有个域叫做DeviceObject,每个这个域中又有个域叫做NextDevice指向同一个驱动中的下一个设备,实际上就是个设备链。另一个方法调用函数IoEnumerateDeviceObjectList
枚举。ObReferenceObjectByName
用于通过名字获得一个对象指针。
1 | extern POBJECT_TYPE IoDriverObjectType; |
应用设备扩展
生成一个过滤设备时可以给这个设备指定任意长度“设备扩展”,扩展中内容可任意填写作为自定义数据结构,这里就是C2P_DEV_EXT。
1 | typedef struct _C2P_DEV_EXT{ |
生成带有设备扩展信息的设备对象关键在调用IoCreateDevice时第二个参数填扩展长度。
1 | status = IoCreateDevice(IN DriverObject, IN sizeof(C2P_DEV_EXT), IN NULL, IN pTargetDeviceObject->DeviceType, IN pTargetDeviceObject->Characteristics, IN FALSE, OUT & pFilterDeviceObject); |
然后就是上面所说的:
1 | // 设备扩展!下面要详细讲述设备扩展的应用。 |
怎样填这个域:
1 | NTSTATUS c2pDevExtInit(IN PC2P_DEV_EXT devExt, IN PDEVICE_OBJECT pFilterDeviceObject, IN PDEVICE_OBJECT pTargetDeviceObject, IN PDEVICE_OBJECT pLowerDeviceObject) { |
驱动入口
直接去找KbdClass下所有设备进行绑定。
1 | NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { |
动态卸载
如果没有按键时请求未必会完成,卸载过滤驱动后等下次一按键直接蓝。防止未决请求没完成的方法是使用gC2pKeyCount
全局变量,有请求到来时加1,完成时减1
1 | VOID c2pDetach(IN PDEVICE_OBJECT pDeviceObject) { |
请求处理
通常的处理
跟串口一样,跳过虚拟设备处理直接发送到真实设备。
1 | NTSTATUS c2pDispatchGeneral(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { |
电源相关的处理
有些不一样,主要是先用PoStartNextPowerIrp
,并用IoCallDriver
代替。
1 | NTSTATUS c2pPower(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { |
PNP处理
设备热插拔,当PNP请求过来时Windows处理掉所有未决IRP。
1 | NTSTATUS c2pPnP(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { |
读处理
这个请求可不敢恭维,咱键盘过滤驱动要的就是这个请求。大概步骤为:先用IoCopyCurrentIrpStackLocationToNext
把当前IRP栈空间拷贝到下一个栈空间;再给这个IRP一个完成函数,当IRP完成时系统调用这个函数;最后用IoCallDriver
把请求发送到下一个设备。
1 | NTSTATUS c2pDispatchRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { |
读请求完成后应获得输出缓冲区,按键信息就在这里。缓冲区有多个KEYBOARD_INPUT_DATA结构。
1 | // 这是一个IRP完成回调函数的原型 |
自己定义控制键状态保存到kb_status
变量中用3位保留。
1 | // flags for keyboard status |
完整代码
Driver.h:
1 |
|
Driver.cpp:
1 |
|