Stories

Detail Return Return

使用VHF框架實現一個虛擬HID鍵盤 - Stories Detail

前幾天我通過改造微軟的vhidmini2這個驅動示例,寫了一個umdf的虛擬hid鍵盤,然後我發現,微軟還提供了一個叫Virtual Hid Framework(VHF)的框架,專門用來實現虛擬hid設備,在kmdf和umdf上都支持(文檔這麼説的),所以就想着用VHF來重寫一下上次的那個虛擬hid鍵盤。

0 VHF概述

使用VHF開發的驅動程序叫做源驅動程序,源驅動程序的作用是控制VHF設備對象的生命週期,以及為VHF設備對象提供數據。下面這張官方文檔中的設備樹顯示了它們之間的層次關係。
img
綠色框的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回調後,源驅動程序才可以再次提交輸入報告。

如果實現的是鍵盤、鼠標、觸摸這類輸入設備的話,一般而言,在啓動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)模板。
img

項目創建後,右鍵項目,點擊屬性,在項目屬性面板中,選擇鏈接器-輸入,在附加依賴項中添加vhfkm.lib,然後在頭文件中包含vhf.h
img

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這裏報錯。

Add a new Comments

Some HTML is okay.