An easy way to detect Guardant key emulators

    When working with the Guardant security key (no matter what model), the developer uses the appropriate APIs, while the mechanism for working with the device is hidden from him, not to mention the exchange protocol. He does not have a valid device handle on his hands, using only the gateway address (the so-called GuardantHandle) through which all the work is going. If the system has a key emulator (especially relevant for models up to Guardant Stealth II inclusive) using this gateway, the developer will not be able to determine whether it works with a real physical key or its emulation.

    Having asked a question in due time: “how to determine the availability of a physical key?”, I had to study a little excellent material presented by Pavel Agurov in the book “ USB Interface. The Practice of Use and Programming". After that, spend time analyzing API function calls from a three-megabyte object linking to an application in which all the" magic "of working with the key is actually hidden.

    As a result, a fairly simple solution to this problem that does not require the use of the original Guardant APIs appeared.
    The only minus is all terribly undocumented and technical support for the company's assets will not even consider your issues associated with using Guardant keys.
    and of course, at some point, all this code can simply stop slave a thief because of changes in Guardant drivers.
    But in the meantime, on April 27, 2013, all of this material is relevant and tested its performance on the drivers from version 5.31.78, up to the current date 6.00.101.

    The procedure will be something like this:
    1. Via SetupDiGetClassDevsA () we get a list of all the devices present.
    2. Check if the device is related to Guardant keys by checking the device’s GUID. (For Guardant, this parameter is {C29CC2E3-BC48-4B74-9043-2C6413FFA784})
    3. We get a symbolic link to each device by calling SetupDiGetDeviceRegistryPropertyA () with the parameter SPDRP_PHYSICAL_DEVICE_OBJECT_NAME.
    4. Let's open the device with ZwOpenFile () (CreateFile () unfortunately will not work here, because there will be difficulties when working with symbolic links).

    Now, having the real key handle on hand, instead of the pseudo-handle (gateway) provided by the Guardant API, we can get a description of its parameters by sending the corresponding IOCTL request. True, there is a slight nuance.

    Starting with Guardant Stealth III and above, the protocol for working with the key has changed, as a result, the IOCTL request constants and the contents of the incoming and outgoing buffers have changed. For the normal operation of the algorithm, it is desirable to support the capabilities of both old and new keys, so I will describe the differences:

    To begin with, the IOCTL constants look like this:

      GetDongleQueryRecordIOCTL = $E1B20008;
      GetDongleQueryRecordExIOCTL = $E1B20018;

    The first is for keys from Guardant Stealth I / II
    The second is for keys of Guardant Stealth III and above (Sign / Time / Flash / Code)

    When sending the first request to the device, we will expect the driver to return the following buffer to us:

      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; // Индекс для удаленного программирования

    In the case of newer keys and taking into account that the protocol has changed, sending the first request will not give us anything. More precisely, the request, of course, will be executed, but the buffer will come empty (nullified). Therefore, we send a second request to the new keys, which will return the data in a slightly different format:

      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;

    Here, a 512-byte block containing more detailed information about the key is already being returned. Unfortunately for some reason I can’t give you a full description of this structure, but I left the fields necessary for this article.

    The general code for receiving data about installed keys looks like this:

    procedure TEnumDonglesEx.Update;
      dwRequired: DWord;
      hAllDevices: H_DEV;
      dwInfo: DWORD;
      Data: SP_DEVINFO_DATA;
      Buff: array [0 .. 99] of AnsiChar;
      hDeviceHandle: THandle;
      NTSTAT, dwReturn: DWORD;
      DongleQueryRecord: TDongleQueryRecord;
      DongleQueryRecordEx: TDongleQueryRecordEx;
      SetLength(FDongles, 0);
      DWord(hAllDevices) := INVALID_HANDLE_VALUE;
        if not InitSetupAPI then
        hAllDevices := SetupDiGetClassDevsA(nil, nil, 0,
        if DWord(hAllDevices) <> INVALID_HANDLE_VALUE then
          FillChar(Data, Sizeof(SP_DEVINFO_DATA), 0);
          Data.cbSize := Sizeof(SP_DEVINFO_DATA);
          dwInfo := 0;
          while SetupDiEnumDeviceInfo(hAllDevices, dwInfo, Data) do
            dwRequired := 0;
            FillChar(Buff[0], 100, #0);
            if SetupDiGetDeviceRegistryPropertyA(hAllDevices, @Data,
              SPDRP_PHYSICAL_DEVICE_OBJECT_NAME, nil, @Buff[0], 100, @dwRequired)
              if CompareGuid(Data.ClassGuid, GrdGUID) then
                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,
                if NTSTAT = STATUS_SUCCESS then
                  if DeviceIoControl(hDeviceHandle, GetDongleQueryRecordIOCTL,
                    nil, 0, @DongleQueryRecord, SizeOf(TDongleQueryRecord),
                    dwReturn, nil) and (DongleQueryRecord.dwID <> 0) then
                    SetLength(FDongles, Count + 1);
                    FDongles[Count - 1].Data := DongleQueryRecord;
                    FDongles[Count - 1].PnPParentPath :=
                  Move(FlashBuffer[0], DongleQueryRecordEx.Unknown0[0], 512);
                  if DeviceIoControl(hDeviceHandle, GetDongleQueryRecordExIOCTL,
                    dwReturn, nil) then
                    DongleQueryRecordEx.wMask :=
                    DongleQueryRecordEx.wSN :=
                    DongleQueryRecordEx.dwID :=
                    DongleQueryRecordEx.dwPublicCode :=
                    DongleQueryRecordEx.wType :=
                    SetLength(FDongles, Count + 1);
                    ZeroMemory(@DongleQueryRecord, SizeOf(DongleQueryRecord));
                    DongleQueryRecord.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 :=
        if DWord(hAllDevices) <> INVALID_HANDLE_VALUE then

    This procedure enumerates all the keys and enters information about them into an array of TDongleQueryRecord structures, after which you can display this data to the user, well, or use them in any way directly in your application.


    As you can see, everything is quite simple, but in the Guardant API object modules this code is placed under a rather serious stacked virtual machine and is practically not available for analysis by a regular developer. In principle, there is nothing secret here, as you see, calls do not even use encryption of transmitted and received buffers, but for some reason the Guardant SDK developers did not consider it necessary to publish this information (though I still managed to get permission to publish this code, because as a result, some critical aspects of the key exchange protocol are not affected here).

    But let's not get distracted, you probably noticed in the above procedure the call to the GetPnP_ParentPath () function. This function returns the full path to the device from the root. Its implementation looks as follows:

      function GetPnP_ParentPath(Value: DWORD): string;
        hParent: DWORD;
        Buffer: array [0..1023] of AnsiChar;
        Len: ULONG;
        S: string;
        Result := '';
        if CM_Get_Parent(hParent, Value, 0) = 0 then
          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
            Len := Length(Buffer);
            CM_Get_DevNode_Registry_PropertyA(hParent, 15, nil,
              @Buffer[0], @Len, 0);
            S := string(PAnsiChar(@Buffer[0]));
            Result := S + '#' + Result;
        if Result = '' then
          Result := 'не определен';

    Actually (you will laugh) emulator detection will occur on the basis of this line.
    Typically, the device path is as follows:
    \ Device \ 00000004 # \ Device \ 00000004 # \ Device \ 00000044 # \ Device \ 00000049 # \ Device \ NTPNP_PCI0005 # \ Device \ USBPDO-3 #

    At least the text NTPNP_PCI or USBPDO will be present in it.
    Those. The PCI bus or HCD hub will be at least one of the ancestors.
    Because the emulator is still a virtual device, the path to it will look something like this:
    \ Device \ 00000040 # \ Device \ 00000040

    Accordingly, based on this information, you can implement a simple function:

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

    Well, in conclusion, I will describe a few more nuances that can be seen in the demo example attached to the article:

    • More recently, new Guardant Flash keys have appeared, consisting of two devices in one. Those. This is a security key and a regular flash drive. In the function UpdateUSBDevices () you can see how you can determine which of the DRIVE_REMOVABLE disks in the system are located in the key. In general, nothing new, the general principle was shown back in the demo example of safe disabling of Flash devices .
    • An example of obtaining a string representation of a PublicCode key is given (naturally, without a trailing control character, to avoid).
    • An example of obtaining a key release date based on its ID is given.

    A small nuance:
    The method described in the article will give a false / positive response when the user uses your Anywhere platform product: You can

    pick up an example here .

    Also popular now: