Простой способ обнаружения эмуляторов ключа Guardant

от автора

При работе с ключом защиты Guardant (не важно какой модели) разработчик использует соответствующие API, при этом от него скрыт сам механизм работы с устройством, не говоря уже о протоколе обмена. Он не имеет на руках валидного хэндла устройства, пользуясь только адресом шлюза (т.н. GuardantHandle) через который идет вся работа. В случае если в системе присутствует эмулятор ключа (особенно актуально для моделей до Guardant Stealth II включительно) используя данный шлюз разработчик не сможет определить, работает ли он с реальным физическим ключом, или его эмуляцией.

Задавшись в свое время вопросом: «как определить наличие физического ключа?», мне пришлось немного поштудировать великолепно поданный материал за авторством Павла Агурова в книге "Интерфейс USB. Практика использования и программирования". После чего потратить время на анализ вызовов API функций из трехмегабайтного объектника, линкуемого к приложению, в котором собственно и сокрыта вся «магия» работы с ключом. В итоге появилось достаточно простое решение данной проблемы не требующее использования оригинальных Guardant API. Единственный минус — все это жутко недокументированно и техническая поддержка компании Актив даже не будет рассматривать ваши вопросы, связанные с таким использованием ключей Guardant. Ну и конечно, в какой-то момент весь данный код может попросту перестать работать из-за изменений в драйверах Guardant.
Но пока что, на 5 сентября 2012 года, весь данный материал актуален и его работоспособность проверена на драйверах от версии 5.31.78, до текущей актуальной 6.00.101.


Порядок действий будет примерно таким:

  1. Через SetupDiGetClassDevsA() получим список всех присутствующих устройств.
  2. Проверим, имеет ли устройство отношение к ключам Guardant через проверку GUID устройства. (У Guardant данный параметр равен {C29CC2E3-BC48-4B74-9043-2C6413FFA784})
  3. Получим символьную ссылку на каждое устройство вызовом SetupDiGetDeviceRegistryPropertyA() с параметром SPDRP_PHYSICAL_DEVICE_OBJECT_NAME.
  4. Откроем устройство при помощи ZwOpenFile() (CreateFile() тут уже к сожалению не подойдет, т.к. будут затруднения при работе с символьными ссылками).

Теперь, имея на руках реальный хэндл ключа, вместо псевдохэндла (шлюза) предоставляемого Guardant API, мы можем получить описание его параметров, послав соответствующий IOCTL запрос. Правда, тут есть небольшой нюанс.

Начиная с Guardant Stealth III и выше, изменился протокол работы с ключом, как следствие поменялись константы IOCTL запросов и содержимое входящего и исходящего буфера. Для нормальной работы алгоритма желательно поддерживать возможности как старых, так и новых ключей, поэтому опишу различия:

Для начала константы IOCTL выглядят так:

  GetDongleQueryRecordIOCTL = $E1B20008;   GetDongleQueryRecordExIOCTL = $E1B20018; 

Первая для ключей от Guardant Stealth I/II
Вторая для Guardant Stealth III и выше (Sign/Time/Flash/Code)

Отправляя первый запрос на устройство, мы будем ожидать что драйвер нам вернет следующий буфер:

  TDongleQueryRecord = packed record     dwPublicCode: DWord; // Public code     byHrwVersion: Byte; // Аппаратная версия ключа     byMaxNetRes: Byte; // Максимальный сетевой ресурс     wType: WORD; // Флаги типа ключа     dwID: DWord; // ID ключа     byNProg: Byte; // Номер программы     byVer: Byte; // Версия     wSN: WORD; // Серийный номер     wMask: WORD; // Битовая маска     wGP: WORD; // Счетчик запусков GP/Счетчик времени     wRealNetRes: WORD; // Текущий сетевой ресурс, д.б. <= byMaxNetRes     dwIndex: DWord; // Индекс для удаленного программирования   end; 

В случае более новых ключей и с учетом того, что протокол изменился, отправка первого запроса уже нам ничего не даст. Точнее запрос конечно, будет выполнен, но буфер придет пустой (обниленый). Поэтому на новые ключи мы посылаем второй запрос, который вернет данные немного в другом формате:

  TDongleQueryRecordEx = packed record     Unknown0: array [0..341] of Byte;     wMask: WORD;    // Битовая маска     wSN: WORD;      // Серийный номер     byVer: Byte;    // Версия     byNProg: Byte;  // Номер программы     dwID: DWORD;    // ID ключа     wType: WORD;    // Флаги типа ключа     Unknown1: array [354..355] of Byte;     dwPublicCode: DWORD;     Unknown2: array [360..375] of Byte;     dwHrwVersion: DWORD; // тип микроконтролера     dwProgNumber: DWORD; // Номер программы     Unknown3: array [384..511] of Byte;   end; 

Здесь уже возвращается блок в 512 байт содержащий более подробную информацию о ключе. К сожалению по некоторым причинам я не могу вам дать полное описание данной структуры, но необходимые для данной статьи поля я в ней оставил.

Общий код получения данных о установленных ключах выглядит так:

procedure TEnumDonglesEx.Update; var   dwRequired: DWord;   hAllDevices: H_DEV;   dwInfo: DWORD;   Data: SP_DEVINFO_DATA;   Buff: array [0 .. 99] of AnsiChar;   hDeviceHandle: THandle;   US: UNICODE_STRING;   OA: OBJECT_ATTRIBUTES;   IO: IO_STATUS_BLOCK;   NTSTAT, dwReturn: DWORD;   DongleQueryRecord: TDongleQueryRecord;   DongleQueryRecordEx: TDongleQueryRecordEx; begin   SetLength(FDongles, 0);   DWord(hAllDevices) := INVALID_HANDLE_VALUE;   try     if not InitSetupAPI then       Exit;     UpdateUSBDevices;       hAllDevices := SetupDiGetClassDevsA(nil, nil, 0,       DIGCF_PRESENT or DIGCF_ALLCLASSES);     if DWord(hAllDevices) <> INVALID_HANDLE_VALUE then     begin       FillChar(Data, Sizeof(SP_DEVINFO_DATA), 0);       Data.cbSize := Sizeof(SP_DEVINFO_DATA);       dwInfo := 0;       while SetupDiEnumDeviceInfo(hAllDevices, dwInfo, Data) do       begin         dwRequired := 0;         FillChar(Buff[0], 100, #0);         if SetupDiGetDeviceRegistryPropertyA(hAllDevices, @Data,           SPDRP_PHYSICAL_DEVICE_OBJECT_NAME, nil, @Buff[0], 100, @dwRequired)           then           if CompareGuid(Data.ClassGuid, GrdGUID) then           begin             RtlInitUnicodeString(@US, StringToOleStr(string(Buff)));             FillChar(OA, Sizeof(OBJECT_ATTRIBUTES), #0);             OA.Length := Sizeof(OBJECT_ATTRIBUTES);             OA.ObjectName := @US;             OA.Attributes := OBJ_CASE_INSENSITIVE;             NTSTAT := ZwOpenFile(@hDeviceHandle,               FILE_READ_DATA or SYNCHRONIZE, @OA, @IO,               FILE_SHARE_READ or FILE_SHARE_WRITE or FILE_SHARE_DELETE,               FILE_SYNCHRONOUS_IO_NONALERT);             if NTSTAT = STATUS_SUCCESS then             try                 if DeviceIoControl(hDeviceHandle, GetDongleQueryRecordIOCTL,                 nil, 0, @DongleQueryRecord, SizeOf(TDongleQueryRecord),                 dwReturn, nil) and (DongleQueryRecord.dwID <> 0) then               begin                 SetLength(FDongles, Count + 1);                 FDongles[Count - 1].Data := DongleQueryRecord;                 FDongles[Count - 1].PnPParentPath :=                   GetPnP_ParentPath(Data.DevInst);                 Inc(dwInfo);                 Continue;               end;                 Move(FlashBuffer[0], DongleQueryRecordEx.Unknown0[0], 512);                 if DeviceIoControl(hDeviceHandle, GetDongleQueryRecordExIOCTL,                 @DongleQueryRecordEx.Unknown0[0],                 SizeOf(TDongleQueryRecordEx),                 @DongleQueryRecordEx.Unknown0[0],                 SizeOf(TDongleQueryRecordEx),                 dwReturn, nil) then               begin                   DongleQueryRecordEx.wMask :=                   htons(DongleQueryRecordEx.wMask);                 DongleQueryRecordEx.wSN :=                   htons(DongleQueryRecordEx.wSN);                 DongleQueryRecordEx.dwID :=                   htonl(DongleQueryRecordEx.dwID);                 DongleQueryRecordEx.dwPublicCode :=                   htonl(DongleQueryRecordEx.dwPublicCode);                 DongleQueryRecordEx.wType :=                   htons(DongleQueryRecordEx.wType);                   SetLength(FDongles, Count + 1);                 ZeroMemory(@DongleQueryRecord, SizeOf(DongleQueryRecord));                 DongleQueryRecord.dwPublicCode :=                   DongleQueryRecordEx.dwPublicCode;                 DongleQueryRecord.dwID := DongleQueryRecordEx.dwID;                 DongleQueryRecord.byNProg := DongleQueryRecordEx.byNProg;                 DongleQueryRecord.byVer := DongleQueryRecordEx.byVer;                 DongleQueryRecord.wSN := DongleQueryRecordEx.wSN;                 DongleQueryRecord.wMask := DongleQueryRecordEx.wMask;                 DongleQueryRecord.wType := DongleQueryRecordEx.wType;                 FDongles[Count - 1].Data := DongleQueryRecord;                 FDongles[Count - 1].PnPParentPath :=                   GetPnP_ParentPath(Data.DevInst);               end;             finally               ZwClose(hDeviceHandle);             end;           end;         Inc(dwInfo);       end;     end;   finally     if DWord(hAllDevices) <> INVALID_HANDLE_VALUE then       SetupDiDestroyDeviceInfoList(hAllDevices);   end; end; 

Данная процедура перебирает все ключи и заносит информацию о них в массив структур TDongleQueryRecord, после чего вы можете вывести эти данные пользователю, ну или использовать их каким либо образом непосредственно в вашем приложении.

image

Как видите все достаточно просто, но в объектных модулях Guardant API данный код помещен под достаточно серьезную стековую виртуальную машину и практически не доступен для анализа обычному разработчику. В принципе здесь нет ничего секретного, как видите при вызовах не используется даже шифрование передаваемых и получаемых буферов, но почему-то разработчики Guardant SDK не сочли нужным опубликовать данную информацию (правда я все-же смог получить разрешение на публикацию данного кода, т.к. в итоге тут не затронуты какие-то критические аспекты протокола обмена с ключом).

Но не будем отвлекаться, вы вероятно заметили в вышеприведенной процедуре вызов функции GetPnP_ParentPath(). Данная функция возвращает полный путь к устройству от рута. Выглядит ее реализация следующим образом:

  function GetPnP_ParentPath(Value: DWORD): string;   var     hParent: DWORD;     Buffer: array [0..1023] of AnsiChar;     Len: ULONG;     S: string;   begin     Result := '';     if CM_Get_Parent(hParent, Value, 0) = 0 then     begin       Len := Length(Buffer);       CM_Get_DevNode_Registry_PropertyA(hParent, 15, nil,         @Buffer[0], @Len, 0);       S := string(PAnsiChar(@Buffer[0]));       while CM_Get_Parent(hParent, hParent, 0) = 0 do       begin         Len := Length(Buffer);         CM_Get_DevNode_Registry_PropertyA(hParent, 15, nil,           @Buffer[0], @Len, 0);         S := string(PAnsiChar(@Buffer[0]));         Result := S + '#' + Result;       end;     end;     if Result = '' then       Result := 'не определен';   end; 

Собственно (вы будете смеяться) детектирование эмулятора будет происходить именно на базе данной строки.
Обычно путь устройства выглядит следующим образом:

\Device\00000004#\Device\00000004#\Device\00000044#\Device\00000049#\Device\NTPNP_PCI0005#\Device\USBPDO-3#

В нем как минимум будет присутствовать текст NTPNP_PCI или USBPDO.
Т.е. PCI шина или HCD хаб как минимум будут одним из предков.
Т.к. эмулятор является все-же виртуальным устройством, то путь к нему будет выглядеть примерно так:

\Device\00000040#\Device\00000040

Соответственно на базе данной информации можно реализовать простую функцию:

  function IsDonglePresent(const Value: string): Boolean;   begin     Result := Pos('NTPNP_PCI', Value) > 0;     if not Result then       Result := Pos('USBPDO', Value) > 0;   end; 

Ну и в завершение опишу еще несколько нюансов, которые можно будет увидеть в демопримере, прилагаемом к статье:

  • Относительно недавно появились новые ключи Guardant Flash представляющие из себя два устройства в одном. Т.е. это и ключ защиты и обычная флэшка. В функции UpdateUSBDevices() вы можете увидеть как можно определить какие из DRIVE_REMOVABLE дисков в системе расположены в ключе. В общем-то ничего нового, общий принцип был показан еще в демопримере безопасного отключения Flash устройств.
  • Приведен пример получения строкового представления PublicCode ключа (естественно без завершающего контрольного символа, во избежание).
  • Приведен пример получения даты выпуска ключа на основе его ID.

Небольшой нюанс:
Описанный в статье метод даст ложно/позитивное срабатывание при использовании пользователем вашего продукта на платформе Anywhere: http://www.digi.com/products/usb/anywhereusb#overview

Забрать пример можно здесь.

ссылка на оригинал статьи http://habrahabr.ru/post/178189/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *