вторник, 24 февраля 2009 г.

Как получить hardware id. Исходники DiskID32 на Delphi

Потребовалось защитить приложение, написанное на Delphi, от копирования. Точнее, потребовалось обеспечить возможность привязывать приложение к конкретному компьютеру, на котором оно устанавливается.

Чтобы "привязать" программу к компьютеру, необходимо уметь генерировать уникальный идентификатор для его "железа" - hardware id. По hardware id генерируется серийный номер, который будет работать только на заданному компьютере. Такой подход активно используется для защиты ПО, хотя для пользователей он порой не слишком удобен - при апгрейде компьютера серийный номер может перестать работать.

Вопрос - как сгенерировать hardware id?

Изучение интернета показало, что имеется три основных варианта.
  1. Использовать MAC-адрес сетевой карты.
  2. Использовать информацию из WMI.
  3. Использовать серийный номер жесткого диска, на котором установлена операционная система.
Недостатки использования MAC-адреса описаны здесь:
  • Сетевых карт может быть несколько.
  • При включении bluetooth/wi-fi появляется новая "сетевая карта".
  • При отключенном сетевом проводе MAC-адрес не определяется.
Недостатки WMI:
  • В Me/2000/XP эта служба встроена, но в более ранних версиях Windows ее нужно доставлять.
  • Под Vista, с включенным UAC, без прав администратора доступ к WMI не получить.
  • Cлужба WMI может быть отключена.
  • Получение информации из WMI - очень медленный процесс. Несколько секунд задержки при запуске гарантированы.
Вариант с серийным номером жесткого диска - хорош. Тем более, что есть программа DiskID32, с открытыми исходными кодами, которая позволяет получать серийный номер жесткого диска в любых версиях Windows (от 9X до 64-bit), причем как при наличии прав администратора, так и при их отсутствии. Бери и пользуйся.

Беда только в том, что у нас приложение написано на Delphi. Значит и процедура получения hardware id нужна на Delphi (вариант с DLL не подходит). А DiskID32 написан на С++...

Делать нечего. Вооружился отладчиком и перевел исходные коды DiskID32 с C++ на Delphi. Основные проблемы, с которыми столкнулся: в Delphi нет битовый полей (они использовались при описании ряда структур) и нет полноценной адресной арифметики. К счастью, битовые поля оказались не принципиальны и я их просто отбросил. Операции над указателями (инкремент, декремент) заменил на аналогичные операции с массивами. Не слишком изящно, но работает. Код старался транслировать один в один, ничего не удаляя. Оригинальные исходники DiskID содержат код для определения MAC-адресов сетевых карт. В моем случае MAC-адрес не требовался, поэтому эти функции я не транслировал.

Код был протестирован на Windows XP, Vista, Server 2003 (с рейдом и без), а так же под Vista 64-bit. Под 9x код не тестировался, поэтому гарантий, что он заработает, нет никаких. Если вы найдете ошибку в коде - пожалуйста, сообщите мне о ней на email, указанный в исходных кодах.

Остается выразить благодарность датской компании Efaktum, для которой была выполнена эта трансляция кода, и которая любезно разрешила мне выложить переведенный код в открытый доступ.

Скачать исходные коды DiskID32 for Delphi.

Update: В первой версии исходных кодов нашлась ошибка, см. комментарии. Исправил ее и, заодно, перевел проект на Delphi 2010 (совместимость с Delphi 7 сохранена):

Скачать исходные коды DiskID32 for Delphi 2010.

Download source codes of DiskId32 for Delphi.
Download source codes of DiskId32 for Delphi (updated 16.01.2012)

View source codes of DiskId32 for Delphi.
Short description in English.

P.s. если вы не хотите, чтобы diskID выводил информацию на консоль, закомментируйте
макрос {$DEFINE PRINTING_TO_CONSOLE_ALLOWED}. В таком виде исходники пригодны для использования в неконсольных приложениях.

Update: Под x64 нет crtdll.dll. Вместо нее следует использовать msvcrt.dll. Т.е. в crtdll_wrapper объявить crt-функции следующим образом:
function crt_isspace(ch: Integer): Integer; cdecl; external 'msvcrt.dll' name 'isspace';
function crt_isalpha(ch: Integer): Integer; cdecl; external 'msvcrt.dll' name 'isalpha';
function crt_tolower(ch: Integer): Integer; cdecl; external 'msvcrt.dll' name 'tolower';
function crt_isprint(ch: Integer): Integer; cdecl; external 'msvcrt.dll' name 'isprint';
function crt_isalnum(ch: Integer): Integer; cdecl; external 'msvcrt.dll' name 'isalnum';
Библиотека crtdll.dll нужна только если требуется совместимость с win95.

Update: август 2013: Прислали на email обновление: "1. Сделал рабочей ReadIdeDriveAsScsiDriveInNT - char/ansichar ошибка; 2. Избавился от msvcrt.dll (переписал функции на дельфи); 3. Немного оптимизации." Обновленную версию следует брать в svn.

47 комментариев:

  1. Большое спасибо. А ошибка небольшая есть:
    В этом месте:
    if ('-' = HardDriveSerialNumber[ip]) then continue;

    Если попадается тире то происходит зацикливаение, потому что переменная ip не меняется :)

    ОтветитьУдалить
  2. Исправляется так:
    if ('-' = HardDriveSerialNumber[ip]) then
    begin
    Inc(ip);
    continue;
    end;

    ОтветитьУдалить
  3. Блин, а визуальных исходников нет?

    ОтветитьУдалить
  4. На семерке почему-то не работает, только в режиме совместимости с ХР

    ОтветитьУдалить
  5. А оригинал работает? Выше правильно указали на ошибку в коде - без этого исправления программа может зависать. Например, у меня на 64-битной семерке с исправлением работает, без него - зависает.

    ОтветитьУдалить
  6. Да сорри, тестил не я, оригинал работает. Не работала версия переведенная на 2009 делфи. Сейчас пытаюсь разобраться, перевести на 2010, при компиле ругается на

    StrCopy(@HardDriveSerialNumber,@serialNumber);
    StrCopy(@HardDriveModelNumber,@modelNumber);
    [DCC Error] hwid_impl.pas(1300): E2251 Ambiguous overloaded call to 'StrCopy';

    Я если честно нуб, не понимаю что тут делают @ки, ведь они возвращают адрес вроде.

    потом после компила уже ругается на
    assert(SIZE_rt_DiskInfo =92);
    assert(SIZE_IDSECTOR =256);
    и
    assert(sizeof(HardDriveSerialNumber) = 1024);

    ОтветитьУдалить
  7. В общем не работает попытка с нулевыми правами. Насчет совместимости с вин7 пока нет возможности попробовать)

    ОтветитьУдалить
  8. Символы @ нужны, чтобы привести массив символов к указателю на строку, оканчиваюующся нулем. Перевел проект на Delphi 2010, по идее, должен и на Delphi 2009 работать.

    ОтветитьУдалить
  9. Исходничег для 2010 не качаетсо...Спасибо

    ОтветитьУдалить
  10. Поправил ссылку, спасибо.

    ОтветитьУдалить
  11. Только вот теперь с выводом что-то не то - выдаёт только первые символы...
    Тоесть если серийник JDJSH2D выдаёт только J=)

    ОтветитьУдалить
  12. Функцию ConvertToString не добил. Поправил.

    ОтветитьУдалить
  13. попробовал определить серийник под Windows 7.
    Под админовскими правами все выдает правильно.
    Если как обычный пользователь, то выдает какую-то ерунду.

    ОтветитьУдалить
  14. А оригинал то не ерунду выдает?

    ОтветитьУдалить
  15. Хорошая работа, спасибо. Но есть пара нюансов:
    1. При вызове функций
    ReadPhysicalDriveInNTWithAdminRights(Dest)
    ReadIdeDriveAsScsiDriveInNT(Dest);
    ReadPhysicalDriveInNTWithZeroRights(Dest);
    ReadPhysicalDriveInNTUsingSmart(Dest);
    ReadDrivePortsInWin9X (Dest);

    массив Dest НЕ будет сохранять предыдущий результат; моя реализация выглядела так: вместо Dest в функциях используем
    ThisDest: tresults_array_dv;

    В конце каждой функции что-то типа
    if Result then begin
    drive := Length(Dest); //offset; using "drive" variable to memory saving
    SetLength(Dest, drive + count_drives_dv);
    for ijk := 0 to count_drives_dv - 1 do
    Dest[drive + ijk] := ThisDest[ijk];
    ThisDest := nil;
    end; //if Result

    2. Серийные номера жестких дисков не обязательно состоят только из цифр; на моих hdd samsung, hitachi и toshiba серийный номер содержит и латинские буквы
    В связи с этим заменил проверку

    if ((Chr(0) = HardDriveSerialNumber [0]) and
    (isalnum (serialNumber [0]) or isalnum (serialNumber [19])))

    на

    if (Chr(0) = HardDriveSerialNumber [0])

    3. Генерация id компьютера с привязкой только к одному параметру (hdd) не даст нормального результата - накрылся винт у клиента, он его сменил - а саппорт говорит, что программу вы не покупали... (думаю, именно для этого используют данный код) - плохо. Я делал привязку к CPU, hdd, video, MB и RAM. CPU brand string (и, возможно, S/N) узнается через ассемблер (в сети есть пример - им и воспользовался), hdd - пример выше, MB и RAM (производитель + модель, а для памяти и обьем) через WMI (при включенном UAC и отключенной службе на Windows 7 x64 определяется нормально). +параметры MB можно считать из реестра. Информация о видео также есть в сети - используя WinAPI без прав админа.
    А суть защиты - определяем количество допустимых замен (у меня 2) - и тогда определяем легальность использования. Думаю, идея ясна =)

    ОтветитьУдалить
  16. Этот комментарий был удален автором.

    ОтветитьУдалить
  17. Этот комментарий был удален автором.

    ОтветитьУдалить
  18. Этот комментарий был удален автором.

    ОтветитьУдалить
  19. Этот комментарий был удален автором.

    ОтветитьУдалить
  20. И еще маленькое дополнение: вместо
    SetLength(Dest, MAX_IDE_DRIVES - 1);
    нужно
    SetLength(Dest, MAX_IDE_DRIVES);
    думаю, ясно почему ;)

    ОтветитьУдалить
  21. Еще мелкий баг вылез

    "The only things i changed was a comment on (file: hwid_impl.pas, line n° 1328):
    Write(Format ('Controller Buffer Size on Drive___: %s bytes'+#$0D#$0A, [bufferSize]));

    because bufferSize was in comment (file: hwid_impl.pas, line n°1284):
    // bufferSize: array [0..32-1] of AnsiChar;"

    В SVN - поправил.

    ОтветитьУдалить
  22. При использовании данного кода заметил, что если программа висит в памяти, то невозможно корректно извлечь флешку через безопасное отключение. Может кто подскажет как с этим можно бороться?

    ОтветитьУдалить
  23. Ошибка в коде была - handle не закрывался в функции ReadPhysicalDriveInNTUsingSmart. Исправил, проблема должна уйти, проверьте. Изменения в модуле:

    https://dvsrc.googlecode.com/svn/trunk/Delphi/DiskId32Port/hwid_impl.pas

    Изменения помечены тегом 20120115. Если все в порядке, обновлю архив.

    ОтветитьУдалить
    Ответы
    1. Да, проблема устранена, огромное спасибо!

      Удалить
  24. Здравствуйте! А можно получить исходник проекта с формой. Чтобы вся информация выводилась непосредственно на форму, а не в консоль.
    Спасибо!

    ОтветитьУдалить
  25. У меня нет таких исходников. Сделать можно, например, так: завести функцию

    procedure MyWrite(s: String);

    которая пишет строки в TMemo, а не в консоль. Заменить в файле hwid_impl.pas все вызовы Write() на MyWrite()

    Функция MyWrite должна быть глобальной. TMemo в нее придется передавать так же через глобальную переменную.

    ОтветитьУдалить
  26. Здравствуйте. Заметил, что функция ReadPhysicalDriveInNTWithZeroRights не определяет vendorId для жестких дисков (по крайней мере на 4 ноутбуках). А для 1 ноута не вообще не определила серийный номер винчестера.
    Вот результат небольшого приложения, использующего только ReadPhysicalDriveInNTWithZeroRights
    HDD #0 of 0
    vendorId:
    modelNumber: HTS541060G9AT00
    productRevision: MB3OA60A
    serialNumber:
    DriveType: FixedMedia
    DriveSizeBytes: 60011642880
    STorageBUSTYPE: BusTypeAta

    К сожалению, протестировать Ваше приложения на этом ноутбуке не удалось, т.к. пользователи не очень опытные, а сам я к нему доберусь не очень скоро.
    Какие могут быть причины?
    Спасибо заранее.

    ОтветитьУдалить
  27. Добрый день. А оригинальный DiskID32 на этих ноутбуках результат выдает?

    Мне кажется, проблема может быть связана с типом HDD. DiskID32 не работает, как минимум, c RAID.

    Кроме того, вот здесь пишут: "дисковая подсистема может быть представлена рейдом, а там серийный номер стандартными средствами всё равно не получишь. И экзотические SSD тоже могут быть разные, которые могут и не поддерживать SMART. Обычно поддерживают, но ведь есть же SSD на PCI экспресе."

    уж не в SSD ли дело?

    ОтветитьУдалить
  28. К сожалению, SSD здесь не причем.
    кратко о проблемах.
    Задача - получить серийник винчестера с НУЛЕВЫМИ правами

    1. Проблема №1 - ни оригинальный DiskID32 , ни Ваш порт не выдает Vendor Id именно в функции ReadPhysicalDriveInNTWithZeroRights

    2. Проблема №2 - на одном из проверяемых винчестеров (HTS541060G9AT00 - это HITACHI на стареньком ACER TravelMate 2490) не определился серийный номер именно в функции ReadPhysicalDriveInNTWithZeroRights.

    Винчестер точно не ссд и точно не рейд.

    В ближайшие несколько недель попробую компонент от артсофт (ссылка есть там же)

    ОтветитьУдалить
  29. Если оригинальный не выдает, то порт тоже не выдаст.

    Посмотрел - действительно, Vendor ID на некоторых дисках пустой. Судя по всему, ошибки тут нет - просто производитель не указывает эту информацию.

    Что касается серийника, может дело в версии windows?
    Earlier versions of Windows did not
    provide the serial number and you have to go fetch the appropriate page using scsi passthrough.



    Замечу следующее. Функция ReadPhysicalDriveInNTWithZeroRights основана на использовании функции DeviceIoControl с кодом IOCTL_STORAGE_QUERY_PROPERTY. При этом используется STORAGE_PROPERTY_QUERY.PropertyId=StorageDeviceProperty. Между тем, этот параметр может принимать и другие значения.

    Вот здесь приведен пример программы, которая умеет получать уникальные идентификаторы SCSI дисков, используя STORAGE_PROPERTY_QUERY.QueryType = StorageDeviceIdProperty.

    А ведь есть еще STORAGE_PROPERTY_QUERY.QueryType=StorageDeviceUniqueIdProperty и другие значения...

    ОтветитьУдалить
    Ответы
    1. Вряд ли XP SP3 можно считать "ранней" версией :).
      Как нибудь попробую DeviceIoControl с другими значениями. Потом отпишусь.

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

    ОтветитьУдалить
    Ответы
    1. Насколько я понимаю, сложность в том, как связать логический раздел с конкретным жестким диском? Попробуйте воспользоваться функцией QueryDosDevice. Более подробно написано здесь и здесь.


      Вот здесь предлагают альтернативный способ - через анализ ключа реестра HKEY_LOCAL_MACHINE\SYSTEM\MountedDevices, причем на delphi.

      Удалить
    2. Спасибо за ответ.
      diskid32 выводит серийный номер, порядок контроллера (primary, secondary...) и тип (master, slave) для каждого физического диска. Мне надо вывести только серийный номер для того физического диска, который содержит логический диск, на котором запущена Windows в данный момент.

      Я могу вывести физический диск для того логического диска, на котором запущена Windows, например, используя WMI. Например получу такой вывод "DeviceID: \\.\PHYSICALDRIVE0".
      Проблема в том, что я не могу связать это с diskid32.

      Удалить
    3. Так если есть "\\.\PHYSICALDRIVE0", в чем проблема? Во всех функциях (ReadPhysicalDriveInNTWithAdminRights, ReadPhysicalDriveInNTWithZeroRights, ReadPhysicalDriveInNTUsingSmart) есть строка типа

      hPhysicalDriveIOCTL := CreateFile(PWideChar('\\.\PhysicalDrive' + IntToStr(drive)), 0, FILE_SHARE_READ or FILE_SHARE_WRITE, nil, OPEN_EXISTING, 0, 0);

      Возможно, ето не то, что в оригинале - я модули менял под себя, но в оригинале обязательно есть что-то подобное. Так вот, там такой вызов стоит в цикле, а Вам нужно будет сделать всего один вызов =)

      Удалить
    4. Функция getHardDriveComputerID заполняет глобальные переменные HardDriveSerialNumber и HardDriveModelNumber информацией о первом найденном HDD. Затем эти данные используются для генерации hardware id.

      Функция getHardDriveComputerID внутри себя содержит вызовы вспомогательных функций, использующих различные методы для поиска информации о дисках:
      ReadIdeDriveAsScsiDriveInNT, ReadDrivePortsInWin9X, ReadPhysicalDriveInNTWithZeroRights и т.д. Каждая из них возвращает массив типа tresults_array_dv. Массив содержит подробную информацию обо всех жестких дисках. К сожалению, в оригинале номер диска, который используется в пути "\\.\PhysicalDriveXXX" в этой структуре не сохраняется.

      Это легко исправить. Достаточно добавить в tresults_dv поле DriveId. В функции PrintIdeInfo, где заполняются все записи типа tresults_dv, после строки:

      Result.ControllerType := drive div 2;

      добавить строку

      Result.DriveId := drive;

      Теперь, при вызове любой из функций ReadIdeDriveAsScsiDriveInNT, ReadDrivePortsInWin9X и т.д. вы будете получать полную информацию обо всех дисках, причем для каждого диска будут указаны DriveModelNumber, DriveSerialNumber и соответствующий им driveId, используемый в "\\.\PhysicalDriveXXX".

      Остальное дело техники. Или можно, действительно, как предложили выше, перекроить код - вместо цикла делать один вызов. Тогда придется немножко изменить код во всех функциях ReadIdeDriveAsScsiDriveInNT, ReadDrivePortsInWin9X и т.д.

      Удалить
  31. Этот комментарий был удален автором.

    ОтветитьУдалить
  32. Здравствуйте. Подскажите, как можно определить кеш память диска?

    ОтветитьУдалить
  33. Спасибо автору за программу! Нашел то, что нужно)

    ОтветитьУдалить
  34. Вопрос, что будет если юзер запустит прогу на ОС без прав администратора и с включенным UAC, что вернет функция getHardDriveComputerID

    ОтветитьУдалить
  35. Спасибо автору !
    На Windows Server 2008 x64 не работает и diskid32 тоже, ID = 0.
    Посмотрел логи, постоянно выводится INVALID_HANDLE видимо при открытии устройства.

    ОтветитьУдалить
  36. Предлагаю написать все варианты ОС в сочетании с правами на которых программа работает/не работает.

    Я проверил (SSD, HDD):
    1. Windows 7 x64 Admin UAC off - ok
    2. Windows 7 x64 User UAC off - ok
    3. Windows 7 x64 Admin UAC on - ok
    4. Windows 7 x64 User UAC on - ok
    5. Windows Server 2008 R2 x64 Admin - error

    Интересно узнать про Windows XP, Windows 8, Windows Server 2003.

    ОтветитьУдалить
  37. Добрый день. Столкнулся с одной неприятной особенностью. Пи считывании серийного номера жесткого диска в DiskId32 вызывается процедура flipAndCodeBytes с параметром flip = 1 (при получении других свойств flip = 0). Так вот. Иногда получается так, что серийный номер возвращается не так, как ожидалось и для получения ожидаемого результата лучше вызывать flipAndCodeBytes с flip = 0. Такое поведение обнаружилось в Windows 8.1 x64. На берусь утверждать, но есть мысли, что на всех x64 версиях Windows. Как с этим лучше бороться???

    ОтветитьУдалить
    Ответы
    1. Этот комментарий был удален автором.

      Удалить
    2. Проблема описана здесь
      http://stackoverflow.com/questions/14623397/hdd-serial-number-flipped-every-2-bytes-in-windows-xp-vista-and-7-but-not-in-wi

      Похоже вариант только один - проверять версию windows и, если это win8 или выше, то вызывать flipAndCodeBytes с flip = 0.

      Удалить
    3. сделал фикс
      https://www.dropbox.com/s/lrdsy5yujurawez/DiskId32Port.7z?dl=0

      дойдут руки - перенесу проект на github

      Удалить
  38. Спасибо, разобрался. На самом деле, надо копнуть чуть глубже. Вызывать flipAndCodeBytes с flip = 0 нужно начиная с Windows Server 2008 R2, по крайней мере на доступных мне машинах дела обстоят именно так. Это Windows 6.1, ProductType in [VER_NT_DOMAIN_CONTROLLER, VER_NT_SERVER]

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