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

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

0 VHF概述

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

绿色框的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

复制代码
FORCEINLINE
VOID
VHF_CONFIG_INIT(
    _Out_
        PVHF_CONFIG     Config,
#ifdef _KERNEL_MODE
    _In_
        PDEVICE_OBJECT  DeviceObject,
#else
    _In_
        HANDLE          FileHandle,
#endif
    _In_
        USHORT          ReportDescriptorLength,
    _In_reads_bytes_(ReportDescriptorLength)
        PUCHAR          ReportDescriptor    
    )

VHF_CONFIG_INIT函数的作用是初始化一个VHF_CONFIG的结构体,VHF_CONFIG结构体用来指定VHF框架对象的一些属性,例如PID、VID、回调函数指针等。

DeviceObject指定一个WDM设备对象与VHF关联,通常就是当前的设备对象。在kdmf中,可以通过WdfDeviceWdmGetDeviceObject来获取与WDF设备对象关联的WDM设备对象。

1.2 VhfCreate

复制代码
NTSTATUS VhfCreate(
  [in]  PVHF_CONFIG VhfConfig,
  [out] VHFHANDLE   *VhfHandle
);

VhfCreate函数的作用是使用刚刚初始化的VHF_CONFIG指定的配置,去创建一个VHF设备对象,调用成功的话,VhfHandle就是新创建的VHF设备对象的句柄。

1.3 VhfStart

复制代码
NTSTATUS VhfStart(
  [in] VHFHANDLE VhfHandle
);

VhfStart函数的作用就是启动刚刚创建的VHF设备对象。

1.4 VhfDelete

复制代码
VOID VhfDelete(
  [in] VHFHANDLE VhfHandle,
  [in] BOOLEAN   Wait
);

在设备或驱动卸载之前,需要调用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,定义如下:

复制代码
EVT_VHF_ASYNC_OPERATION EvtVhfAsyncOperation;

VOID EvtVhfAsyncOperation(
  [in]           PVOID VhfClientContext,
  [in]           VHFOPERATIONHANDLE VhfOperationHandle,
  [in, optional] PVOID VhfOperationContext,
  [in]           PHID_XFER_PACKET HidTransferPacket
)
{...}

VhfClientContext是回调的上下文参数,是在初始化时通过VHF_CONFIG结构体设置的。VhfOperationHandle是这次异步操作的句柄,通常用于设置异步操作的结果。HidTransferPacket是请求报告的数据包。

2.2 VhfAsyncOperationComplete

复制代码
NTSTATUS VhfAsyncOperationComplete(
  [in] VHFOPERATIONHANDLE VhfOperationHandle,
  [in] NTSTATUS           CompletionStatus
);

当源驱动程序处理完异步请求之后,必须要用回调传入的VhfOperationHandle参数,调用VhfAsyncOperationComplete函数来设置此次异步请求的结果。

2.3 VhfReadReportSubmit

复制代码
NTSTATUS VhfReadReportSubmit(
  [in] VHFHANDLE        VhfHandle,
  [in] PHID_XFER_PACKET HidTransferPacket
);

源驱动程序可以通过VhfReadReportSubmit函数向VHF提交一个输入报告,然后由VHF决定何时将该报告提交给系统。

2.4 EVT_VHF_READY_FOR_NEXT_READ_REPORT

源驱动程序也可以自己决定何时将输入报告提交给系统。可以通过VHF_CONFIG的EvtVhfReadyForNextReadReport字段来设置一个EVT_VHF_READY_FOR_NEXT_READ_REPORT类型的回调,它的定义如下:

复制代码
EVT_VHF_READY_FOR_NEXT_READ_REPORT EvtVhfReadyForNextReadReport;

VOID EvtVhfReadyForNextReadReport(
  [in] PVOID VhfClientContext
)
{...}

如果设置了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)模板。

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

3.2 修改INF文件

Vhf.sys需要安装为原驱动程序的Lower Filter驱动,这可以通过INF文件来指定(仅限通过INF文件安装的情况)。模板中包含默认的INF文件,我们需要在INF文件的DDInstall.HW部分中添加一个AddReg指令(如果没有DDInstall.HW部分则添加一个),再添加一个对应的AddReg部分。类似下面这样:

复制代码
[vhfkeyboardkm_Device.NT.HW]
AddReg = vhfkeyboardkm_Device.NT.AddReg

[vhfkeyboardkm_Device.NT.AddReg]
HKR,,"LowerFilters",0x00010000,"vhf"

3.3 初始化VHF设备对象的代码

模板实现的是一个PNP样式的驱动程序,它在EvtDriverDeviceAdd事件中完成WDF设备对象的创建和初始化,我们在它创建WDF设备对象后初始化VHF设备对象。模板的代码如下:

复制代码
NTSTATUS
vhfkeyboardkmEvtDeviceAdd(
    _In_    WDFDRIVER       Driver,
    _Inout_ PWDFDEVICE_INIT DeviceInit
    )
{
    NTSTATUS status;

    UNREFERENCED_PARAMETER(Driver);

    PAGED_CODE();

    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Entry");

    status = vhfkeyboardkmCreateDevice(DeviceInit);

    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "%!FUNC! Exit");

    return status;
}

NTSTATUS
vhfkeyboardkmCreateDevice(
    _Inout_ PWDFDEVICE_INIT DeviceInit
    )
{
    WDF_OBJECT_ATTRIBUTES deviceAttributes;
    PDEVICE_CONTEXT deviceContext;
    WDFDEVICE device;
    NTSTATUS status;
    VHF_CONFIG vhfConfig;
    PDEVICE_OBJECT pdo;

    PAGED_CODE();

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
    deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;

    status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);

    if (NT_SUCCESS(status)) {

        deviceContext = DeviceGetContext(device); 

        RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));

        //status = WdfDeviceCreateDeviceInterface(
        //    device,
        //    &GUID_DEVINTERFACE_vhfkeyboardkm,
        //    NULL // ReferenceString
        //    );

        //if (NT_SUCCESS(status)) {
        //    //
        //    // Initialize the I/O Package and any Queues
        //    //
        //    status = vhfkeyboardkmQueueInitialize(device);
        //}
    }

    return status;
}

先把模板中创建设备接口和初始化事件队列的代码删掉,我们可以通过pid、vid来访问虚拟设备,暂时不需要设备接口,而事件主要由VHF处理,所以也暂时不需要事件队列。然后加入我们初始化VHF设备对象的代码:

复制代码
typedef struct _DEVICE_CONTEXT
{
    UCHAR Data[8];

    VHFHANDLE VhfHandle;

} DEVICE_CONTEXT, *PDEVICE_CONTEXT;

NTSTATUS
vhfkeyboardkmCreateDevice(
    _Inout_ PWDFDEVICE_INIT DeviceInit
    )
{
    WDF_OBJECT_ATTRIBUTES deviceAttributes;
    PDEVICE_CONTEXT deviceContext;
    WDFDEVICE device;
    NTSTATUS status;
    VHF_CONFIG vhfConfig;
    PDEVICE_OBJECT pdo;

    PAGED_CODE();

    WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&deviceAttributes, DEVICE_CONTEXT);
    deviceAttributes.EvtCleanupCallback = EVT_CONTEXT_CLEANUP;
    status = WdfDeviceCreate(&DeviceInit, &deviceAttributes, &device);

    if (NT_SUCCESS(status)) {
        deviceContext = DeviceGetContext(device);

        RtlZeroMemory(deviceContext, sizeof(DEVICE_CONTEXT));

        pdo = WdfDeviceWdmGetDeviceObject(device);
        KdPrint(("Device Object Pointer: %p\n", pdo));
        if (pdo == NULL)
        {
            KdPrint(("Invalid Device Object Pointer\n"));
            return STATUS_INVALID_PARAMETER;
        }

        VHF_CONFIG_INIT(&vhfConfig, pdo, sizeof(G_DefaultReportDescriptor), G_DefaultReportDescriptor);
        KdPrint(("VHF configuration initialized\n"));
        vhfConfig.EvtVhfAsyncOperationWriteReport = EvtVhfWriteReport;
        vhfConfig.VendorID = 0xDEED;
        vhfConfig.ProductID = 0xFEED;
        vhfConfig.VersionNumber = 0x101;
        vhfConfig.VhfClientContext = device;

        status = VhfCreate(&vhfConfig, &deviceContext->VhfHandle);
        if (!NT_SUCCESS(status))
        {
            KdPrint(("VhfCreate failed with status: 0x%08x\n", status));
            return status;
        }

        KdPrint(("VhfCreate succeeded\n"));

        status = VhfStart(deviceContext->VhfHandle);
        if (!NT_SUCCESS(status))
        {
            KdPrint(("VhfStart failed with status: 0x%08x\n", status));
            return status;
        }
    }

    return status;
}

简单来说就是先初始化VHF_CONFIG,然后设置回调和其他属性,注意这里将WDF设备对象device设置为回调的上下文,是为了在回调中获取WDF设备对象的设备上下文。然后调用VhfCreate创建VHF设备对象,创建出的VHF设备对象的句柄保存到WDF设备对象的设备上下文中。最后调用VhfStart启动VHF设备对象。

报告描述符是通过VHF_CONFIG传给VHF的,还是分为键盘(report_id=2)和Vendor definded(report_id=1)两个顶级集合,定义如下:

复制代码
UCHAR       G_DefaultReportDescriptor[] = {
    0x06,0x00, 0xFF,                // USAGE_PAGE (Vender Defined Usage Page)
    0x09,0x01,                      // USAGE (Vendor Usage 0x01)
    0xA1,0x01,                      // COLLECTION (Application)
    0x85,0x01,    // REPORT_ID (1)
    0x09,0x01,                         // USAGE (Vendor Usage 0x01)
    0x15,0x00,                         // LOGICAL_MINIMUM(0)
    0x26,0xff, 0x00,                   // LOGICAL_MAXIMUM(255)
    0x75,0x08,                         // REPORT_SIZE (0x08)
    0x96,(FEATURE_REPORT_SIZE_CB & 0xff), (FEATURE_REPORT_SIZE_CB >> 8), // REPORT_COUNT
    0xB1,0x00,                         // FEATURE (Data,Ary,Abs)
    0x09,0x01,                         // USAGE (Vendor Usage 0x01)
    0x75,0x08,                         // REPORT_SIZE (0x08)
    0x96,(INPUT_REPORT_SIZE_CB & 0xff), (INPUT_REPORT_SIZE_CB >> 8), // REPORT_COUNT
    0x81,0x00,                         // INPUT (Data,Ary,Abs)
    0x09,0x01,                         // USAGE (Vendor Usage 0x01)
    0x75,0x08,                         // REPORT_SIZE (0x08)
    0x96,(OUTPUT_REPORT_SIZE_CB & 0xff), (OUTPUT_REPORT_SIZE_CB >> 8), // REPORT_COUNT
    0x91,0x00,                         // OUTPUT (Data,Ary,Abs)
    0xC0,                           // END_COLLECTION

    0x05, 0x01,        // USAGE_PAGE (Generic Desktop)
    0x09, 0x06,        // USAGE (Keyboard)
    0xA1, 0x01,        // COLLECTION (Application)

    0x85, 0x02,    // REPORT_ID (2)

    0x05, 0x07,        //   USAGE_PAGE (Keyboard)
    0x19, 0xE0,        //   USAGE_MINIMUM (Left Control)
    0x29, 0xE7,        //   USAGE_MAXIMUM (Right GUI)
    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, 0x03,        //   INPUT (Const, Var, Abs)

    0x95, 0x06,        //   REPORT_COUNT (6) 
    0x75, 0x08,        //   REPORT_SIZE (8)
    0x15, 0x00,        //   LOGICAL_MINIMUM (0)
    0x25, 0x65,        //   LOGICAL_MAXIMUM (101)
    0x05, 0x07,        //   USAGE_PAGE (Keyboard)
    0x19, 0x00,        //   USAGE_MINIMUM (No Event)
    0x29, 0x65,        //   USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00,        //   INPUT (Data, Ary, Abs)

    0xC0,               // END_COLLECTION
};

3.4 实现键盘功能

这里我们还是实现下面这个功能:

应用程序向源驱动程序发送一个键值,然后虚拟键盘就模拟这个键的按下,如果发送0,则模拟抬起。

3.4.1 使用默认缓冲策略

由于请求处理和缓冲策略都是VHF实现的,所以我们要做的很少,只需要注册EvtVhfAsyncOperationWriteReport的回调来接收应用程序发送的键值,然后调用VhfReadReportSubmit提交一个输入报告就可以了,回调函数的代码如下:

复制代码
typedef struct _HIDMINI_KBD_INPUT_REPORT {

    UCHAR ReportId;

    UCHAR Data[8];

} HIDMINI_KBD_INPUT_REPORT, * PHIDMINI_KBD_INPUT_REPORT;

typedef struct _HIDMINI_OUTPUT_REPORT {

    UCHAR ReportId;

    UCHAR Data;

    USHORT Pad1;

    ULONG Pad2;

} HIDMINI_OUTPUT_REPORT, * PHIDMINI_OUTPUT_REPORT;

VOID EvtVhfWriteReport(
    _In_           PVOID VhfClientContext,
    _In_           VHFOPERATIONHANDLE VhfOperationHandle,
    _In_           PVOID VhfOperationContext,
    _In_           PHID_XFER_PACKET HidTransferPacket
)
{
    ULONG reportSize;
    NTSTATUS status;
    PHIDMINI_OUTPUT_REPORT  outputReport;
    PDEVICE_CONTEXT deviceContext;
    HID_XFER_PACKET inputPacket;
    HIDMINI_KBD_INPUT_REPORT inputReport;

    UNREFERENCED_PARAMETER(VhfOperationContext);
    status = STATUS_SUCCESS;
    deviceContext = DeviceGetContext(VhfClientContext);

    if (HidTransferPacket->reportId != 1) 
    {
        status = STATUS_INVALID_PARAMETER;
        KdPrint(("WriteReport: unkown report id %d\n", HidTransferPacket->reportId));
        goto Exit;
    }
    
    reportSize = sizeof(HIDMINI_OUTPUT_REPORT);

    if (HidTransferPacket->reportBufferLen < reportSize) {
        status = STATUS_INVALID_BUFFER_SIZE;
        KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n",
            HidTransferPacket->reportBufferLen, reportSize));
        goto Exit;
    }

    outputReport = (PHIDMINI_OUTPUT_REPORT)HidTransferPacket->reportBuffer;
    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "data:%08x\n", outputReport->Data);

    RtlZeroMemory(&inputReport, sizeof(HIDMINI_KBD_INPUT_REPORT));
    inputReport.ReportId = 2;
    inputReport.Data[2] = outputReport->Data;
    inputPacket.reportBuffer = (PUCHAR)&inputReport;
    inputPacket.reportBufferLen = sizeof(HIDMINI_KBD_INPUT_REPORT);
    inputPacket.reportId = 2;
    VhfReadReportSubmit(deviceContext->VhfHandle, &inputPacket);

Exit:
    VhfAsyncOperationComplete(VhfOperationHandle, status);
}

这个函数从入参的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的代码

复制代码
typedef struct _DEVICE_CONTEXT
{
    BOOLEAN Ready;

    UCHAR Data[8];

    VHFHANDLE VhfHandle;

} DEVICE_CONTEXT, *PDEVICE_CONTEXT;

VOID EvtVhfReadyForNextReadReport(
    _In_ PVOID VhfClientContext
)
{
    PDEVICE_CONTEXT deviceContext = DeviceGetContext(VhfClientContext);
    KdPrint(("EvtVhfReadyForNextReadReport...Entry\n"));
    deviceContext->Ready = TRUE;
}

VOID EvtVhfWriteReport(
    _In_           PVOID VhfClientContext,
    _In_           VHFOPERATIONHANDLE VhfOperationHandle,
    _In_           PVOID VhfOperationContext,
    _In_           PHID_XFER_PACKET HidTransferPacket
)
{
    ULONG reportSize;
    NTSTATUS status;
    PHIDMINI_OUTPUT_REPORT  outputReport;
    PDEVICE_CONTEXT deviceContext;
    HID_XFER_PACKET inputPacket;
    HIDMINI_KBD_INPUT_REPORT inputReport;

    UNREFERENCED_PARAMETER(VhfOperationContext);
    status = STATUS_SUCCESS;
    deviceContext = DeviceGetContext(VhfClientContext);

    if (HidTransferPacket->reportId != CONTROL_COLLECTION_REPORT_ID) 
    {
        status = STATUS_INVALID_PARAMETER;
        KdPrint(("WriteReport: unkown report id %d\n", HidTransferPacket->reportId));
        goto Exit;
    }
    
    reportSize = sizeof(HIDMINI_OUTPUT_REPORT);

    if (HidTransferPacket->reportBufferLen < reportSize) {
        status = STATUS_INVALID_BUFFER_SIZE;
        KdPrint(("WriteReport: invalid input buffer. size %d, expect %d\n",
            HidTransferPacket->reportBufferLen, reportSize));
        goto Exit;
    }

    outputReport = (PHIDMINI_OUTPUT_REPORT)HidTransferPacket->reportBuffer;
    TraceEvents(TRACE_LEVEL_INFORMATION, TRACE_DRIVER, "data:%08x\n", outputReport->Data);
    deviceContext->Data[2] = outputReport->Data;

    RtlZeroMemory(&inputReport, sizeof(HIDMINI_KBD_INPUT_REPORT));
    if (deviceContext->Ready)
    {
        deviceContext->Ready = FALSE;
        inputReport.ReportId = 2;
        inputReport.Data[2] = deviceContext->Data[2];
        inputPacket.reportBuffer = (PUCHAR)&inputReport;
        inputPacket.reportBufferLen = sizeof(HIDMINI_KBD_INPUT_REPORT);
        inputPacket.reportId = 2;
        status = VhfReadReportSubmit(deviceContext->VhfHandle, &inputPacket);
        if (!NT_SUCCESS(status)) 
        {
            KdPrint(("VhfReadReportSubmit failed: 0x%08x", status));
        }
    }

Exit:
    VhfAsyncOperationComplete(VhfOperationHandle, status);
}

这里在DEVICE_CONTEXT结构体中新增了一个Ready字段,当EvtVhfReadyForNextReadReport被调用时,将它设为TRUE。然后当EvtVhfWriteReport被调用时,首先检查Ready字段,如果为TRUE,则提交输入报告。

像键盘这类输入设备,系统会持续请求输入报告,所以正常情况下,提交输入报告后,VHF就会马上再次调用EvtVhfReadyForNextReadReport。

3.5 驱动安装方法

将vhfkeyboardkm.sys, vhfkeyboardkm.inf, vhfkeyboardkm.cat三个文件拷贝到需要安装的设备上,使用管理员权限的cmd命令行输入

复制代码
devcon.exe install vhfkeyboardkm.inf Root\vhfkeyboardkm

上面的vhfkeyboardkm默认情况下应替换为项目名称。devcon.exe会随WDK一起安装,如果找不到可以全局搜索一下。

如果是测试签名,需要设备开启测试模式才能安装,开启测试模式的方法是,在管理员权限的cmd命令行输入

复制代码
bcdedit.exe /set testsigning on

然后重启。

3.6 应用程序

应用程序的话还是用这份代码,这份代码监听Alt/Ctrl+字母键的全局快捷键,在Alt+字母键时,向源驱动程序发送该字母键的键值,在Ctrl+字母键时,向源驱动程序发送0. 最后实现的效果跟上一次使用hidmini2实例改造的驱动程序效果一样。

复制代码
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using HidSharp;

namespace VirtualHidKbdClient
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private const int AltKeyEventId = 9000;

        private bool PressedKey = false;
        private HidStream _stream;

        [DllImport("user32.dll")]
        public static extern uint MapVirtualKey(uint uCode, uint type);

        [DllImport("user32.dll")]
        public static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);

        [DllImport("user32.dll")]
        public static extern bool UnregisterHotKey(IntPtr hWnd, int id);

        public MainWindow()
        {
            InitializeComponent();
            SourceInitialized += MainWindow_SourceInitialized;
        }

        private void MainWindow_SourceInitialized(object? sender, EventArgs e)
        {
            var handle = new WindowInteropHelper(this).Handle;
            var source = HwndSource.FromHwnd(handle);
            source?.AddHook(HwndHook);

            //注册Alt+F1和Alt+字母键的全局快捷键
            for (int i = (int)Key.A; i <= (int)Key.Z; i++)
            {
                RegisterHotKey(handle, AltKeyEventId, (uint)ModifierKeys.Alt, (uint)KeyInterop.VirtualKeyFromKey((Key)i));
            }
            RegisterHotKey(handle, AltKeyEventId, (uint)ModifierKeys.Alt, (uint)KeyInterop.VirtualKeyFromKey(Key.F1));

            //找到虚拟设备下面的vendor defined设备
            var devices = DeviceList.Local.GetHidDevices(0xDEED, 0xFEED);
            foreach (var d in devices)
            {
                Console.WriteLine(d.DevicePath);
            }

            var device = devices.FirstOrDefault(d => !d.DevicePath.EndsWith("kbd"));

            _stream = device.Open();
        }

        private IntPtr HwndHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            const int wmHotkey = 0x0312;

            switch (msg)
            {
                case wmHotkey:
                    switch (wParam.ToInt32())
                    {
                        case AltKeyEventId:
                            var lp = lParam.ToInt32();
                            var virtualKey = lp >> 16;
                            var key = KeyInterop.KeyFromVirtualKey(virtualKey);
                            //虚拟键码到HID键码的转换,字母键是连续的,所以可以简单写成这样
                            var hidKey = virtualKey - 61;
                            var charKey = (char)MapVirtualKey((uint)virtualKey, 2);

                            if (key == Key.F1 && PressedKey)
                            {
                                _stream.Write([1, 0]);
                                MessageBox.Show($"cancel long press");
                                PressedKey = false;
                            }

                            else if(key != Key.F1 && !PressedKey)
                            {
                                PressedKey = true;
                                _stream.Write([1, (byte)hidKey]);
                                MessageBox.Show($"long press {charKey}");
                            }

                            break;
                    }
                    break;
            }
            return IntPtr.Zero;
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            var handle = new WindowInteropHelper(this).Handle;
            //关闭窗口时取消注册
            UnregisterHotKey(handle, AltKeyEventId);

            //关闭窗口时控制虚拟设备按键抬起
            _stream.Write([1, 0]);
            _stream.Close();
        }
    }
}

4 在UMDF中使用VHF

据VHF的文档描述,VHF是支持UMDF的,其使用方法和KMDF也大差不差,只是在初始化的时候不太一样。

在前面介绍VHF_CONFIG_INIT函数的时候,可以看到函数定义里有一个条件编译,这个条件编译指示,在KMDF中,第二个参数应传入WDM设备对象,而在UMDF中,第二个参数应传入一个IoTarget.

据文档描述,这里的IoTarget应该是通过WdfIoTargetCreate创建,然后通过WdfIoTargetOpen打开的。但我调试了很久没有调通,实在搞不明白IoTarget应该关联什么样一个对象,总是在WdfIoTargetOpen这里报错。

相关推荐
程序员徐师兄20 小时前
Windows JDK11 下载安装教程,适合新手
java·windows·jdk11 下载安装·jdk11 下载教程
编码者卢布1 天前
【App Service】Java应用上传文件功能部署在App Service Windows上报错 413 Payload Too Large
java·开发语言·windows
多来哈米1 天前
openclaw在Windows部署
windows·openclaw
视觉AI1 天前
【踩坑实录】Windows ICS 共享网络下,国产化盒子 SSH 连接异常的完整分析
网络·windows·ssh
qq_246646191 天前
openclaw快速安装-windows版
windows·stm32·单片机
sonrisa_1 天前
Python同一类不同方法中变量值的传递
开发语言·windows·python
玖釉-1 天前
探索连续细节层次(Continuous LOD):深入解析 NVIDIA 的 nv_cluster_lod_builder
c++·windows·图形渲染
MyY_DO1 天前
第九课ida与花指令
windows·od
多多*1 天前
Mysql数据库相关 事务 MVCC与锁的爱恨情仇 锁的层次架构 InnoDB锁分析
java·数据库·windows·sql·oracle·面试·哈希算法
LateFrames1 天前
“蚯蚓涌动” 的屏保: DirectX 12 + ComputeSharp + Win32
windows·ui·gpu算力