0%

【Linux技术分享】wine下捕捉hid通信

wine下捕捉hid通信

问题概述

hid设备在wine下无法被识别window中正常使用

问题截图

图1

分析流程

  1. 使用wireshark进行捕捉hid设备的通讯数据,需要安装插件并且选中usbpcab
    图99

  2. 在usbtree中发现我的设备是6号端口
    图2

  3. 在wireshark中选择好过滤条件usb.device_address=6
    图3

  4. 在wireshark中找到数据段,并将其拷贝出来
    图4

  5. 打印wine下日志,梳理wine与hid设备通讯流程,大致为HidD_SetOutputReport与设备通讯,再通过HidD_GetInputReport捕捉设备传来的数据

    1
    2
    3
    4
    5
    6
    7
    0024:trace:hid:HidD_SetOutputReport file 000000D8, report_buf 7BFDCD78, report_len 65.
    00c4:trace:hid:pdo_ioctl device 00ED0430 code 0xb0195
    00c4:trace:hid:hid_internal_dispatch write output report id 0 length 64:
    ......
    0024:trace:hid:HidD_SetOutputReport file 000000D8, report_buf 7BFDCD78, report_len 65.
    00c4:trace:hid:pdo_ioctl device 00ED0430 code 0xb0195
    00c4:trace:hid:hid_internal_dispatch write output report id 0 length 64:
  6. 完成测试程序,将步骤一捕捉的数据进行转发给设备

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    // 测试发送报告
    void TestHidApi(const char* apiName, HANDLE hDevice,
    BOOLEAN (WINAPI *pfnSendReport)(HANDLE, PVOID, ULONG),
    USHORT reportLen, BOOL isOutput)
    {
    printf("\n--- Testing %s ---\n", apiName);

    if (reportLen == 0) {
    printf("Report length is zero, cannot test.\n");
    return;
    }

    // 分配符合设备要求的缓冲区
    BYTE *reportBuffer = (BYTE*)malloc(reportLen);
    if (!reportBuffer) {
    printf("Memory allocation failed.\n");
    return;
    }
    memset(reportBuffer, 0, reportLen);

    // 复制命令数据(注意不要超出缓冲区)
    size_t dataSize = sizeof(g_ReportData) > reportLen ? reportLen : sizeof(g_ReportData);
    memcpy(reportBuffer, g_ReportData, dataSize);

    // 如果报告ID == 0,第一个字节必须为0(已经是0)
    reportBuffer[0] = 0x00;

    // 发送报告
    BOOL bResult = pfnSendReport(hDevice, reportBuffer, reportLen);
    if (bResult) {
    HidD_GetInputReport(hDevice, reportBuffer, reportLen);
    printf("HidD_GetInputReport succeeded, received %u bytes:\n", sizeof(reportBuffer));
    for (DWORD i = 0; i < 64; i++) {
    printf("%02X ", reportBuffer[i]);
    if ((i+1) % 16 == 0) printf("\n");
    }
    printf("\n");

    // 如果愿意,可 以尝试读取 Input Report,但此处不展开
    } else {
    printf("Call to %s failed, Error: %d\n", apiName, GetLastError());
    }

    free(reportBuffer);
    }

    int main()
    {
    printf("HID API Test Program (Fixed)\n");

    // 1. 打开设备
    HANDLE hDevice = GetHidDeviceHandle(VENDOR_ID, PRODUCT_ID);
    if (hDevice == INVALID_HANDLE_VALUE) {
    printf("Device not found (VID=0x%04X, PID=0x%04X)\n", VENDOR_ID, PRODUCT_ID);
    return 1;
    }
    printf("Device opened successfully.\n");

    // 2. 获取设备的报告长度
    USHORT outputLen = 0, featureLen = 0;
    if (!GetReportLengths(hDevice, &outputLen, &featureLen)) {
    printf("Failed to get report lengths. Using defaults (64).\n");
    outputLen = 64;
    featureLen = 64;
    }

    printf("Force set featureLen = %u (ignoring device descriptor)\n", featureLen);
    // 3. 测试两个 API
    TestHidApi("HidD_SetOutputReport", hDevice, HidD_SetOutputReport, outputLen, TRUE);
    //TestHidApi("HidD_SetFeature", hDevice, HidD_SetFeature, featureLen, FALSE);

    // 4. 清理
    CloseHandle(hDevice);
    return 0;
    }
  7. 发现在wine下和window显示不一样,wine下并没有数据,window是可以捕捉的回复

  8. 这时候调研wine下是HidD_SetOutputReport和HidD_GetInputReport是如何访问设备,与设备进行交互,大致为

    • HidD_SetOutputReport:HidD_SetOutputReport->pdo_ioctl->hid_device_xfer_report->unix_device_set_output_report->在调用linux的write进行写入数据
    • HidD_GetInputReport:和上面一样不过是write换成了read
  9. 根据上面的逻辑完成一个linux读取的demo进行测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    int test_output_report(int fd) {
    unsigned char buffer[REPORT_LEN + 1];
    int ret, len = REPORT_LEN;
    fd_set fds;
    struct timeval tv;

    // 发送 Output Report(中断端点)
    // 如果 reportId == 0,需要先写一个字节的 0 作为报告 ID,后面跟数据
    memset(buffer, 0, sizeof(buffer));
    buffer[0] = 0x00;
    memcpy(buffer + 1, report_data, REPORT_LEN);
    ret = write(fd, buffer, REPORT_LEN + 1);
    if (ret < 0) {
    perror("write (Output Report)");
    return -1;
    }
    printf("Output Report sent %d bytes (including report ID)\n", ret);

    // 尝试读取 Input Report(中断端点)
    FD_ZERO(&fds);
    FD_SET(fd, &fds);
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    ret = select(fd + 1, &fds, NULL, NULL, &tv);
    if (ret < 0) {
    perror("select");
    return -1;
    } else if (ret == 0) {
    printf("Output Report: No Input Report received (timeout)\n");
    return -1;
    } else {
    unsigned char inbuf[REPORT_LEN + 1];
    ret = read(fd, inbuf, sizeof(inbuf));
    if (ret > 0) {
    print_hex("Input Report (via interrupt)", inbuf, ret);
    return 0;
    } else {
    printf("Output Report: read failed\n");
    return -1;
    }
    }
    }

    int test_feature_report(int fd) {
    unsigned char outbuf[REPORT_LEN];
    unsigned char inbuf[REPORT_LEN];
    int ret;

    // 发送 Feature Report
    memcpy(outbuf, report_data, REPORT_LEN);
    ret = ioctl(fd, HIDIOCSFEATURE(REPORT_LEN), outbuf);
    if (ret < 0) {
    perror("ioctl HIDIOCSFEATURE");
    return -1;
    }
    printf("Feature Report sent %d bytes\n", ret);

    // 读取 Feature Report
    memset(inbuf, 0, REPORT_LEN);
    ret = ioctl(fd, HIDIOCGFEATURE(REPORT_LEN), inbuf);
    if (ret < 0) {
    perror("ioctl HIDIOCGFEATURE");
    return -1;
    }
    print_hex("Feature Report received", inbuf, ret);
    return 0;
    }
  10. 发现在debian13下无法使用read来读取和这个hid设备通讯的数据,但是FEATURE是可以的,如下图:
    图5

  11. 由于该问题是系统性不支持,只能在wine中做一个重试机制,代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    +BOOL IsDataEmpty(const BYTE *data, DWORD length)
    +{
    + DWORD i;
    + for (i = 0; i < length; i++) if (data[i]) return FALSE;
    + return TRUE;
    +}
    +
    static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
    {
    IO_STACK_LOCATION *irpsp = IoGetCurrentIrpStackLocation(irp);
    @@ -1570,6 +1577,12 @@ static NTSTATUS WINAPI hid_internal_dispatch(DEVICE_OBJECT *device, IRP *irp)
    packet->reportBufferLen = last_report->length;
    irp->IoStatus.Information = packet->reportBufferLen;
    irp->IoStatus.Status = STATUS_SUCCESS;
    + if(IsDataEmpty(packet->reportBuffer, packet->reportBufferLen)){
    + TRACE("read empty input report id %u length %lu, need feature report\n", packet->reportId, packet->reportBufferLen);
    + unix_device_get_feature_report(device, packet, &irp->IoStatus);
    + if (!irp->IoStatus.Status) hidraw_disable_report_fixups(device);
    + break;
    + }
    if (TRACE_ON(hid))
    {
    TRACE("read input report id %u length %lu:\n", packet->reportId, packet->reportBufferLen);
    --

验证

修改后运行如下图:
图6