首页
论坛
课程
招聘
[原创]按键监控技术
2021-11-21 10:19 10647

[原创]按键监控技术

2021-11-21 10:19
10647

一.前言

实验目的实现对键盘按键的监控
操作系统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结构体,代表提供原始输入设备
uiNumDevicespRawInputDevices指向的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(设备类内具体设备)说明了它所希望接收设备的类别

  • 键盘设备:usUsagePage == 0x1,usUsage == 0x06

  • 鼠标设备:usUsagePage == 0x01,usUsage == 0x02

因此,想要接收WM_INPUT消息,就需要通过指定对应的usUsagePage和usUsage来注册输入设备

成功注册设备以后,接下来就需要使用GetRawInputData函数来从设备中获取原始输入,该函数的定义如下:

UINT GetRawInputData(HRAWINPUT hRawInput,
                     UINT uiCommand,
                     LPVOID pData,
                     PUINT pcbSize,
                     UINT cbSizeHeader);
参数含义
hRawInput指向RAWINPUT结构的句柄。它来自于WM_INPUT消息中的lParam
uiCommand

命令标志,此参数可以是以下值之一

  • RID_HEADER:从RAWINPUT结构获取头信息

  • RID_INPUT:从RAWINPUT结构获取原始数据

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指定其他特定于消息的信息
  • 根据uMsg所代表的消息的不同,就可以在该函数中可以做出不同的处理。

  • 对于接收到的不同类别的消息,如果要在该函数中处理,则返回值需要为TRUE,否则返回值应该为FALSE

根据以上内容最终可以使用以下代码来实现键盘按键监控

// 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消息,此时就可以设置完成回调函数,向下传递按键信息,获取按键信息。该键盘驱动设备就是一个过滤驱动设备,附加在键盘类驱动设备栈之上。

具体的创建过滤驱动设备的实现流程如下:

  1. 调用IoCreateDevice创建FILE_DEVICE_KEYBOARD键盘设备,并调用IoCreateSymbolicLink函数为该设备创建一个符号链接

  2. 调用IoGetDeviceObjectPointer函数获取KeyboardClass0驱动设备对象

  3. 调用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读请求中具体的处理流程如下:

  1. 调用IoCopyCurrentIrpStackLocationToNext函数将当前设备中的IRP赋值到下一个设备中

  2. 调用IoSetCompletionRoutine函数设置完成回调函数,将在下一层驱动完成由IRP指定的操作请求时调用这个函数

  3. 调用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.运行结果

可以看到程序可以正常监控到按键信息,并且驱动也可以正常卸载


[注意] 欢迎加入看雪团队!base上海,招聘CTF安全工程师,将兴趣和工作融合在一起!看雪20年安全圈的口碑,助你快速成长!

最后于 5天前 被1900编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (6)
雪    币: 229
活跃值: 活跃值 (105)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
shun其自然 活跃值 2021-11-22 04:21
2
1
不错,可惜是老技术,我他喵羡慕现在的00后,上来文章大篇列子一堆,学习一点不废劲~
雪    币: 3790
活跃值: 活跃值 (5557)
能力值: ( LV10,RANK:160 )
在线值:
发帖
回帖
粉丝
1900 活跃值 2021-11-22 08:21
3
0
shun其自然 不错,可惜是老技术,我他喵羡慕现在的00后,上来文章大篇列子一堆,学习一点不废劲~
可惜我是90后
雪    币: 2494
活跃值: 活跃值 (900)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
木志本柯 活跃值 2021-11-25 10:46
4
0
NtUserGetKeyState
NtUserGetAsyncKeyState
NtUserGetKeyboardState
NtUserAttachThreadInput
NtUserGetRawInputBuffer
NtUserGetRawInputData
NtUserRegisterHotKey
雪    币: 30
活跃值: 活跃值 (157)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
luckyyan 活跃值 2021-11-27 00:37
5
0
厉害了
雪    币: 5435
活跃值: 活跃值 (894)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
TopC 活跃值 2021-11-27 16:25
6
0
过滤驱动的缺点就是第一个按键监控不到,卸载时需要等一个按键
可以去hook kbdclass中的回调
雪    币: 46
活跃值: 活跃值 (702)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tmflxw 活跃值 1天前
7
0
第二种原始设备模型可以做成dll,在窗口程序里面加载dll检测吗
游客
登录 | 注册 方可回帖
返回