Устройство CreateFile через USB HID не работает с Access Denied (5), начиная с Windows 10 1809

Со времени последнего обновления Windows 10 1809 мы больше не можем открывать USB-устройства, похожие на HID, с помощью CreateFile. Мы сократили проблему до этого минимального примера:

#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, пытаясь получить идентификатор поставщика/идентификатор продукта для каждого, используя CreateFile по пути, SetupDiGetDeviceInterfaceDetail а затем вызывая HidD_GetAttributes.

Этот код работает без проблем в предыдущих версиях Windows (протестирован в Windows 7, Windows 10 1709 и 1803, и оригинальный код, из которого он был извлечен, работает всегда начиная с XP и далее), но с последним обновлением (1809) для всех клавиатурных устройств ( в том числе и наш) не может быть открыт, так как 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? Был ли этот код всегда неправильным, и раньше он работал случайно? Это можно исправить?


Обновить

В качестве отчаянной попытки мы попытались удалить \kbd из возвращенной строки... и CreateFile теперь работает! Итак, теперь у нас есть обходной путь, но было бы интересно понять, если это ошибка в SetupDiGetDeviceInterfaceDetail, если он намеренный, и если этот обходной путь на самом деле правильный.

Ответы

Ответ 2

Я думаю, что это новое ограничение безопасности в последней версии Windows 10.

Я искал строку KBD (в формате UTF-16) - она существует только в двух драйверах в версии 1809, hidclass.sys и kbdhid.sys, и не существует в версии 1709.

В hidclass.sys они изменили функцию HidpRegisterDeviceInterface. Перед этим выпуском он вызывался IoRegisterDeviceInterface с GUID_DEVINTERFACE_HID и указателем ReferenceString, установленным на 0. Но в новой версии, в зависимости от результата GetHidClassCollection, он передает KBD как указатель ReferenceString.

Внутри kbdhid.sys они изменили KbdHid_Create, и вот проверка строки KBD, возвращающей ошибки (отказ в доступе или нарушение совместного доступа).

Чтобы понять, почему, нужно больше исследований. Некоторая беда:

enter image description here enter image description here enter image description here


Для справки: HidpRegisterDeviceInterface из сборки 1709 года

enter image description here

здесь ReferenceString == 0 всегда (xor r8d, r8d), и здесь нет проверки cmp word [rbp + a],6 для данных коллекции классов


Однако KbdHid_Create 1809 года содержит ошибку. Код:

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;
        }
    }
    // ...
}

Что это за функция пытается здесь делать? Он ищет переданный PFILE_OBJECT FileObject из текущего местоположения стека Irp. Он не указан FileObject или имеет пустое имя, разрешить открытие; в противном случае открытие не удастся.

До 1809 года всегда возникала ошибка с ошибкой STATUS_ACCESS_DENIED (0xc0000022), но начиная с 1809 проверяется имя, и если оно равно KBD (чувствительно к регистру), возвращается другая ошибка - STATUS_SHARING_VIOLATION. Однако имя всегда начинается с символа \, поэтому оно никогда не будет совпадать с KBD. Это может быть \KBD, поэтому для исправления этой проверки необходимо изменить следующую строку на:

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

и проведите сравнение с этой строкой. Итак, по замыслу у нас должна была быть ошибка STATUS_SHARING_VIOLATION при попытке открыть устройство клавиатуры по имени *\KBD, но из-за ошибки реализации мы фактически получили STATUS_ACCESS_DENIED здесь

enter image description here

Другое изменение было в HidpRegisterDeviceInterface - перед вызовом IoRegisterDeviceInterface на устройстве оно запрашивает результат GetHidClassCollection, и если какое-то поле WORD (2 байта) в структуре равно 6, добавляет суффикс KBD (ReferenceString). Я думаю (но я не уверен), что 6 может быть идентификатором использования для клавиатуры, и обоснование для этого префикса состоит в том, чтобы установить эксклюзивный режим доступа


На самом деле, мы можем начать FileName без \, если мы используем относительное устройство, открытое через OBJECT_ATTRIBUTES. Итак, просто для проверки, мы можем сделать это: если имя интерфейса заканчивается на \KBD, сначала откройте файл без этого суффикса (то есть с пустым относительным именем устройства), и это открытие должно работать нормально; затем мы можем попробовать открыть относительный открытый файл с именем KBD - мы должны получить STATUS_SHARING_VIOLATION в 1809 году и 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));

Ответ 3

Обходной путь можно найти в Delphi-Praxis на немецком языке

Для краткости: изменение в модуле JvHidControllerClass

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

в

    HidD_GetAttributes(HidFileHandle, FAttributes);

и перекомпилируйте Delhi JCL и JCVL Components, запустив JEDI Install EXE.