关于如何挂钩子,请看:常见的几种DLL注入技术中后面的全局钩子注入,后面有对钩子技术的解释。要用钩子技术实现按键监控,整体过程和挂全局钩子是一样的,只是一些参数和回调函数设置不同罢了。
想要通过全局键盘钩子来实现对键盘消息进行截获,就需要当调用SetWindowHookEx的时候,将参数idHook,也就是安装的钩子类型设置为WH_KEYBOARD,此时的回调函数就是KeyboardProc,通过该回调函数就可以实现按键监控,该函数定义如下:
指定钩子过程用于确定如何处理消息的代码。如果代码小于零,则钩子过程必须将消息传递给CallNextHookEx函数而无需进一步处理,并且应该返回CallNextHookEx返回的值。此参数可以是以下值之一。
HC_ACTION:
wParam和lParam参数包含有关击键消息的信息。
HC_NOREMOVE:
wParam和lParam参数包含有关击键消息的信息,并且击键消息尚未从消息队列中删除。
0-15
指定重复计数。该值是由于用户按住键而重复击键的次数。
16-23
指定扫描代码。该值取决于OEM。
24
指定该键是扩展键,如功能键还是数字键盘上的键。如果该键是扩展键,则该值为1;否则,它是0。
25-28
保留的。
29
指定上下文代码。如果ALT键按下,则该值为1;否则,它是0。
30
指定上一个键状态。如果在发送消息之前按键已按下,则该值为1;如果钥匙向上,则为0。
31
指定转换状态。如果按下该键,则该值为0,如果释放该键,则该值为1。
由此可知,当code等于HC_ACTION的时候,lParam包含了消息的信息,此时可以通过GetKeyNameText来获得消息,该函数定义如下:
因此,通过设置回调函数和GetKeyNameText函数就可以截获按键名,就可以知道此时键盘按下的按键是哪一个,具体代码如下
可以看到,当钩子函数成功挂载以后,所有的按键消息将被监控到
想要实现按键监控,可以利用原始输入模型直接从输入设备上获取数据,该方法相比于全局键盘钩子技术更为底层有效,功能也更为强大。但是,在默认情况下,应用程序不接收原始输入,也就是不接收WM_INPUT消息,所以首先要做的就是通过RegisterRawInputDevices来注册原始输入设备,该函数定义如下:
其中RAWINPUTDEVICE结构体定义如下:
注册设备的时候,RAWINPUTDEVICE中的usUsagePage(设备类)和usUsage(设备类内具体设备)说明了它所希望接收设备的类别
键盘设备:usUsagePage == 0x1,usUsage == 0x06
鼠标设备:usUsagePage == 0x01,usUsage == 0x02
因此,想要接收WM_INPUT消息,就需要通过指定对应的usUsagePage和usUsage来注册输入设备
成功注册设备以后,接下来就需要使用GetRawInputData函数来从设备中获取原始输入,该函数的定义如下:
命令标志,此参数可以是以下值之一:
RID_HEADER:从RAWINPUT结构获取头信息
RID_INPUT:从RAWINPUT结构获取原始数据
通过这个函数,就可以将接收到的消息保存在pData中,其中RAWINPUT结构体定义如下:
RAWINPUTHEADER类型的header保存了原始输入的信息,该结构体定义如下:
当dwType表示原始输入的类型,当它为RIM_TYPEBOARD的时候表示的就是键盘的原始输入。
要获取的键盘消息则在RAWKEYBOARD的keyboard中,RAWKEYBOARD的定义如下:
表示相应的窗口消息:
WM_KEYDOWN:普通按键消息
WM_SYSKEYDOWN:系统按键消息
由于这种键盘监控的方法需要用到窗口,所以需要创建窗口程序,接下来就看看在VS2017中如何使用资源来创建窗口程序。
首先新建一个Win32应用程序,并把自动生成的文件删除,只留下.cpp文件
接着新增对话框的资源
此时会生成resource.h和MonitorKeyboard.rc,打开MonitorKeyboard.rc文件就会出现创建的对话框,删除创建的对话框中的按钮,并把对话框的ID改为IDD_DIALOG_MAIN
此时打开resource.h文件,就会看到IDD_DIALOG_MAIN的宏定义被添加进去了
此时只需要在.cpp文件中将resource.h文件引入就可以使用创建的对话框,而要创建窗口就需要使用DialogBox函数,该函数定义如下:
其中的lpDialogFunc函数是用来处理接收到的消息的,该函数定义如下
根据uMsg所代表的消息的不同,就可以在该函数中可以做出不同的处理。
对于接收到的不同类别的消息,如果要在该函数中处理,则返回值需要为TRUE,否则返回值应该为FALSE
根据以上内容最终可以使用以下代码来实现键盘按键监控
最终可以看到按键操作被监控到了,并以虚拟键码的形式弹出来了
相对于用户层的键盘监控,驱动层的监控更加底层,它可以绕过绝大多数的保护。
按键操作IRP在驱动层会经过分层驱动设备栈,对于同一设备栈中,IRP会从栈顶一直传递到栈底,且新添加的设备总是附加在设备栈的顶部,也就是说新添加的设备可以更先获取IRP。
过滤驱动的实现便是基于此:
分层驱动中在加一层而不影响它的上下层,以过滤它们之间的数据,对数据或行为进行安全的控制。
过滤是通过设备绑定实现的。
键盘过滤驱动是工作在异步模式下的,为了得到一个按键操作,它首先需要发送IRP_MJ_READ到驱动的设备栈,驱动会立刻完成这个IRP,并将刚按下的键的相关数据作为该IRP的返回值返回。
键盘过滤驱动的按键记录就是基于这个原理。驱动程序创建一个键盘设备,将它附加在键盘类KbdClass设备栈上,此时该设备就是设备栈的栈顶。当有键盘按下IRP消息的时候,该设备最先接收到IRP消息,此时就可以设置完成回调函数,向下传递按键信息,获取按键信息。该键盘驱动设备就是一个过滤驱动设备,附加在键盘类驱动设备栈之上。
具体的创建过滤驱动设备的实现流程如下:
调用IoCreateDevice创建FILE_DEVICE_KEYBOARD键盘设备,并调用IoCreateSymbolicLink函数为该设备创建一个符号链接
调用IoGetDeviceObjectPointer函数获取KeyboardClass0驱动设备对象
调用IoAttachDeviceToDeviceStack函数将创建的键盘设备附加到KeyboardClass0设备对象所在的设备栈顶之上
IoGetDeviceObjectPointer函数的定义如下:
IoAttachDeviceToDeviceStack函数的定义如下:
该函数的返回值是原来设备栈上的栈顶设备,也就是当前设备栈上附加设备的下一个设备
想要截获键盘输入,重点是截获操作系统发给键盘的IRP读请求。当键盘收到IRP读请求时,等待用户输入,若用户输入,则把用户输入的数据填充到IRP读请求中,在把这个IRP读请求发送给操作系统。自定义的键盘过滤驱动设备最先捕获到这个IRP读请求,但是里面并没有按键数据。解决方法是设置一个回调函数,然后,将IRP读请求继续往下传递。等到键盘的IRP读请求处理完毕之后,再去执行先前设置的回调函数,从中获取键盘信息。
在IRP读请求中具体的处理流程如下:
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2021-12-2 09:40
被1900编辑
,原因: