文章目录
技术原理
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,所以不用向之前一样自己在进行处理。
- 具体操作看完整代码。
读的处理
- 处理键盘读请求时,像之前那样处理完毕后直接下发不再可行。
- 因为当一个键盘读请求带来时,我们只能拦截到一个键扫描码值,但是在完成前并不知道这个值是多少。但是我们要达到获取键的值的目的,所以使用如下的步骤进行过滤:
- 调用
IoCopyCurrentIrpStackLocationToNext
把当前IRP栈空间拷贝到下一个栈空间。 - 使用函数
IoSetCompletionRoutine
给这个IRP设置一个完成函数,完成函数的含义是,如果这个IRP完成了,系统就会回调这个函数。 - 调用
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<