从.到实现一个Windows上的虚拟hid键盘设备

从.到实现一个Windows上的虚拟HID键盘设备

一、引言:什么是HID和虚拟键盘?在计算机系统中,HID(Human Interface Device,人机交互设备)是一类标准的输入设备协议,包括键盘、鼠标、游戏手柄等。Windows操作系统通过HID协议与这些设备通信,而虚拟HID键盘则是一种用软件模拟物理键盘行为的设备。简单来说,虚拟HID键盘允许程序向系统发送按键事件,就像用户正在真实键盘上打字一样。想象一个场景:你想为游戏或自动化工具创建一个自定义的快捷键输入系统,或者需要远程控制另一台电脑的键盘输入。传统的SendKeyskeybd_event函数在某些场景下会受到UIPI(用户界面特权隔离)限制,而虚拟HID设备可以绕过这些限制,因为它以硬件驱动级别的身份工作。本文将带你从零开始,理解HID基本概念,并使用C语言在Windows上实现一个完整的虚拟HID键盘驱动程序。---## 二、HID协议基础:数据包结构HID协议的核心是通过报告描述符(Report Descriptor)定义设备与主机之间的数据交换格式。对于键盘,报告描述符定义了按键状态的编码方式。一个标准键盘报告通常包含:- 1字节修饰键(Modifier Keys):如Ctrl、Shift、Alt、Win键的位掩码- 1字节保留位- 6字节按键数组:最多同时按下6个普通键例如,按下"A"键(按键码0x04)时,报告数据包为:[0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00]。### 报告描述符示例报告描述符使用HID协议定义的字节码描述,下面是一个标准键盘的报告描述符:0x05, 0x01, // Usage Page (Generic Desktop)0x09, 0x06, // Usage (Keyboard)0xA1, 0x01, // Collection (Application)0x05, 0x07, // Usage Page (Key Codes)0x19, 0xE0, // Usage Minimum (224)0x29, 0xE7, // Usage Maximum (231)0x15, 0x00, // Logical Minimum (0)0x25, 0x01, // Logical Maximum (1)0x75, 0x01, // Report Size (1)0x95, 0x08, // Report Count (8)0x81, 0x02, // Input (Data, Var, Abs)0x95, 0x01, // Report Count (1)0x75, 0x08, // Report Size (8)0x81, 0x01, // Input (Const)0x95, 0x06, // Report Count (6)0x75, 0x08, // Report Size (8)0x15, 0x00, // Logical Minimum (0)0x25, 0x65, // Logical Maximum (101)0x05, 0x07, // Usage Page (Key Codes)0x19, 0x00, // Usage Minimum (0)0x29, 0x65, // Usage Maximum (101)0x81, 0x00, // Input (Data, Array)0xC0 // End Collection这段描述符定义了8个修饰键位(每个1位)和6个普通键位(每个8位)的数据格式。理解这个结构是后续实现的基础。---## 三、Windows虚拟HID框架:HID Minidriver在Windows上创建虚拟HID设备需要实现一个HID Minidriver (小型驱动)。这并不是一个完整的硬件驱动,而是一个运行在用户模式下的服务,通过一个特殊的API(如HidRegisterMinidriver)向系统注册为虚拟HID设备。Windows提供了HidRegisterMinidriver函数(在hid.dll中),它允许用户模式程序注册一个回调,当系统需要读取HID报告时调用。### 开发环境准备你需要:1. Windows SDK(包含hid.dllsetupapi.h)2. C/C++编译器(如Visual Studio或MinGW)3. 管理员权限运行(驱动注册需要)### 核心步骤1. 定义报告描述符2. 实现回调函数(读取报告、获取描述符等)3. 调用HidRegisterMinidriver注册设备4. 启动消息循环处理事件---## 四、代码实现一:基础框架与设备注册下面是一个完整的虚拟HID键盘注册程序框架。注意:为了简化,我们使用HidRegisterMinidriver的旧版API(Windows 10 1809之前可用),新版需要额外的驱动签名。c#include <windows.h>#include <hidclass.h>#include <hidsdi.h>#include <stdio.h>#pragma comment(lib, "hid.lib")#pragma comment(lib, "setupapi.lib")// 键盘报告描述符(标准6键键盘)const UCHAR KeyboardReportDescriptor[] = { 0x05, 0x01, // Usage Page (Generic Desktop) 0x09, 0x06, // Usage (Keyboard) 0xA1, 0x01, // Collection (Application) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0xE0, // Usage Minimum (224) 0x29, 0xE7, // Usage Maximum (231) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x01, // Logical Maximum (1) 0x75, 0x01, // Report Size (1) 0x95, 0x08, // Report Count (8) 0x81, 0x02, // Input (Data, Var, Abs) 0x95, 0x01, // Report Count (1) 0x75, 0x08, // Report Size (8) 0x81, 0x01, // Input (Const) 0x95, 0x06, // Report Count (6) 0x75, 0x08, // Report Size (8) 0x15, 0x00, // Logical Minimum (0) 0x25, 0x65, // Logical Maximum (101) 0x05, 0x07, // Usage Page (Key Codes) 0x19, 0x00, // Usage Minimum (0) 0x29, 0x65, // Usage Maximum (101) 0x81, 0x00, // Input (Data, Array) 0xC0 // End Collection};// 全局设备句柄HANDLE g_hDevice = INVALID_HANDLE_VALUE;// 回调函数:获取报告描述符NTSTATUS __stdcall MiniDriverGetReportDescriptor( PVOID Context, PHIDP_REPORT_DESCRIPTOR *Descriptor, PULONG DescriptorLength) { *Descriptor = (PHIDP_REPORT_DESCRIPTOR)KeyboardReportDescriptor; *DescriptorLength = sizeof(KeyboardReportDescriptor); return STATUS_SUCCESS;}// 回调函数:读取输入报告(系统调用此函数获取按键状态)NTSTATUS __stdcall MiniDriverReadReport( PVOID Context, UCHAR ReportID, PHIDP_REPORT_TYPE ReportType, PUCHAR ReportBuffer, PULONG ReportLength) { // 这里应该返回当前的键盘状态数据 // 为了演示,我们返回一个按下'A'键的报告 if (ReportType == HidP_Input && ReportID == 0) { // 报告格式: [modifier, reserved, key1, key2, key3, key4, key5, key6] ReportBuffer[0] = 0x00; // 无修饰键 ReportBuffer[1] = 0x00; // 保留 ReportBuffer[2] = 0x04; // 按键码 0x04 = 'A' ReportBuffer[3] = 0x00; ReportBuffer[4] = 0x00; ReportBuffer[5] = 0x00; ReportBuffer[6] = 0x00; ReportBuffer[7] = 0x00; *ReportLength = 8; return STATUS_SUCCESS; } return STATUS_UNSUCCESSFUL;}// 注册HID MinidriverBOOL RegisterVirtualHID() { HID_MINIDRIVER_REGISTRATION reg; ZeroMemory(&reg, sizeof(reg)); reg.Revision = HID_MINIDRIVER_REGISTRATION_REVISION; reg.DriverObject = NULL; // 用户模式不需要驱动对象 reg.RegistryPath = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\VirtualHIDKeyboard"; reg.MiniDriverExtension = NULL; // 设置回调函数 reg.GetReportDescriptor = MiniDriverGetReportDescriptor; reg.ReadReport = MiniDriverReadReport; // 其他回调可以设为NULL使用默认 NTSTATUS status = HidRegisterMinidriver(&reg); if (status == STATUS_SUCCESS) { printf("虚拟HID键盘注册成功!\n"); return TRUE; } else { printf("注册失败,错误码: 0x%08X\n", status); return FALSE; }}int main() { printf("虚拟HID键盘演示程序\n"); printf("需要管理员权限运行\n\n"); if (!RegisterVirtualHID()) { printf("按任意键退出..."); getchar(); return 1; } printf("设备已注册,按Enter键退出..."); getchar(); return 0;}这段代码展示了最基本的结构:定义报告描述符、实现两个关键回调(获取描述符和读取报告),然后通过HidRegisterMinidriver向系统注册。运行后,系统会识别出一个新的键盘设备,并持续调用MiniDriverReadReport读取按键状态。---## 五、代码实现二:动态按键发送与测试工具上面示例只能发送固定的"A"键,我们需要一个可以动态控制按键的版本。下面实现一个交互式程序,允许用户输入按键码并发送到系统。c#include <windows.h>#include <stdio.h>#include <conio.h>// 全局变量:当前键盘状态UCHAR g_KeyboardReport[8] = {0}; // 8字节报告缓冲区// 发送按键按下事件void SendKeyDown(UCHAR keyCode) { // 在6个按键槽中找到第一个空位 for (int i = 2; i < 8; i++) { // 前两个字节是修饰键和保留位 if (g_KeyboardReport[i] == 0) { g_KeyboardReport[i] = keyCode; break; } } // 注意:实际应用中需要通知系统报告已更新 // 这里简化处理,假设系统会定期轮询}// 发送按键释放事件void SendKeyUp(UCHAR keyCode) { for (int i = 2; i < 8; i++) { if (g_KeyboardReport[i] == keyCode) { g_KeyboardReport[i] = 0; break; } }}// 发送完整按键事件(按下+释放)void SendKeyPress(UCHAR keyCode) { SendKeyDown(keyCode); Sleep(50); // 模拟按键持续时间 SendKeyUp(keyCode);}// 修改后的读取回调,返回动态数据NTSTATUS __stdcall DynamicReadReport( PVOID Context, UCHAR ReportID, PHIDP_REPORT_TYPE ReportType, PUCHAR ReportBuffer, PULONG ReportLength) { if (ReportType == HidP_Input && ReportID == 0) { // 复制当前键盘状态到输出缓冲区 memcpy(ReportBuffer, g_KeyboardReport, 8); *ReportLength = 8; return STATUS_SUCCESS; } return STATUS_UNSUCCESSFUL;}// 修改注册函数,使用动态回调BOOL RegisterDynamicVirtualHID() { HID_MINIDRIVER_REGISTRATION reg; ZeroMemory(&reg, sizeof(reg)); reg.Revision = HID_MINIDRIVER_REGISTRATION_REVISION; reg.DriverObject = NULL; reg.RegistryPath = L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\VirtualHIDKeyboard"; reg.MiniDriverExtension = NULL; // 使用新的动态回调 reg.GetReportDescriptor = MiniDriverGetReportDescriptor; // 复用之前的 reg.ReadReport = DynamicReadReport; NTSTATUS status = HidRegisterMinidriver(&reg); if (status == STATUS_SUCCESS) { printf("动态虚拟键盘注册成功!\n"); return TRUE; } else { printf("注册失败,错误码: 0x%08X\n", status); return FALSE; }}// 主程序:交互式按键发送int main() { printf("=== 虚拟HID键盘交互测试程序 ===\n"); printf("需要管理员权限运行\n\n"); if (!RegisterDynamicVirtualHID()) { printf("按任意键退出..."); getchar(); return 1; } printf("\n可用命令:\n"); printf(" p <hex> - 发送按键按下(如 p 0x04 发送A键)\n"); printf(" r <hex> - 释放按键\n"); printf(" s <hex> - 发送完整的按下+释放\n"); printf(" q - 退出\n\n"); char cmd; UCHAR keyCode; while (1) { printf("> "); cmd = _getche(); printf("\n"); if (cmd == 'q' || cmd == 'Q') { break; } // 读取十六进制按键码 scanf("%x", &keyCode); switch (cmd) { case 'p': case 'P': SendKeyDown(keyCode); printf("按下按键 0x%02X\n", keyCode); break; case 'r': case 'R': SendKeyUp(keyCode); printf("释放按键 0x%02X\n", keyCode); break; case 's': case 'S': SendKeyPress(keyCode); printf("敲击按键 0x%02X\n", keyCode); break; default: printf("未知命令\n"); } // 清空输入缓冲区 while (_kbhit()) _getch(); } printf("程序退出\n"); return 0;}这个扩展版本引入了全局状态数组g_KeyboardReport,并提供了三个函数来动态修改按键状态。运行后,你可以输入命令发送特定按键码(例如p 04按下A键,r 04释放A键)。注意:实际部署时,需要确保回调函数线程安全,并使用同步机制(如互斥锁)保护全局状态。此外,系统轮询频率很高(通常每8ms一次),因此状态更新后系统会很快响应。---## 六、高级话题与注意事项### 1. UIPI与权限问题虚拟HID设备注册需要管理员权限,因为它在系统级别注册驱动。如果你的应用程序需要在用户模式下运行,可以考虑使用服务+用户程序通信的方式。### 2. 新版Windows的替代方案从Windows 10 19H1开始,微软引入了Virtual HID Framework (VHF) ,提供了更稳定的API。VHF使用VhfCreateVhfStart等函数,无需直接操作Minidriver。使用VHF的代码更简洁:c// VHF示例(需要Windows 10 19H1+)VHF_CONFIG config;VHF_CONFIG_INIT(&config, sizeof(KeyboardReportDescriptor), KeyboardReportDescriptor, &g_KeyboardReport, sizeof(g_KeyboardReport)); VHFHANDLE handle;VhfCreate(&config, &handle);VhfStart(handle);### 3. 安全考量虚拟HID设备可以模拟任何按键输入,包括系统快捷键(如Ctrl+Alt+Del)。恶意软件可能利用此功能,因此Windows对虚拟HID设备的创建有严格限制,需要数字签名或管理员权限。### 4. 性能优化实际应用中,你可能需要支持同时按下多个键、修饰键组合、甚至多媒体键。这需要扩展报告描述符,比如增加报告ID区分不同功能。---## 七、总结本文从HID协议的基本概念出发,解释了报告描述符的结构和键盘数据的编码方式,然后通过两个逐步深入的代码示例,演示了如何在Windows上使用HID Minidriver实现一个虚拟键盘设备。第一个示例展示了设备注册的最小框架,第二个示例增加了动态按键控制的功能,允许程序实时发送按键事件。最后,我们讨论了高级话题如VHF框架、权限问题和安全考量。虚拟HID设备是一个强大的工具,可以用于自动化测试、辅助功能开发、远程控制等场景。但使用时务必遵守安全规范,避免滥用。通过本文的学习,你已经具备了从零开始构建虚拟HID键盘的基础知识,可以进一步探索更复杂的实现,比如模拟鼠标、游戏手柄等设备。

相关推荐
伟大的前程2 小时前
unsigned int 定义的变量 在 电脑的 64 位系统中 和 32 位 cortex-m0 和 8 位的 51 单片机
单片机·嵌入式硬件
小渔村的拉线工2 小时前
1.HPM6E80 解析芯片整体工作原理和存储架构
单片机·嵌入式硬件·mcu·架构·hpm6e80
熙芯XiChip3 小时前
CAN与CANopen的本质区别
单片机
神罚天下ET4 小时前
典型arm位单片机启动流程(从上电到main.c)
c语言·arm开发·单片机
CODER03045 小时前
封装忽律底层硬件差异的自定义系统
windows·microsoft
软件资深者6 小时前
Windows10、11轻松设置 使用教程:把Windows繁杂的设置集成到一个面板,关广告/关更新/调隐私全搞定,系统设置工具新手 5 分钟上手(2026)
windows
脚踏实地皮皮晨6 小时前
003005002_WPF StackPanel 基类官方类定义逐行深度解析
开发语言·windows·算法·c#·wpf·visual studio
Zevalin爱灰灰6 小时前
模拟信号链电路——基于TL431的电压基准电路
单片机·嵌入式硬件
csdn杰哥7 小时前
瑞萨单片机开发教程[二十七] DTC 外部中断触发传输实验
单片机·嵌入式硬件·mongodb·瑞萨·ra6m5