自 Windows 10 1809 以来,通过 USB HID 设备创建文件失败,访问被拒绝 (5)

CreateFile over USB HID device fails with Access Denied (5) since Windows 10 1809

自从最新的 Windows 10 1809 更新后,我们无法再使用 CreateFile 打开我们的 USB HID 键盘类设备。我们将问题简化为这个最小的例子:

#include <windows.h>
#include <setupapi.h>
#include <stdio.h>
#include <hidsdi.h>

void bad(const char *msg) {
    DWORD w = GetLastError();
    fprintf(stderr, "bad: %s, GetLastError() == 0x%08x\n", msg, (unsigned)w);
}

int main(void) {
    int i;
    GUID hidGuid;
    HDEVINFO deviceInfoList;
    const size_t DEVICE_DETAILS_SIZE = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA) + MAX_PATH;
    SP_DEVICE_INTERFACE_DETAIL_DATA *deviceDetails = alloca(DEVICE_DETAILS_SIZE);
    deviceDetails->cbSize = sizeof(*deviceDetails);

    HidD_GetHidGuid(&hidGuid);
    deviceInfoList = SetupDiGetClassDevs(&hidGuid, NULL, NULL,
                                         DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
    if(deviceInfoList == INVALID_HANDLE_VALUE) {
        bad("SetupDiGetClassDevs");
        return 1;
    }

    for (i = 0; ; ++i) {
        SP_DEVICE_INTERFACE_DATA deviceInfo;
        DWORD size = DEVICE_DETAILS_SIZE;
        HIDD_ATTRIBUTES deviceAttributes;
        HANDLE hDev = INVALID_HANDLE_VALUE;

        fprintf(stderr, "Trying device %d\n", i);
        deviceInfo.cbSize = sizeof(deviceInfo);
        if (!SetupDiEnumDeviceInterfaces(deviceInfoList, 0, &hidGuid, i,
                                         &deviceInfo)) {
            if (GetLastError() == ERROR_NO_MORE_ITEMS) {
                break;
            } else {
                bad("SetupDiEnumDeviceInterfaces");
                continue;
            }
        }

        if(!SetupDiGetDeviceInterfaceDetail(deviceInfoList, &deviceInfo,
                                        deviceDetails, size, &size, NULL)) {
            bad("SetupDiGetDeviceInterfaceDetail");
            continue;
        }

        fprintf(stderr, "Opening device %s\n", deviceDetails->DevicePath);
        hDev = CreateFile(deviceDetails->DevicePath, 0,
                          FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
                          OPEN_EXISTING, 0, NULL);
        if(hDev == INVALID_HANDLE_VALUE) {
            bad("CreateFile");
            continue;
        }

        deviceAttributes.Size = sizeof(deviceAttributes);
        if(HidD_GetAttributes(hDev, &deviceAttributes)) {
            fprintf(stderr, "VID = %04x PID = %04x\n", (unsigned)deviceAttributes.VendorID, (unsigned)deviceAttributes.ProductID);
        } else {
            bad("HidD_GetAttributes");
        }
        CloseHandle(hDev);
    }

    SetupDiDestroyDeviceInfoList(deviceInfoList);
    return 0;
}

它枚举所有 HID 设备,尝试通过 SetupDiGetDeviceInterfaceDetail 提供的路径使用 CreateFile 为每个设备获取供应商 ID/product ID,然后调用 HidD_GetAttributes

此代码在以前的 Windows 版本上运行没有问题(在 Windows 7、Windows 10 1709 和 1803 上测试,从中提取此代码的原始代码始终有效从 XP 开始),但最新更新 (1809) 所有 keyboard 设备(包括我们的)都无法打开,因为 CreateFile 失败并拒绝访问(GetLastError() == 5). 运行作为管理员的程序没有任何作用。

比较更新前后的输出,我注意到现在无法打开的设备在设备路径中增加了尾随\kbd,即之前的

\?\hid#vid_24d6&pid_8000&mi_00#7&294a3305&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}

现在是

\?\hid#vid_24d6&pid_8000&mi_00#7&294a3305&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}\kbd

最新的 Windows 10 版本是否有 bug/new 安全限制?这段代码是否总是错误的并且之前偶然起作用?这可以修复吗?


更新

作为孤注一掷的尝试,我们尝试从返回的字符串中删除 \kbd... 现在 CreateFile 可以工作了!所以,现在我们有了解决方法,但了解这是否是 SetupDiGetDeviceInterfaceDetail 中的错误、是否是故意的以及此解决方法是否确实是正确的做法会很有趣。

我认为这是最新 Windows 10 版本中的新安全限制。

我查找字符串 KBD(UTF-16 格式)- 它只存在于版本 1809 的两个驱动程序中,hidclass.syskbdhid.sys,1709版本不存在。

hidclass.sys 中,他们更改了 HidpRegisterDeviceInterface 函数。在此版本之前,它调用 IoRegisterDeviceInterface with GUID_DEVINTERFACE_HID 并将 ReferenceString 指针设置为 0。但在新版本中,根据 GetHidClassCollection 的结果,它传递 KBD 作为 ReferenceString 指针。

kbdhid.sys 中,他们更改了 KbdHid_Create,这里是对 KBD 字符串的 return 错误检查(访问被拒绝或共享违规)。

要更准确地了解原因,需要进行更多研究。一些不满:


供参考,HidpRegisterDeviceInterface 来自 1709 build

here ReferenceString == 0 always (xor r8d,r8d),并且没有检查 cmp word [rbp + a],6 on class采集数据


但是,1809 中的 KbdHid_Create 包含一个错误。代码是:

NTSTATUS KbdHid_Create(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
  //...

    PIO_STACK_LOCATION IrpSp = IoGetCurrentIrpStackLocation(Irp);

    if (PFILE_OBJECT FileObject = IrpSp->FileObject)
    {
        PCUNICODE_STRING FileName = &FileObject->FileName;

        if (FileName->Length)
        {
        #if ver == 1809
            UNICODE_STRING KBD = RTL_CONSTANT_STRING(L"KBD"); // !! bug !!

            NTSTATUS status = RtlEqualUnicodeString(FileName, &KBD, FALSE)
                ? STATUS_SHARING_VIOLATION : STATUS_ACCESS_DENIED;
        #else
            NTSTATUS status = STATUS_ACCESS_DENIED;
        #endif

            // log

            Irp->IoStatus.Status = status;
            IofCompleteRequest(Irp, IO_NO_INCREMENT);
            return status;
        }
    }
    // ...
}

这个函数想在这里做什么?它从 Irp 当前堆栈位置查找传递的 PFILE_OBJECT FileObject。没有提供FileObject或名称为空,允许打开;否则,打开失败。

在 1809 之前它总是失败,错误为 STATUS_ACCESS_DENIED (0xc0000022),但是从 1809 开始,检查名称,如果它等于 KBD(区分大小写)另一个错误 - STATUS_SHARING_VIOLATION 是 returned。但是,名称始终以 \ 符号开头,因此它永远不会匹配 KBD。它可以是 \KBD,因此,要修复此检查,需要将以下行更改为:

UNICODE_STRING KBD = RTL_CONSTANT_STRING(L"\KBD");

并与该字符串进行比较。因此,根据设计,我们在尝试通过 *\KBD 名称打开键盘设备时应该会出现 STATUS_SHARING_VIOLATION 错误,但由于实施错误,我们实际上会在此处出现 STATUS_ACCESS_DENIED

另一个变化是 HidpRegisterDeviceInterface - 在设备上调用 IoRegisterDeviceInterface 之前它会查询 GetHidClassCollection 结果,如果某些 WORD (2 字节)字段在结构中等于6,添加KBD后缀(ReferenceString)。我猜(但我不确定)6 可以是 Usage ID for keyboard,这个前缀的基本原理是设置 Exclusive access mode


实际上,如果我们使用通过 OBJECT_ATTRIBUTES 打开的相关设备,我们可以让文件名不以 \ 开头。所以,只是为了测试,我们可以这样做:如果接口名称以\KBD结尾,首先打开没有这个后缀的文件(所以相对设备名是空的),这个打开一定可以正常工作;然后,我们可以尝试使用名称 KBD 的相对打开文件 - 我们必须在 1809 中获得 STATUS_SHARING_VIOLATION 并且在以前的版本中获得 STATUS_ACCESS_DENIED (但在这里我们将没有 \KBD 后缀) :

void TestOpen(PWSTR pszDeviceInterface)
{
    HANDLE hFile;

    if (PWSTR c = wcsrchr(pszDeviceInterface, '\'))
    {
        static const UNICODE_STRING KBD = RTL_CONSTANT_STRING(L"KBD");

        if (!wcscmp(c + 1, KBD.Buffer))
        {
            *c = 0;

            OBJECT_ATTRIBUTES oa = { sizeof(oa), 0, const_cast<PUNICODE_STRING>(&KBD) };

            oa.RootDirectory = CreateFileW(pszDeviceInterface, 0, 
                FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, 0, 0);

            if (oa.RootDirectory != INVALID_HANDLE_VALUE)
            {
                IO_STATUS_BLOCK iosb;

                // will be STATUS_SHARING_VIOLATION (c0000043)
                NTSTATUS status = NtOpenFile(&hFile, SYNCHRONIZE, &oa, &iosb, 
                    FILE_SHARE_VALID_FLAGS, FILE_SYNCHRONOUS_IO_NONALERT);

                CloseHandle(oa.RootDirectory);

                if (0 <= status)
                {
                    PrintAttr(hFile);
                    CloseHandle(hFile);
                }
            }

            return ;
        }
    }

    hFile = CreateFileW(pszDeviceInterface, 0, 
         FILE_SHARE_VALID_FLAGS, 0, OPEN_EXISTING, 0, 0);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        PrintAttr(hFile);
        CloseHandle(hFile);
    }
}
void PrintAttr(HANDLE hFile)
{
    HIDD_ATTRIBUTES deviceAttributes = { sizeof(deviceAttributes) };

    if(HidD_GetAttributes(hFile, &deviceAttributes)) {
        printf("VID = %04x PID = %04x\r\n", 
            (ULONG)deviceAttributes.VendorID, (ULONG)deviceAttributes.ProductID);
    } else {
        bad(L"HidD_GetAttributes");
    }
}

在 1809 的测试中我实际上得到了 STATUS_SHARING_VIOLATION,这也显示了 kbdhid.KbdHid_Create 中的另一个错误 - 如果我们检查 FileName,我们需要检查 RelatedFileObject -是不是0.


此外,与错误无关,但作为建议:使用 CM_Get_Device_Interface_List 代替 SetupAPI 更有效:

volatile UCHAR guz = 0;

CONFIGRET EnumInterfaces(PGUID InterfaceClassGuid)
{
    CONFIGRET err;

    PVOID stack = alloca(guz);
    ULONG BufferLen = 0, NeedLen = 256;

    union {
        PVOID buf;
        PWSTR pszDeviceInterface;
    };

    for(;;) 
    {
        if (BufferLen < NeedLen)
        {
            BufferLen = RtlPointerToOffset(buf = alloca((NeedLen - BufferLen) * sizeof(WCHAR)), stack) / sizeof(WCHAR);
        }

        switch (err = CM_Get_Device_Interface_ListW(InterfaceClassGuid, 
            0, pszDeviceInterface, BufferLen, CM_GET_DEVICE_INTERFACE_LIST_PRESENT))
        {
        case CR_BUFFER_SMALL:
            if (err = CM_Get_Device_Interface_List_SizeW(&NeedLen, InterfaceClassGuid, 
                0, CM_GET_DEVICE_INTERFACE_LIST_PRESENT))
            {
        default:
            return err;
            }
            continue;

        case CR_SUCCESS:

            while (*pszDeviceInterface)
            {
                TestOpen(pszDeviceInterface);

                pszDeviceInterface += 1 + wcslen(pszDeviceInterface);
            }
            return 0;
        }
    }
}

EnumInterfaces(const_cast<PGUID>(&GUID_DEVINTERFACE_HID));

修复在今天(2019 年 3 月 1 日)发布的 windows 更新中。

https://support.microsoft.com/en-us/help/4482887/windows-10-update-kb4482887

您可以在 Delphi-Praxis in German

找到解决方法

简而言之:更改单元 JvHidControllerClass

    if not HidD_GetAttributes(HidFileHandle, FAttributes) then
  raise EControllerError.CreateRes(@RsEDeviceCannotBeIdentified);

    HidD_GetAttributes(HidFileHandle, FAttributes);

并通过运行 JEDI 安装 EXE 重新编译 Delhi JCL 和 JCVL 组件。