内核与驱动_08_键盘驱动原理及代码

技术原理

Windows中从击键到内核

  • 打开任务管理器,可以看到一个“csrss.exe”进程,这个进程很关键,它有一个线程叫做win32!RawInputThread,这个线程通过一个GUID(GUID_CLASS_KEYBOARD)来获得键盘设备栈的PDO(Phsiycal Device Object)的符号连接名。
  • csrss.exe通常是系统的正常进程,所在的进程文件是csrss或csrss.exe,是微软客户端、服务端运行时子系统,windows的核心进程之一。管理Windows图形相关任务,对系统的正常运行非常重要。csrss是Client/Server Runtime Server Subsystem的简称,即客户/服务器运行子系统,用以控制Windows图形相关子系统,必须一直运行。csrss用于维持Windows的控制,创建或者删除线程和一些16位的虚拟MS-DOS环境。也有可能是W32.Netsky.AB@mm等病毒创建的
  • 应用程序是不能直接依据设备名来打开设备的,一般都可以使用CreateFile通过符号链接名来打开。

流程

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqwzYeCz-1581949420750)(E:\笔记\Windows内核安全与驱动开发\第二部分-开发\第八章-键盘的过滤\原理及框架.assets\1578474035408.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YxmUr6Wf-1581949420758)(E:\笔记\Windows内核安全与驱动开发\第二部分-开发\第八章-键盘的过滤\原理及框架.assets\1578474054145.png)]

  • 上面一大段调用过程暂时不用仔细去理解,简单来说就是win32k!RawInputThread线程总是调用nt!ZwReadFile函数要求读入数据,然后等待键盘上的键被按下,当键盘上的键被按下时,win32k!RawInputThread处理nt!ZwReadFile得到的数据,然后nt!ZwReadFile要求读入数据,再等待键盘上的键被按下。
  • 我们一般看到的PS/2(Personal System2,常用于链接电脑键盘、鼠标等设备的接口)键盘的设备栈,如果自己没有安装另外其他键盘过滤程序,那么设备栈的情况就如下:
    • 最顶层的设备对象是驱动KbdClass生成的设备对象
    • 中间层的设备对象是驱动i8042prt生成的设备对象
    • 最底层的设备对象是驱动ACPI生成得设备对象

键盘硬件原理

  • 键盘并不用字符来表示键,而是给每一个键规定了一个扫描码,当然因为键盘的排布方式不同,所以去搞清楚每个键的扫描码时多时是没有意义的。
  • 键盘和CPU的交互方式是通过中断和读取端口,这是个串行操作。发生依次中断,就等于键盘给了CPU依次通知,这个通知只能通知一个事件;某个键被按下或弹起。CPU不会主动去“查看”任何键,只是接收通知并读取端口的扫描码。
  • 所以一个键需要两个扫描码:一个表示键按下,另一个表示键弹起。依据网上的资料来看,如果按下的键的扫描码为X,那么同一个键弹起的扫描码就为X+0x80.
  • XP下端口和和中断号都是定死的,即中断号为0x93,端口号为0x60。每次中断时发生时,CPU都去读取端口0x60中的扫描码,0x60中只保存一个字节,但是扫描码是可以有两个字节的,此时就会发生两次中断,CPU会先后读到扫描码的两个字节。

键盘过滤的框架搭建

  • 如果绑定了KbdClass驱动的所有设备对象,那么代表键盘的设备一定也在其中。

  • 获取一个驱动全部设备对象可以有下面的方法:

    • 驱动对象结构DRIVER_OBJECT下有一个DeviceObject的域,有因为每一个DeviceObject中又有一个叫做NextDevice的域指向了驱动中的下一个设备,所以这实际上就是一个设备链,可以从这里获取到驱动的所有设备。
    • 另一种方法是调用函数IoEnumerateDeviceObjectList,这个函数可以枚举出一个驱动下的所有设备。
  • 依据开源的键盘过滤示例Ctr12Cap的源码和书中作者的代码,完成自己的键盘过滤驱动:

  • 这里用到了一个新的函数-ObRefferenceObjectByName,函数可以通过一个名字来获得一个对象的指针。

//函数需要自己声明
NTSTATUS ObReferenceObjectByName(
	PUNICODE_STRING ObjectName,
    ULONG Attribute,
    PACCESS_STATE AccessState,
    ACCESS_MASK DesiredAccess,
    POBJECT_TYPE ObjectType,
    KPROCESSOR_MODE AccessMode,
    PVOID ParseContext,
    PVOID *Object
);

应用设备扩展

  • 在之前串口过滤的例子中,用到了两个数组:一个用于保存所有的过滤设备;另一个用于保存所有的真实设备。两个数组依据下标形成一个一一映射的表的作用,拿到过滤设备的指针,马上就可以找到真实设备的指针。

  • 但是实际上我们可以在生成一个过滤设备时,为这个设备指定一个任意长度的“设备扩展”,扩展中的内容可以任意填写,形成一个自定义的数据结构。这样就可以把真实设备的指针保存在虚拟的设备对象中,更加方便查询。

  • 在代码中有体现,定义了一个拓展结构,然后在使用IoCrreateDevice创建设备对象时第二个参数这次填入结构体的长度如:


//设备拓展结构体
typedef struct _C2P_DEVICE_EXT
{
   
	//结构体大小
	ULONG thisSize;	
	//过滤设备对象
	PDEVICE_OBJECT pFilterDeviceObject;
	//同时调用时的保护锁
	KSPIN_LOCK IoRequestsSpinLock;
	//进程间同步处理
	KEVENT IoInprogressEvent;
	//绑定的设备对象
	PDEVICE_OBJECT targetDeviceObject;
	//绑定前底层设备对象
	PDEVICE_OBJECT lowerDeviceObject;
}C2P_DEVICE_EXT,*PC2P_DEVICE_EXT;
//创建示例
status=IoCreateDevice(
	pDriver,
    sizeof(C2P_DEVICE_EXT),
    NULL,
    pTargetDeviceObject->DeviceType,
    pTargetDeviceObject->Characteristics,
    FALSE,
    &pFilterDeviceObject
);
  • 具体设备扩展的使用体现在了完整代码中,可以将填写设备扩展中的内容封装在了一个MyC2pDeviceExtInit自定义函数中,这样更加方便使用。

键盘过滤模块的动态卸载

  • 键盘的过滤模块的动态卸载和前面的串口过滤稍有不同,回忆之前的内容,可以想到键盘总是处于“有一个读请求没有完成”的状态。

  • 简单在回顾一下就是:当键盘上有键被按下时,将触发键盘的中断,引起中断服务历程的执行,键盘中断的中断服务历程由键盘驱动提供,键盘驱动从端口读取扫描码,经过一系列的处理之后,把从键盘的到的数据交给IRP,然后结束这个IRP。

    这个IRP的结束将导致win32k!RawInputThread这个线程对读操作的等待结束,win32k!RawInputThread线程会对得到的数据进行处理,发送给合适的进程。一旦把输入的数据处理完之后,win32k!RawInputThread会立即再调用一个nt!ZwReadFile向键盘驱动请求读入数据,也就是在开始一次等待,等待键盘上的键被按下。

  • 因此,即使向串口一样等待5秒,这个等待请求也不会完成。这是如果卸载了过滤驱动,那么可能造成蓝屏崩溃。

  • 实际实现在完整代码中。

键盘过滤的请求处理

通常的处理

  • 最通常的处理就是直接发送到真实设备,跳过虚拟设备的处理,类似于前面串口过滤用过的方法一样。
  • 代码示例如下:
NTSTATUS MyC2pDispatchGeneral(
	IN PDEVICE_OBJECT pDevice,
    IN PIRP pIrp
)
{
   
    //其它不处理的IRP直接skip然后再用IoCallDriver把IRP发送到帧数设备的设备对象上
    Kdprintf(("Other Dispatch\r\n"));
    IoSkipCurrentIrpStackLocation(pIrp);
    return IoCallDriver(((PC2P_DEVICE_EXT)pDriver->DeviceExtension)->LowerDeviceObject,Irp);
}
  • 与电源相关的IRP的处理相对不同:
    • 在调用IoSipCurrentIrpStackLocation前,先调用了PoStartNextPowerIrp
    • PoCallDriver代替了IoCallDriver
NTSTATUS MyC2pPowerDispatch(
	IN PDEVICE_OBJECT pDevice,
    IN PIRP pIrp
)
{
   
    PC2P_DEVICE_EXT devExt=(PC2P_DEVICE_EXT)pDevice->DeviceExtension;
    PoStartNextPowerIrp(pIrp);
    IoSkipCurrentirpStackLocation(pIrp);
    return PoCallDriver(devExt->lowerDeviceObject,pIrp);
}

PNP的处理

  • 前面说到的PNP,对其唯一需要处理的是,当有一个设备被拔出时,解除绑定,并删除过滤设备。
  • 当PNP请求过来时,是没有必要担心是否还有未完成的IRP的,因为这是Windows系统要求卸载设备,此时Windows自己应该已经处理掉了未决的IRP,所以不用向之前一样自己在进行处理。
  • 具体操作看完整代码。

读的处理

  • 处理键盘读请求时,像之前那样处理完毕后直接下发不再可行。
  • 因为当一个键盘读请求带来时,我们只能拦截到一个键扫描码值,但是在完成前并不知道这个值是多少。但是我们要达到获取键的值的目的,所以使用如下的步骤进行过滤:
    1. 调用IoCopyCurrentIrpStackLocationToNext把当前IRP栈空间拷贝到下一个栈空间。
    2. 使用函数IoSetCompletionRoutine给这个IRP设置一个完成函数,完成函数的含义是,如果这个IRP完成了,系统就会回调这个函数。
    3. 调用IoCallDriver把请求发送到下一个设备,也就是真实设备。

读完成的处理

  • 也就是上面设置的完成回调函数的编写,这里是读请求完成后的调用,应该用来获得缓冲区,按键信息就存在输出缓冲区中。

  • 这个时候打印出的按键信息其实并不是我们想要的可读的信息,所以我们要对信息做进一步的处理。

从请求中打印出按键信息

从缓冲区中获得KEYBOARD_INPUT_DATA

  • 上面已经通过pIrp->AssociatedIrp.SystemBuffer中获取到了缓冲区的数据,不过这个缓冲区有固定的格式,其中可能含有n个KEYBOARD_INPUT_DATA结构,结构体定义如下:
typedef struct _KEYBOARD_INPUT_DATA{
   
   //在头文件里解释如下:对于设备\Device\KeyboardPort0,这个值是0,对于\Device\KeyboardPort1,这个值是1,依次类推
    USHORT UnitId;
    //扫描码
    USHORT MakeCode;
    //一个标志,标志着键是按下还是弹起
    USHORT Flags;
    //保留
    USHORT Reserved;
    //扩展信息
    ULONG ExtraInformation;
}KEYBOARD_INPUT_DATA,*PKEYBOARD_INPUT_DATA;
  • Flags可能的取值可以有如下这些:
#define KEY_MAKE 	0
#define KEY_BREAK 	1
#define KEY_E0		2
#define KEY_E1		3
#define KEY_TERMSRV_SET_LED  8
#define KEY_TERMSRV_SHADOW   0x10
#define KEY_TERMSRV_VKPACKET 0x20

从KEYBOARD_INPUT_DATA中得到键

  • KEYBOARD_INPUT_DATA下的MakeCode就是扫描码,不过暂时只考虑Flags为KEY_MAKE(0)KEY_BREAK(非0)两种可能,一种表示按下;另一种表示弹起。

从MakeCode得到实际字符

  • 大小写字符的ASCII码不同,但是它们的按键码却是相同的,具体是哪个会取决于如Shift、Caps Lock键的状态,因此必须先把这几个控制键的状态保存下来。
  • 然后对于不同的控制键使用不同的过滤方法。

完整代码

#include <ntddk.h>
#include <ntddkbd.h>

//全局的一个对象类型,IoDriverObjectType实际上是一个全局变量
//但是在头文件中灭有,需要声明
extern  POBJECT_TYPE *IoDriverObjectType;

ULONG g_C2PKeyCount = 0;	//存储未决请求的个数

//函数ObRegerenceObjectByName原型声明
NTSTATUS ObReferenceObjectByName(
	PUNICODE_STRING ObjectName,
	ULONG Attribute,
	PACCESS_STATE AccessState,
	ACCESS_MASK DesiredAccess,
	POBJECT_TYPE ObjectType,
	KPROCESSOR_MODE AccessMode,
	PVOID ParseContext,
	PVOID *Object
);

//全局的KbdClass驱动的名称
#define KBD_DRIVER_NAME L"\\Driver\\Kbdclass"

//设备拓展结构体
typedef struct _C2P_DEVICE_EXT
{
   
	//结构体大小
	ULONG thisSize;	
	//过滤设备对象
	PDEVICE_OBJECT pFilterDeviceObject;
	//同时调用时的保护锁
	KSPIN_LOCK IoRequestsSpinLock;
	//进程间同步处理
	KEVENT IoInprogressEvent;
	//绑定的设备对象
	PDEVICE_OBJECT targetDeviceObject;
	//绑定前底层设备对象
	PDEVICE_OBJECT lowerDeviceObject;
}C2P_DEVICE_EXT,*PC2P_DEVICE_EXT;


//初始化设备扩展
NTSTATUS MyC2pDeviceExtInit(
	IN PC2P_DEVICE_EXT devExt,
	IN PDEVICE_OBJECT pFilterDeviceObject,
	IN PDEVICE_OBJECT pTargetDeviceObject,
	IN PDEVICE_OBJECT pLowerDeviceObject
)
{
   
	memset(devExt, 0, sizeof(C2P_DEVICE_EXT));
	devExt->thisSize = sizeof(C2P_DEVICE_EXT);
	devExt->pFilterDeviceObject = pFilterDeviceObject;
	KeInitializeSpinLock<
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值