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

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

    Задавшись в свое время вопросом: «как определить наличие физического ключа?», мне пришлось немного поштудировать великолепно поданный материал за авторством Павла Агурова в книге "Интерфейс USB. Практика использования и программирования". После чего потратить время на анализ вызовов API функций из трехмегабайтного объектника, линкуемого к приложению, в котором собственно и сокрыта вся «магия» работы с ключом.

    В итоге появилось достаточно простое решение данной проблемы не требующее использования оригинальных Guardant API.
    Единственный минус — все это жутко недокументированно и техническая поддержка компании Актив даже не будет рассматривать ваши вопросы, связанные с таким использованием ключей Guardant.
    Ну и конечно, в какой-то момент весь данный код может попросту перестать работать из-за изменений в драйверах Guardant.
    Но пока что, на 27 апреля 2013 года, весь данный материал актуален и его работоспособность проверена на драйверах от версии 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

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 22

      +4
      вы только что подкинули эмульмейкерам отличную идею для выпуска новых версий
        0
        Сомневаюсь в том, что это отличная идея :)
        Есть множество других способов, как минимум ни один из эмуляторов (даже реализующих виртуальную шину) не воспроизводит ее поведение досконально, я уж не говорю о эмуляции целевого устройства.
        Это так — ради шутки можно сказать, дабы было просто и доступно :)
          +1
          ну да, проще снять конверт чем эмулить весь хардварный стек)
            0
            Согласен, но использование конверта (утилит автозащиты) категорически не желательно для построения более-менее стойкой защиты. Он хорош для тех, кто абсолютно не умеет или не хочет заниматься защитой ПО.
            Я по этой теме выступал на семинарах компании «Актив», но не уверен в том, что я хороший лектор, поэтому особенной заинтересованности во взглядах аудитории, за исключением пары-тройки человек, я не увидел. Увы :)

            Впрочем, я описал суть данного подхода и свои рекомендации в данной статье: alexander-bagel.blogspot.ru/2012/09/blog-post.html
            0
            Хмм а как же эмулятор HASP тот как раз виртуальное устройство создает. Вполне себе полая эмуляция.
              0
              Создание виртуального устройства не является полной эмуляцией. Правда как там обстоят дела с HASP-ом я не знаю, но виденные мной варианты эмуляторов для Guardant показывают что эмулируется не полный спектр ICOTL запросов, на которые может отвечать реальный ключ.
          +1
          Логичнее пользоваться ассиметричной криптографией, предоставляемой ключом.
          Тогда никакие эмуляторы не помогут, только полный анализ алгоритмов.
            0
            Согласен, но это возможно только в последних поколениях ключей.
            Guardant Stealth II и ниже такого не умеет.
              0
              Guardant Stealth II, как и III, уже давно устарел, есть гораздо более инересный Guardant Code, в который можно перенести часть реальной логики программы.
                0
                Так-то оно так, но что делать со старыми ключами?
                К примеру у нас сейчас в районе 230 тысяч установок, нужно потратить в районе 115 миллионов рублей, чтобы купить такое-же количество Code, забрать у пользователей старые ключи, раздать новые, и в итоге на руках останется целый вагон старых ключей с которыми не понятно что делать. В итоге прямой убыток будет в районе 230 миллионов — да меня проще уволить, когда я приду к начальству с таким предложением :)
                  0
                  Просто интересно, а отдавать 230 тысячам клиентов ПО, использующее недокументированные возможности, которые в любой момент могут перестать работать — за такое не увольняют? =)
                    0
                    А кто сказал, что это используется в реальном ПО? :)
                    Это код из определенной утилиты, высылаемой пользователю в том случае, если у него что-то там опять не работает :)
            +1
            Интересная идея просмотра родителей устройства.

            Но тем не менее всё это как-то не очень серьезно выглядит.
            Чур не закидывать меня помидорами, но я всегда считал, что аппаратные ключи защиты кода (не данных) это в первую очередь психологическая защита. Мол если у тебя нет железки, то программа ворованная, если есть, то ты купил не абстрактную программу а материальную железку.
            В самом деле — если мы проверяем факт наличия ключа (не важно каким образом) то взлом сводится к классической замене условного перехода на NOP или безусловный переход (утрирую, но железяка тут ничего дополнительного к программной защите не приносит).
            Если мы используем железку для расшифровки кода, то код рано или поздно оказывается в памяти. Тут мы его тепленьким и берем.

            Единственное что реально остается это или перенос части бизнеслогики в железяку, но тут мы упираемся как в производительность железяки (а она в свою очередь в цену), либо в шину — в результате да, такие черные ящики сильно осложняют обратный инжиниринг, но всё-же дороги и не универсальны. Что еще можно придумать? Брать пример с антипарсеров и не давать сливать ВЕСЬ код?
            т.е. если ты расшифровал код для работы под х86 то зачем ты еще и ARM трогаешь? (утрирую опять, но в реальности может быть много других вариантов — клиентский и серверный код ит.д.) — тут мы либо имеем слишком высокую вероятность ложных срабатываний, либо эффективность сводится к нулю несколькими ключами.

            Прошу ногами не пинать, если я не прав то объясните в чем. Спасибо.
              +1
              Теоретически, так. Только нужно править не один JZ, а 100-200. И расшифровать не пару кусков кода, а 300 функций, которые перед выполнением расшифровываются, а после выполнения стираются.

              Но ведь и поставить такую защиту трудно? Достаточно держать специальный отдел на зарплате и за год-два параллельно с разработкой программы к 5-й версии они напишут 100500 обёрток, чтобы каждая расшифровка и проверка была уникальной и требовала её разбора по-новой.

              Задача состоит в том, чтобы даже самому сильному хакеру объективно потребовалось 20-30 рабочих дней (8-часовых) на удаление защиты. Тогда типичный диалог: «Вась, ты же умный, посмотри тут программу...» закончится тем, что Вася вечер-другой посмотрит-посмотрит и скажет: «да пошло оно всё...»

              Если вы считаете, что всё сводится к простановке NOP в нужное место (хотя и таких «взломов» предостаточно), наверное вы не разбирали серьёзные защиты. Например, StarForce, который в kernel-mode распаковывает куски кода и выполняет их.
                0
                Мое общение с ассемблером и прочими низкоуровневыми инструментами закончилось когда закончилась эра 16-биток.
                Так что да, не колупал серьезных защит. Но я примерно представляю, что там уже давно одним NOP не отделаться :)
                Я потому и писал:
                В самом деле — если мы проверяем факт наличия ключа (не важно каким образом) то взлом сводится к классической замене условного перехода на NOP или безусловный переход (утрирую, но железяка тут ничего дополнительного к программной защите не приносит).

                Утрирую — это было про NOP.
                Основная мысль была в том, что проверка железки лечится програмно с такой же сложностью как и чисто програмные проверки.
                  0
                  «Чисто програмные проверки» не требуют анализа кода.
                  Достаточно скопировать окружение (в простом случае — файл лицензии), купить идентичные конфигурации ПК и сделать побитные копии дисков. Если проверка завязывается на серийные номера BIOS или HDD, это можно выяснить и перехватить системный вызов в этом месте.

                  Даже «тупой» ключ (у которого есть только S/N) не даёт возможность воспроизвести (скопировать) среду.
                  А ковыряться в запутанном драйвере, выясняя как он читает S/N, намного сложнее, чем встроиться в ATA-стек и подменять серийник HDD

                  То есть, плюс ключа — заставить-таки взломщика «хакать честно», а не пытаться как-то обхитрить защиту (хотя есть эмуляторы...).
                    0
                    Аргумент.
                    Но это скорее вопрос масштаба.
                    В программной защите уже есть некоторый наработанный набор «заглушек». Известно как влезть в запрос серийника диска/биоса, где перехватывать низкоуровневое чтение диска и т.п.
                    В случае железяки это не известно. Но только до поры до времени.
                    Как только появляется с десяток программ (или одна популярная), так сразу же «поваренная книга» точек для перехвата пополнится рецептами по ловле запросов к ключикам этого производителя.

                    Итого — если решение типовое, то и отучать от него будут так-же типовыми методами.
                    Если решение единичное, то оно соизмеримо по сложности с единичным программным решением.
                    Ну разве что склепать какой-то самопальный ключ на PIC18F4550, который будет представляться HID-клавиатурой, но при этом по факту иметь отличный от стандарта стек команд будет проще, чем придумать какой-то новый источник инфы для привязки к компьютеру. Но это ведь не промышленное решение, а частный случай…
                      0
                      Да-да, я понял.
                      1. При условии необходимости полного разбора кода зашиты разницы никакой.
                      2. Но без аппаратного ключа хакеру легче схалтурить, выяснив точки привязки и склонировав их, не ковыряя код.
                      №1 это какое-то надуманное условие, всегда идут по пути наименьшего сопротивления.
                        0
                        Я согласен что в качестве дополнительного пугала, чтобы отпугнуть слабых специалистов вроде меня это эффективно. Но с этим и программная защита справляется.

                        На мелкосериных вещах это вполне рационально.
                        Но на решениях, где стоит несколько сотен тысяч копий целесообразность такого решения именно как защиты — вызывает сомнения.
                          0
                          Допустим, цена взлома $10000.
                          Цена лицензии $1000.
                          Обычный пользователь купит.

                          Пиратам в оффлайне делать нечего — это не 90-е годы, сейчас контрольная закупка и в тюрьму (если производитель не поощряет пиратство).

                          Взломать и выложить в интернете — возможно ради «славы», если аудитория проекта большая. Но если это специфичный бизнес-пакет — не тот случай.
                            0
                            чтобы отпугнуть слабых специалистов вроде меня это эффективно.
                            Рассматриваем только профессионалов. Если директор просит знакомого «ломани по-дружбе» и защита обходится за день, всё ок. Если через 2 дня хакер понимает, что снял 8 слоёв и сколько ещё впереди — неизвестно, то «по дружбе» уже не выйдет, и заказчику дешевле купить лицензию, чем оплатить работу (не говоря о том, что на выходные поковырять много кто возьмёт, а отказаться от основной работы и месяц разбираться с непонятно какими ожиданиями — немного).
                              0
                              угу.
                              На мелкосериных вещах это вполне рационально.
                              Но на решениях, где стоит несколько сотен тысяч копий целесообразность такого решения именно как защиты — вызывает сомнения.


                              А восемь слоев в железной защите это как-то немного странно :)

              Only users with full accounts can post comments. Log in, please.