找回密码
 立即注册
首页 业界区 业界 从0.99到1实现一个Windows上的虚拟hid键盘设备 ...

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

炳裘垦 4 天前
在虚拟机、远程控制、或者诸如云电脑之类的应用中,我们经常能够看到虚拟设备的身影。对于初学者来说,从0到1实现一个虚拟设备或许非常困难,但从0.99到1改造一个虚拟设备就简单多了。本文根据微软提供的UMDF版本HID minidriver的示例代码,稍加改造,将其变成一个虚拟HID键盘设备。
HID minidriver sample

微软提供的这个示例代码的仓库地址是:https://github.com/microsoft/Windows-driver-samples/tree/main/hid/vhidmini2
这个示例代码实现了一个hid驱动程序和一个客户端,演示了客户端如何与hid驱动程序通信。我想将其改造成一个HID键盘设备,通过客户端与其通信,控制其按键。
第一步 修改HID报告描述符

原代码的HID报告描述符包含了一个Vender Usage的顶级集合,集合里定义了一个HID报告,用于客户端和驱动程序的通信。这个集合可以保留下来继续用来通信。
要实现HID键盘的功能,需要一个Keyboard的顶级集合,下面是一个常见的HID键盘的报告描述符,我把LED相关的输出报告删除了,仅保留了按键相关的输入报告。报告描述符直接添加到原来的报告描述符的尾部即可。
  1. HID_REPORT_DESCRIPTOR       G_DefaultReportDescriptor[] = {
  2.     //......
  3.     //原本的报告描述符
  4.     //......
  5.     0x05, 0x01,        // USAGE_PAGE (Generic Desktop)
  6.     0x09, 0x06,        // USAGE (Keyboard)
  7.     0xA1, 0x01,        // COLLECTION (Application)
  8.     0x85, 0x02,        // REPORT_ID (2)
  9.     0x05, 0x07,        //   USAGE_PAGE (Keyboard)
  10.     0x19, 0xE0,        //   USAGE_MINIMUM (Left Control)
  11.     0x29, 0xE7,        //   USAGE_MAXIMUM (Right GUI)
  12.     0x15, 0x00,        //   LOGICAL_MINIMUM (0)
  13.     0x25, 0x01,        //   LOGICAL_MAXIMUM (1)
  14.     0x75, 0x01,        //   REPORT_SIZE (1)
  15.     0x95, 0x08,        //   REPORT_COUNT (8)
  16.     0x81, 0x02,        //   INPUT (Data, Var, Abs)
  17.     0x95, 0x01,        //   REPORT_COUNT (1)
  18.     0x75, 0x08,        //   REPORT_SIZE (8)
  19.     0x81, 0x03,        //   INPUT (Const, Var, Abs)
  20.     0x95, 0x06,        //   REPORT_COUNT (6)
  21.     0x75, 0x08,        //   REPORT_SIZE (8)
  22.     0x15, 0x00,        //   LOGICAL_MINIMUM (0)
  23.     0x25, 0x65,        //   LOGICAL_MAXIMUM (101)
  24.     0x05, 0x07,        //   USAGE_PAGE (Keyboard)
  25.     0x19, 0x00,        //   USAGE_MINIMUM (No Event)
  26.     0x29, 0x65,        //   USAGE_MAXIMUM (Keyboard Application)
  27.     0x81, 0x00,        //   INPUT (Data, Ary, Abs)
  28.     0xC0,              // END_COLLECTION
  29. };
复制代码
添加了报告描述符后,安装一下看下效果,安装的方法是,在管理员权限的cmd中,执行以下命令
  1. devcon.exe install "VhidminiUm.inf" root\VhidminiUm
复制代码
或者
  1. devgen.exe /add /bus root /hardwareid root\VhidminiUm
  2. pnputil /add-driver "VhidminiUm.inf" /install
复制代码
以上两种方式都是创建一个硬件ID为root\VhidminiUm的设备,然后将我们编译的驱动安装到设备上。devcon和devgen可以在WDK的工具目录下找到,pnputil是全局安装的内置工具。因为这是一个UMDF驱动程序,所以不要进入测试模式也可以安装。
安装完成后可以在设备管理器看到一个HID minidriver设备,在它下面有一个键盘设备和一个Vendor-defined设备,对应HID报告描述符中的两个顶级集合。
1.png

第二步 修改IOCTL_HID_READ_REPORT请求的处理函数

现在,这个键盘设备还不会上报任何按键。要让键盘设备上报按键,需要在IOCTL_HID_READ_REPORT请求的处理函数中提供正确的HID报告。
根据HID报告描述符,HID报告包含报告ID在内应该有9个字节
byte(s)description0报告ID(2)1每个bit代表一个控制键,1表示按下2保留3~8每个字节表示一个按下的按键例如,下面这个HID报告表示'A'键处于按下状态
  1. [0x02, 0, 0, 0x04, 0, 0, 0, 0, 0]
复制代码
在接收到IOCTL_HID_READ_REPORT请求的时候,把上面的数据复制到输出缓冲区,就可以模拟'A'键按下的状态。在后续的IOCTL_HID_READ_REPORT请求中,我们再将下面的数据复制到输出缓冲区,就可以模拟'A'键抬起的状态
  1. [0x02, 0, 0, 0, 0, 0, 0, 0, 0]
复制代码
在原始的示例代码中,IOCTL_HID_READ_REPORT请求没有直接在处理函数中进行处理,而是传递给另一个请求队列,然后由一个定时器去轮询,当这个队列中有请求时就处理这个请求。这模拟的是真实设备中:

  • 请求达到时设备还没有准备好数据,请求处理未完成状态
  • 一定时间后(也就是定时器到期后),数据准备好了,填充数据,完成请求
改造后的虚拟HID键盘设备中,这个逻辑可以保留,也可以删除。

  • 如果保留的话,将定时器的处理函数EvtTimerFunc修改为
  1. void
  2. EvtTimerFunc(
  3.     _In_  WDFTIMER          Timer
  4.     )
  5. {
  6.     NTSTATUS                status;
  7.     WDFQUEUE                queue;
  8.     PMANUAL_QUEUE_CONTEXT   queueContext;
  9.     WDFREQUEST              request;
  10.     HIDMINI_KBD_INPUT_REPORT    readReport;
  11.     KdPrint(("EvtTimerFunc\n"));
  12.     queue = (WDFQUEUE)WdfTimerGetParentObject(Timer);
  13.     queueContext = GetManualQueueContext(queue);
  14.     //
  15.     // see if we have a request in manual queue
  16.     //
  17.     status = WdfIoQueueRetrieveNextRequest(
  18.                             queueContext->Queue,
  19.                             &request);
  20.     if (NT_SUCCESS(status)) {
  21.         memset(&readReport, 0, sizeof(readReport));
  22.         readReport.ReportId = 2;
  23.         readReport.Data[2] = 0x04; //'A'键
  24.         status = RequestCopyFromBuffer(request,
  25.                             &readReport,
  26.                             sizeof(readReport));
  27.         WdfRequestComplete(request, status);
  28.     }
  29. }
复制代码
其中,_HIDMINI_KBD_INPUT_REPORT是在common.h中新增的结构体,用来表示键盘设备的HID输入报告,代码如下
  1. typedef struct _HIDMINI_KBD_INPUT_REPORT {
  2.     UCHAR ReportId;
  3.     UCHAR Data[8];
  4. } HIDMINI_KBD_INPUT_REPORT, * PHIDMINI_KBD_INPUT_REPORT;
复制代码

  • 如果不需要这个定时器逻辑的话,就将ReadReport函数改为
  1. NTSTATUS
  2. ReadReport(
  3.     _In_  PQUEUE_CONTEXT    QueueContext,
  4.     _In_  WDFREQUEST        Request,
  5.     _Always_(_Out_)
  6.           BOOLEAN*          CompleteRequest
  7.     )
  8. {
  9.     NTSTATUS                status;
  10.     HIDMINI_KBD_INPUT_REPORT    readReport;
  11.     UNREFERENCED_PARAMETER(CompleteRequest);
  12.     KdPrint(("ReadReport\n"));
  13.     memset(&readReport, 0, sizeof(readReport));
  14.     readReport.ReportId = 2;
  15.     readReport.Data[2] = 0x4;
  16.     status = RequestCopyFromBuffer(Request,
  17.         &readReport,
  18.         sizeof(readReport));
  19.     return status;
  20. }
复制代码
修改完代码后,再次安装,就会发现,彷佛有一个键盘一直在长按'A'键。
第三步 使用客户端控制按键的状态

现在这个驱动一旦安装之后,就会马上按下'A'键,并且不会抬起,因此需要一个客户端来控制按哪个键,什么时候按下,什么时候抬起。
原来的代码定义了一个vendor defined设备,这个设备的输出报告中可以传递一个字节的数据,那么客户端就可以利用这个字节将需要按下的HID键码发送给设备。如果要控制多个按键的话,就需要修改这个输出报告,本文先讨论单个按键的情况。
客户端通过IOCTL_HID_WRITE_REPORT请求将输出报告发送到设备,输出报告携带了一个字节的数据,在该请求的处理函数WriteReport中,这个字节的数据被保存到了设备上下文的自定义字段中,代码如下
  1. QueueContext->DeviceContext->DeviceData = outputReport->Data;
复制代码
这是原始的代码,不需要修改,需要修改的是第二步中IOCTL_HID_READ_REPORT请求的处理函数,将固定填充'A'键键码的代码,改成用DeviceData来填充
  1. readReport.Data[2] = QueueContext->DeviceContext->DeviceData;
复制代码
这样一来,客户端向设备发送哪个键的HID键码,设备就会模拟哪个键的按下,客户端如果发送0,设备就会模拟按键抬起。
下面是一个用WPF实现的客户端的代码,这个客户端实现的功能是

  • 当按下Alt+字母键的组合键时,让虚拟设备模拟该字母键长按
  • 当按下Alt+F1的组合键时,让虚拟设备模拟按键抬起
  1. using System.ComponentModel;
  2. using System.Runtime.InteropServices;
  3. using System.Windows;
  4. using System.Windows.Input;
  5. using System.Windows.Interop;
  6. using HidSharp;
  7. namespace VirtualHidKbdClient
  8. {
  9.     /// <summary>
  10.     /// Interaction logic for MainWindow.xaml
  11.     /// </summary>
  12.     public partial class MainWindow : Window
  13.     {
  14.         private const int AltKeyEventId = 9000;
  15.         private bool PressedKey = false;
  16.         private HidStream _stream;
  17.         [DllImport("user32.dll")]
  18.         public static extern uint MapVirtualKey(uint uCode, uint type);
  19.         [DllImport("user32.dll")]
  20.         public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
  21.         [DllImport("user32.dll")]
  22.         public static extern bool UnregisterHotKey(IntPtr hWnd, int id);
  23.         public MainWindow()
  24.         {
  25.             InitializeComponent();
  26.             SourceInitialized += MainWindow_SourceInitialized;
  27.         }
  28.         private void MainWindow_SourceInitialized(object? sender, EventArgs e)
  29.         {
  30.             var handle = new WindowInteropHelper(this).Handle;
  31.             var source = HwndSource.FromHwnd(handle);
  32.             source?.AddHook(HwndHook);
  33.             //注册Alt+F1和Alt+字母键的全局快捷键
  34.             for (int i = (int)Key.A; i <= (int)Key.Z; i++)
  35.             {
  36.                 RegisterHotKey(handle, AltKeyEventId, (uint)ModifierKeys.Alt, (uint)KeyInterop.VirtualKeyFromKey((Key)i));
  37.             }
  38.             RegisterHotKey(handle, AltKeyEventId, (uint)ModifierKeys.Alt, (uint)KeyInterop.VirtualKeyFromKey(Key.F1));
  39.             //找到虚拟设备下面的vendor defined设备
  40.             var devices = DeviceList.Local.GetHidDevices(0xDEED, 0xFEED);
  41.             foreach (var d in devices)
  42.             {
  43.                 Console.WriteLine(d.DevicePath);
  44.             }
  45.             var device = devices.FirstOrDefault(d => !d.DevicePath.EndsWith("kbd"));
  46.             _stream = device.Open();
  47.         }
  48.         private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
  49.         {
  50.             const int wmHotkey = 0x0312;
  51.             switch (msg)
  52.             {
  53.                 case wmHotkey:
  54.                     switch (wParam.ToInt32())
  55.                     {
  56.                         case AltKeyEventId:
  57.                             var lp = lParam.ToInt32();
  58.                             var virtualKey = lp >> 16;
  59.                             var key = KeyInterop.KeyFromVirtualKey(virtualKey);
  60.                             //虚拟键码到HID键码的转换,字母键是连续的,所以可以简单写成这样
  61.                             var hidKey = virtualKey - 61;
  62.                             var charKey = (char)MapVirtualKey((uint)virtualKey, 2);
  63.                             if (key == Key.F1 && PressedKey)
  64.                             {
  65.                                 _stream.Write([1, 0]);
  66.                                 MessageBox.Show($"cancel long press");
  67.                                 PressedKey = false;
  68.                             }
  69.                             else if(key != Key.F1 && !PressedKey)
  70.                             {
  71.                                 PressedKey = true;
  72.                                 _stream.Write([1, (byte)hidKey]);
  73.                                 MessageBox.Show($"long press {charKey}");
  74.                             }
  75.                             break;
  76.                     }
  77.                     break;
  78.             }
  79.             return IntPtr.Zero;
  80.         }
  81.         protected override void OnClosing(CancelEventArgs e)
  82.         {
  83.             base.OnClosing(e);
  84.             var handle = new WindowInteropHelper(this).Handle;
  85.             //关闭窗口时取消注册
  86.             UnregisterHotKey(handle, AltKeyEventId);
  87.             //关闭窗口时控制虚拟设备按键抬起
  88.             _stream.Write([1, 0]);
  89.             _stream.Close();
  90.         }
  91.     }
  92. }
复制代码
最后的效果如下
2.gif


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册