找回密码
 立即注册
首页 业界区 业界 使用VHF框架实现一个虚拟HID键盘

使用VHF框架实现一个虚拟HID键盘

东新 昨天 17:06
前几天我通过改造微软的vhidmini2这个驱动示例,写了一个umdf的虚拟hid键盘,然后我发现,微软还提供了一个叫Virtual Hid Framework(VHF)的框架,专门用来实现虚拟hid设备,在kmdf和umdf上都支持(文档这么说的),所以就想着用VHF来重写一下上次的哪个虚拟hid键盘。
0 VHF概述

使用VHF开发的驱动程序叫做源驱动程序,源驱动程序的作用是控制VHF设备对象的生命周期,以及为VHF设备对象提供数据。下面这张官方文档中的设备树显示了它们之间的层次关系。
1.png

绿色框的FDO指的是源驱动程序,就是我们要开发的部分,开发时需要引用Vhfkm.lib(在umdf中是Vhfum.lib)来使用vhf提供的api。PDO是物理设备对象,通常是上级设备的FDO枚举出来的设备,对于虚拟设备来说,一般是通过devgen等方式生成的设备。PDO一般会显示在设备管理器中,未安装驱动时显示为Unknown Device,源驱动程序就是安装在这个设备上的。Vhf.sys是VHF框架的核心,作为LowerFilter安装在源驱动程序上,过滤PDO到源驱动程序之间的请求,这些请求一般是设备生命周期相关的请求,例如PNP事件等,与HID功能无关。Vhf.sys会枚举出一个PDO来,这个是实现虚拟HID设备功能的PDO,系统会在它上面安装HidClass的驱动。
VHF之于虚拟hid设备的开发,就像WPF之于桌面应用开发,虽然实现的自由度会稍微受限,但确实方便很多。使用VHF,不需要自己去处理复杂的IRP请求,其缓冲策略也有默认的实现,只需要设置好几个回调函数就行了,给我的感觉真的是像开发WPF应用一样。
虽然VHF用起来很方便,但是官方文档较少,示例代码也不完整,所以用起来还是遇到不少困难。我最初还是想用umdf的驱动来实现虚拟hid键盘,但实在是调不通,文档和示例代码大多是基于kmdf的,搞不懂到底是哪里有问题,所以就先实现了一个kmdf的。
使用VHF的步骤非常简单,就分为两步:1.编写初始化代码;2.编写处理请求的回调函数。
下面先介绍一下可能会用到的主要函数和数据结构,然后再说明如何编写实例代码。
1 初始化相关的函数和数据结构

初始化需要依次使用VHF_CONFIG_INIT、VhfCreate、VhfStart三个函数,这三个函数是不是光看名称就很容易理解?
1.1 VHF_CONFIG_INIT
  1. FORCEINLINE
  2. VOID
  3. VHF_CONFIG_INIT(
  4.     _Out_
  5.         PVHF_CONFIG     Config,
  6. #ifdef _KERNEL_MODE
  7.     _In_
  8.         PDEVICE_OBJECT  DeviceObject,
  9. #else
  10.     _In_
  11.         HANDLE          FileHandle,
  12. #endif
  13.     _In_
  14.         USHORT          ReportDescriptorLength,
  15.     _In_reads_bytes_(ReportDescriptorLength)
  16.         PUCHAR          ReportDescriptor   
  17.     )
复制代码
VHF_CONFIG_INIT函数的作用是初始化一个VHF_CONFIG的结构体,VHF_CONFIG结构体用来指定VHF框架对象的一些属性,例如PID、VID、回调函数指针等。
DeviceObject指定一个WDM设备对象与VHF关联,通常就是当前的设备对象。在kdmf中,可以通过WdfDeviceWdmGetDeviceObject来获取与WDF设备对象关联的WDM设备对象。
1.2 VhfCreate
  1. NTSTATUS VhfCreate(
  2.   [in]  PVHF_CONFIG VhfConfig,
  3.   [out] VHFHANDLE   *VhfHandle
  4. );
复制代码
VhfCreate函数的作用是使用刚刚初始化的VHF_CONFIG指定的配置,去创建一个VHF设备对象,调用成功的话,VhfHandle就是新创建的VHF设备对象的句柄。
1.3 VhfStart
  1. NTSTATUS VhfStart(
  2.   [in] VHFHANDLE VhfHandle
  3. );
复制代码
VhfStart函数的作用就是启动刚刚创建的VHF设备对象。
1.4 VhfDelete
  1. VOID VhfDelete(
  2.   [in] VHFHANDLE VhfHandle,
  3.   [in] BOOLEAN   Wait
  4. );
复制代码
在设备或驱动卸载之前,需要调用VhfDelete方法删除掉VHF设备对象。未正常删除VHF设备对象的话,系统会提示设备已更改,需要重启系统。
2 处理请求相关的函数和数据结构

2.1 EVT_VHF_ASYNC_OPERATION

源驱动程序可以支持这些异步请求:GetFeature、 SetFeature、 WriteReport、 GetInputReport。在VHF_CONFIG结构体中设置相应的回调函数:EvtVhfAsyncOperationGetFeature、EvtVhfAsyncOperationSetFeature、EvtVhfAsyncOperationWriteReport、EvtVhfAsyncOperationGetInputReport,然后在VHF处理这些请求时,就会调用这些回调。
这些回调的类型都是EVT_VHF_ASYNC_OPERATION,定义如下:
  1. EVT_VHF_ASYNC_OPERATION EvtVhfAsyncOperation;
  2. VOID EvtVhfAsyncOperation(
  3.   [in]           PVOID VhfClientContext,
  4.   [in]           VHFOPERATIONHANDLE VhfOperationHandle,
  5.   [in, optional] PVOID VhfOperationContext,
  6.   [in]           PHID_XFER_PACKET HidTransferPacket
  7. )
  8. {...}
复制代码
VhfClientContext是回调的上下文参数,是在初始化时通过VHF_CONFIG结构体设置的。VhfOperationHandle是这次异步操作的句柄,通常用于设置异步操作的结果。HidTransferPacket是请求报告的数据包。
2.2 VhfAsyncOperationComplete
  1. NTSTATUS VhfAsyncOperationComplete(
  2.   [in] VHFOPERATIONHANDLE VhfOperationHandle,
  3.   [in] NTSTATUS           CompletionStatus
  4. );
复制代码
当源驱动程序处理完异步请求之后,必须要用回调传入的VhfOperationHandle参数,调用VhfAsyncOperationComplete函数来设置此次异步请求的结果。
2.3 VhfReadReportSubmit
  1. NTSTATUS VhfReadReportSubmit(
  2.   [in] VHFHANDLE        VhfHandle,
  3.   [in] PHID_XFER_PACKET HidTransferPacket
  4. );
复制代码
源驱动程序可以通过VhfReadReportSubmit函数向VHF提交一个输入报告,然后由VHF决定何时将该报告提交给系统。
2.4 EVT_VHF_READY_FOR_NEXT_READ_REPORT

源驱动程序也可以自己决定何时将输入报告提交给系统。可以通过VHF_CONFIG的EvtVhfReadyForNextReadReport字段来设置一个EVT_VHF_READY_FOR_NEXT_READ_REPORT类型的回调,它的定义如下:
  1. EVT_VHF_READY_FOR_NEXT_READ_REPORT EvtVhfReadyForNextReadReport;
  2. VOID EvtVhfReadyForNextReadReport(
  3.   [in] PVOID VhfClientContext
  4. )
  5. {...}
复制代码
如果设置了EvtVhfReadyForNextReadReport回调,则当VHF准备好将缓冲区提交给系统时调用这个回调,然后由源驱动程序决定何时向缓冲区中填充输入报告。
源驱动程序仍然通过调用VhfReadReportSubmit来填充输入报告,一旦调用VhfReadReportSubmit后,VHF会尽快提交缓冲区,然后,直到下一次VHF调用EvtVhfReadyForNextReadReport回调后,源驱动程序才可以再次调用EvtVhfReadyForNextReadReport。
如果实现的是键盘、鼠标、触摸这类输入设备的话,一般而言,在启动VHF设备对象后会EvtVhfReadyForNextReadReport会立即被调用。
3 实例代码演示

还是以虚拟HID键盘为例,下面会从项目创建开始,完整演示一下用VHF框架实现虚拟HID设备的过程。
3.1 项目创建

这里通过VS2022来创建项目,在创建项目前需要先完整地安装好WDK,WDK怎么安装官方有详细的文档,这里就不讲了。
项目模板就选择Kernel Mode Driver(KMDF)或者Kernel Mode Driver, Empty(KMDF),如果选择空模板的话,就要自己实现DriverEntry等函数,这里我选了Kernel Mode Driver(KMDF)模板。
2.png

项目创建后,右键项目,点击属性,在项目属性面板中,选择链接器-输入,在附加依赖项中添加vhfkm.lib,然后在头文件中包含vhf.h
3.png

3.2 修改INF文件

Vhf.sys需要安装为原驱动程序的Lower Filter驱动,这可以通过INF文件来指定(仅限通过INF文件安装的情况)。模板中包含默认的INF文件,我们需要在INF文件的DDInstall.HW部分中添加一个AddReg指令(如果没有DDInstall.HW部分则添加一个),再添加一个对应的AddReg部分。类似下面这样:
  1. [vhfkeyboardkm_Device.NT.HW]
  2. AddReg = vhfkeyboardkm_Device.NT.AddReg
  3. [vhfkeyboardkm_Device.NT.AddReg]
  4. HKR,,"LowerFilters",0x00010000,"vhf"
复制代码
3.3 初始化VHF设备对象的代码

模板实现的是一个PNP样式的驱动程序,它在EvtDriverDeviceAdd事件中完成WDF设备对象的创建和初始化,我们在它创建WDF设备对象后初始化VHF设备对象。模板的代码如下:
  1. NTSTATUS
  2. vhfkeyboardkmEvtDeviceAdd(
  3.     _In_    WDFDRIVER       Driver,
  4.     _Inout_ PWDFDEVICE_INIT DeviceInit
  5.     )
  6. {
  7.     NTSTATUS status;
  8.     UNREFERENCED_PARAMETER(Driver);
  9.     PAGED_CODE();
  10.     TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");
  11.     status = vhfkeyboardkmCreateDevice(DeviceInit);
  12.     TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");
  13.     return status;
  14. }
  15. NTSTATUS
  16. vhfkeyboardkmCreateDevice(
  17.     _Inout_ PWDFDEVICE_INIT DeviceInit
  18.     )
  19. {
  20.     WDF_OBJECT_ATTRIBUTES deviceAttributes;
  21.     PDEVICE_CONTEXT deviceContext;
  22.     WDFDEVICE device;
  23.     NTSTATUS status;
  24.     VHF_CONFIG vhfConfig;
  25.     PDEVICE_OBJECT pdo;
  26.     PAGED_CODE();
  27.     WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
  28.     deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;
  29.     status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
  30.     if (NT_SUCCESS(status)) {
  31.         deviceContext = DeviceGetContext(device);
  32.         RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));
  33.         //status = WdfDeviceCreateDeviceInterface(
  34.         //    device,
  35.         //    &GUID_DEVINTERFACE_vhfkeyboardkm,
  36.         //    NULL // ReferenceString
  37.         //    );
  38.         //if (NT_SUCCESS(status)) {
  39.         //    //
  40.         //    // Initialize the I/O Package and any Queues
  41.         //    //
  42.         //    status = vhfkeyboardkmQueueInitialize(device);
  43.         //}
  44.     }
  45.     return status;
  46. }
复制代码
先把模板中创建设备接口和初始化事件队列的代码删掉,我们可以通过pid、vid来访问虚拟设备,暂时不需要设备接口,而事件主要由VHF处理,所以也暂时不需要事件队列。然后加入我们初始化VHF设备对象的代码:
  1. typedef struct _DEVICE_CONTEXT
  2. {
  3.     UCHAR Data[8];
  4.     VHFHANDLE VhfHandle;
  5. } DEVICE_CONTEXT, *PDEVICE_CONTEXT;
  6. NTSTATUS
  7. vhfkeyboardkmCreateDevice(
  8.     _Inout_ PWDFDEVICE_INIT DeviceInit
  9.     )
  10. {
  11.     WDF_OBJECT_ATTRIBUTES deviceAttributes;
  12.     PDEVICE_CONTEXT deviceContext;
  13.     WDFDEVICE device;
  14.     NTSTATUS status;
  15.     VHF_CONFIG vhfConfig;
  16.     PDEVICE_OBJECT pdo;
  17.     PAGED_CODE();
  18.     WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
  19.     deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;
  20.     status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);
  21.     if (NT_SUCCESS(status)) {
  22.         deviceContext = DeviceGetContext(device);
  23.         RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));
  24.         pdo = WdfDeviceWdmGetDeviceObject(device);
  25.         KdPrint(("Device Object Pointer: %p\n", pdo));
  26.         if (pdo == NULL)
  27.         {
  28.             KdPrint(("Invalid Device Object Pointer\n"));
  29.             return STATUS_INVALID_PARAMETER;
  30.         }
  31.         VHF_CONFIG_INIT(&vhfConfig, pdo, sizeof(G_DefaultReportDescriptor), G_DefaultReportDescriptor);
  32.         KdPrint(("VHF configuration initialized\n"));
  33.         vhfConfig.EvtVhfAsyncOperationWriteReport = EvtVhfWriteReport;
  34.         vhfConfig.VendorID = 0xDEED;
  35.         vhfConfig.ProductID = 0xFEED;
  36.         vhfConfig.VersionNumber = 0x101;
  37.         vhfConfig.VhfClientContext = device;
  38.         status = VhfCreate(&vhfConfig, &deviceContext->VhfHandle);
  39.         if (!NT_SUCCESS(status))
  40.         {
  41.             KdPrint(("VhfCreate failed with status: 0x%08x\n", status));
  42.             return status;
  43.         }
  44.         KdPrint(("VhfCreate succeeded\n"));
  45.         status = VhfStart(deviceContext->VhfHandle);
  46.         if (!NT_SUCCESS(status))
  47.         {
  48.             KdPrint(("VhfStart failed with status: 0x%08x\n", status));
  49.             return status;
  50.         }
  51.     }
  52.     return status;
  53. }
复制代码
简单来说就是先初始化VHF_CONFIG,然后设置回调和其他属性,注意这里将WDF设备对象device设置为回调的上下文,是为了在回调中获取WDF设备对象的设备上下文。然后调用VhfCreate创建VHF设备对象,创建出的VHF设备对象的句柄保存到WDF设备对象的设备上下文中。最后调用VhfStart启动VHF设备对象。
报告描述符是通过VHF_CONFIG传给VHF的,还是分为键盘(report_id=2)和Vendor definded(report_id=1)两个顶级集合,定义如下:
  1. UCHAR       G_DefaultReportDescriptor[] = {
  2.     0x06,0x00, 0xFF,                // USAGE_PAGE (Vender Defined Usage Page)
  3.     0x09,0x01,                      // USAGE (Vendor Usage 0x01)
  4.     0xA1,0x01,                      // COLLECTION (Application)
  5.     0x85,0x01,    // REPORT_ID (1)
  6.     0x09,0x01,                         // USAGE (Vendor Usage 0x01)
  7.     0x15,0x00,                         // LOGICAL_MINIMUM(0)
  8.     0x26,0xff, 0x00,                   // LOGICAL_MAXIMUM(255)
  9.     0x75,0x08,                         // REPORT_SIZE (0x08)
  10.     0x96,(FEATURE_REPORT_SIZE_CB & 0xff), (FEATURE_REPORT_SIZE_CB >> 8), // REPORT_COUNT
  11.     0xB1,0x00,                         // FEATURE (Data,Ary,Abs)
  12.     0x09,0x01,                         // USAGE (Vendor Usage 0x01)
  13.     0x75,0x08,                         // REPORT_SIZE (0x08)
  14.     0x96,(INPUT_REPORT_SIZE_CB & 0xff), (INPUT_REPORT_SIZE_CB >> 8), // REPORT_COUNT
  15.     0x81,0x00,                         // INPUT (Data,Ary,Abs)
  16.     0x09,0x01,                         // USAGE (Vendor Usage 0x01)
  17.     0x75,0x08,                         // REPORT_SIZE (0x08)
  18.     0x96,(OUTPUT_REPORT_SIZE_CB & 0xff), (OUTPUT_REPORT_SIZE_CB >> 8), // REPORT_COUNT
  19.     0x91,0x00,                         // OUTPUT (Data,Ary,Abs)
  20.     0xC0,                           // END_COLLECTION
  21.     0x05, 0x01,        // USAGE_PAGE (Generic Desktop)
  22.     0x09, 0x06,        // USAGE (Keyboard)
  23.     0xA1, 0x01,        // COLLECTION (Application)
  24.     0x85, 0x02,    // REPORT_ID (2)
  25.     0x05, 0x07,        //   USAGE_PAGE (Keyboard)
  26.     0x19, 0xE0,        //   USAGE_MINIMUM (Left Control)
  27.     0x29, 0xE7,        //   USAGE_MAXIMUM (Right GUI)
  28.     0x15, 0x00,        //   LOGICAL_MINIMUM (0)
  29.     0x25, 0x01,        //   LOGICAL_MAXIMUM (1)
  30.     0x75, 0x01,        //   REPORT_SIZE (1)
  31.     0x95, 0x08,        //   REPORT_COUNT (8)
  32.     0x81, 0x02,        //   INPUT (Data, Var, Abs)
  33.     0x95, 0x01,        //   REPORT_COUNT (1)
  34.     0x75, 0x08,        //   REPORT_SIZE (8)
  35.     0x81, 0x03,        //   INPUT (Const, Var, Abs)
  36.     0x95, 0x06,        //   REPORT_COUNT (6)
  37.     0x75, 0x08,        //   REPORT_SIZE (8)
  38.     0x15, 0x00,        //   LOGICAL_MINIMUM (0)
  39.     0x25, 0x65,        //   LOGICAL_MAXIMUM (101)
  40.     0x05, 0x07,        //   USAGE_PAGE (Keyboard)
  41.     0x19, 0x00,        //   USAGE_MINIMUM (No Event)
  42.     0x29, 0x65,        //   USAGE_MAXIMUM (Keyboard Application)
  43.     0x81, 0x00,        //   INPUT (Data, Ary, Abs)
  44.     0xC0,               // END_COLLECTION
  45. };
复制代码
3.4 实现键盘功能

这里我们还是实现下面这个功能:
应用程序向源驱动程序发送一个键值,然后虚拟键盘就模拟这个键的按下,如果发送0,则模拟抬起。
3.4.1 使用默认缓冲策略

由于请求处理和缓冲策略都是VHF实现的,所以我们要做的很少,只需要注册EvtVhfAsyncOperationWriteReport的回调来接收应用程序发送的键值,然后调用VhfReadReportSubmit提交一个输入报告就可以了,回调函数的代码如下:
  1. typedef struct _HIDMINI_KBD_INPUT_REPORT {
  2.     UCHAR ReportId;
  3.     UCHAR Data[8];
  4. } HIDMINI_KBD_INPUT_REPORT, * PHIDMINI_KBD_INPUT_REPORT;
  5. typedef struct _HIDMINI_OUTPUT_REPORT {
  6.     UCHAR ReportId;
  7.     UCHAR Data;
  8.     USHORT Pad1;
  9.     ULONG Pad2;
  10. } HIDMINI_OUTPUT_REPORT, * PHIDMINI_OUTPUT_REPORT;
  11. VOID EvtVhfWriteReport(
  12.     _In_           PVOID VhfClientContext,
  13.     _In_           VHFOPERATIONHANDLE VhfOperationHandle,
  14.     _In_           PVOID VhfOperationContext,
  15.     _In_           PHID_XFER_PACKET HidTransferPacket
  16. )
  17. {
  18.     ULONG reportSize;
  19.     NTSTATUS status;
  20.     PHIDMINI_OUTPUT_REPORT  outputReport;
  21.     PDEVICE_CONTEXT deviceContext;
  22.     HID_XFER_PACKET inputPacket;
  23.     HIDMINI_KBD_INPUT_REPORT inputReport;
  24.     UNREFERENCED_PARAMETER(VhfOperationContext);
  25.     status = STATUS_SUCCESS;
  26.     deviceContext = DeviceGetContext(VhfClientContext);
  27.     if (HidTransferPacket->reportId != 1)
  28.     {
  29.         status = STATUS_INVALID_PARAMETER;
  30.         KdPrint(("WriteReport: unkown report id %d\n", HidTransferPacket->reportId));
  31.         goto Exit;
  32.     }
  33.    
  34.     reportSize = sizeof(HIDMINI_OUTPUT_REPORT);
  35.     if (HidTransferPacket->reportBufferLen < reportSize) {
  36.         status = STATUS_INVALID_BUFFER_SIZE;
  37.         KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n",
  38.             HidTransferPacket->reportBufferLen, reportSize));
  39.         goto Exit;
  40.     }
  41.     outputReport = (PHIDMINI_OUTPUT_REPORT)HidTransferPacket->reportBuffer;
  42.     TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "data:%08x\n", outputReport->Data);
  43.     RtlZeroMemory(&inputReport, sizeof(HIDMINI_KBD_INPUT_REPORT));
  44.     inputReport.ReportId = 2;
  45.     inputReport.Data[2] = outputReport->Data;
  46.     inputPacket.reportBuffer = (PUCHAR)&inputReport;
  47.     inputPacket.reportBufferLen = sizeof(HIDMINI_KBD_INPUT_REPORT);
  48.     inputPacket.reportId = 2;
  49.     VhfReadReportSubmit(deviceContext->VhfHandle, &inputPacket);
  50. Exit:
  51.     VhfAsyncOperationComplete(VhfOperationHandle, status);
  52. }
复制代码
这个函数从入参的PHID_XFER_PACKET中获取了应用程序传进来的键值,然后创建一个新的PHID_XFER_PACKET来填充输入报告,再调用VhfReadReportSubmit提交输入报告给VHF,最后调用VhfAsyncOperationComplete来设置回调的结果。
注意这里调用VhfReadReportSubmit时用到的VhfHandle是我们之前保存在WDF设备对象的设备上下文中的,先前我们将WDF的设备对象设置为回调的上下文参数,也就是VhfClientContext,所以在这里可以通过DeviceGetContext来获取设备上下文。
3.4.2 使用EVT_VHF_READY_FOR_NEXT_READ_REPORT控制缓冲方案

上面的例子我们使用的默认的缓冲策略,通过VhfReadReportSubmit提交输入报告后,由VHF决定何时将输入报告提交给系统。
也可以使用EVT_VHF_READY_FOR_NEXT_READ_REPORT来控制缓冲策略,这需要在初始化VHF设备对象时注册EvtVhfReadyForNextReadReport回调。当VHF调用EvtVhfReadyForNextReadReport回调时,意味着源驱动程序可以通过VhfReadReportSubmit向VHF提交一次输入报告,然后VHF会尽快将输入报告提交给系统。
使用EVT_VHF_READY_FOR_NEXT_READ_REPORT的代码时,需要编写EvtVhfReadyForNextReadReport回调,然后修改EvtVhfWriteReport和DEVICE_CONTEXT的代码
  1. typedef struct _DEVICE_CONTEXT
  2. {
  3.     BOOLEAN Ready;
  4.     UCHAR Data[8];
  5.     VHFHANDLE VhfHandle;
  6. } DEVICE_CONTEXT, *PDEVICE_CONTEXT;
  7. VOID EvtVhfReadyForNextReadReport(
  8.     _In_ PVOID VhfClientContext
  9. )
  10. {
  11.     PDEVICE_CONTEXT deviceContext = DeviceGetContext(VhfClientContext);
  12.     KdPrint(("EvtVhfReadyForNextReadReport...Entry\n"));
  13.     deviceContext->Ready = TRUE;
  14. }
  15. VOID EvtVhfWriteReport(
  16.     _In_           PVOID VhfClientContext,
  17.     _In_           VHFOPERATIONHANDLE VhfOperationHandle,
  18.     _In_           PVOID VhfOperationContext,
  19.     _In_           PHID_XFER_PACKET HidTransferPacket
  20. )
  21. {
  22.     ULONG reportSize;
  23.     NTSTATUS status;
  24.     PHIDMINI_OUTPUT_REPORT  outputReport;
  25.     PDEVICE_CONTEXT deviceContext;
  26.     HID_XFER_PACKET inputPacket;
  27.     HIDMINI_KBD_INPUT_REPORT inputReport;
  28.     UNREFERENCED_PARAMETER(VhfOperationContext);
  29.     status = STATUS_SUCCESS;
  30.     deviceContext = DeviceGetContext(VhfClientContext);
  31.     if (HidTransferPacket->reportId != CONTROL_COLLECTION_REPORT_ID)
  32.     {
  33.         status = STATUS_INVALID_PARAMETER;
  34.         KdPrint(("WriteReport: unkown report id %d\n", HidTransferPacket->reportId));
  35.         goto Exit;
  36.     }
  37.    
  38.     reportSize = sizeof(HIDMINI_OUTPUT_REPORT);
  39.     if (HidTransferPacket->reportBufferLen < reportSize) {
  40.         status = STATUS_INVALID_BUFFER_SIZE;
  41.         KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n",
  42.             HidTransferPacket->reportBufferLen, reportSize));
  43.         goto Exit;
  44.     }
  45.     outputReport = (PHIDMINI_OUTPUT_REPORT)HidTransferPacket->reportBuffer;
  46.     TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "data:%08x\n", outputReport->Data);
  47.     deviceContext->Data[2] = outputReport->Data;
  48.     RtlZeroMemory(&inputReport, sizeof(HIDMINI_KBD_INPUT_REPORT));
  49.     if (deviceContext->Ready)
  50.     {
  51.         deviceContext->Ready = FALSE;
  52.         inputReport.ReportId = 2;
  53.         inputReport.Data[2] = deviceContext->Data[2];
  54.         inputPacket.reportBuffer = (PUCHAR)&inputReport;
  55.         inputPacket.reportBufferLen = sizeof(HIDMINI_KBD_INPUT_REPORT);
  56.         inputPacket.reportId = 2;
  57.         status = VhfReadReportSubmit(deviceContext->VhfHandle, &inputPacket);
  58.         if (!NT_SUCCESS(status))
  59.         {
  60.             KdPrint(("VhfReadReportSubmit failed: 0x%08x", status));
  61.         }
  62.     }
  63. Exit:
  64.     VhfAsyncOperationComplete(VhfOperationHandle, status);
  65. }
复制代码
这里在DEVICE_CONTEXT结构体中新增了一个Ready字段,当EvtVhfReadyForNextReadReport被调用时,将它设为TRUE。然后当EvtVhfWriteReport被调用时,首先检查Ready字段,如果为TRUE,则提交输入报告。
像键盘这类输入设备,系统会持续请求输入报告,所以正常情况下,提交输入报告后,VHF就会马上再次调用EvtVhfReadyForNextReadReport。
3.5 驱动安装方法

将vhfkeyboardkm.sys, vhfkeyboardkm.inf, vhfkeyboardkm.cat三个文件拷贝到需要安装的设备上,使用管理员权限的cmd命令行输入
  1. devcon.exe install vhfkeyboardkm.inf Root\vhfkeyboardkm
复制代码
上面的vhfkeyboardkm默认情况下应替换为项目名称。devcon.exe会随WDK一起安装,如果找不到可以全局搜索一下。
如果是测试签名,需要设备开启测试模式才能安装,开启测试模式的方法是,在管理员权限的cmd命令行输入
  1. bcdedit.exe /set testsigning on
复制代码
然后重启。
3.6 应用程序

应用程序的话还是用这份代码,这份代码监听Alt/Ctrl+字母键的全局快捷键,在Alt+字母键时,向源驱动程序发送该字母键的键值,在Ctrl+字母键时,向源驱动程序发送0. 最后实现的效果跟上一次使用hidmini2实例改造的驱动程序效果一样。
  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. }
复制代码
4 在UMDF中使用VHF

据VHF的文档描述,VHF是支持UMDF的,其使用方法和KMDF也大差不差,只是在初始化的时候不太一样。
在前面介绍VHF_CONFIG_INIT函数的时候,可以看到函数定义里有一个条件编译,这个条件编译指示,在KMDF中,第二个参数应传入WDM设备对象,而在UMDF中,第二个参数应传入一个IoTarget.
据文档描述,这里的IoTarget应该是通过WdfIoTargetCreate创建,然后通过WdfIoTargetOpen打开的。但我调试了很久没有调通,实在搞不明白IoTarget应该关联什么样一个对象,总是在WdfIoTargetOpen这里报错。

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