среда, 5 сентября 2012 г.

Простой способ обнаружения эмуляторов ключа 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, после чего вы можете вывести эти данные пользователю, ну или использовать их каким либо образом непосредственно в вашем приложении.



Как видите все достаточно просто, но в объектных модулях 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.

Забрать пример можно по данной ссылке.

Подсветка кода выполнена при помощи: http://highlight.hohli.com/





2 комментария:

  1. очень интересная статья, спасибо автору!

    ОтветитьУдалить
  2. Небольшой апдейт от ребят из Актива. Описанный в статье метод даст ложно/позитивное срабатывание при использовании пользователем вашего продукта на платформе Anywhere: http://www.digi.com/products/usb/anywhereusb#overview
    У нас запрещена эмуляция USB в любом ее виде, поэтому я могу опираться на данный подход при построении алгоритма защиты, вам-же нужно смотреть самому.

    ОтветитьУдалить