В период активной разработки программного обеспечения на Delphi 7 возникла необходимость в защите коммерчески успешных продуктов от несанкционированного копирования. Перед созданием собственного решения был проведен анализ существующих систем защиты с использованием инструментов IDA Pro, FileMon, ProcMon и RegMon и т.п. Объектами исследования стали десктопные программы с trial-периодами и механизмами регистрации, которые в то время составляли основу рынка ПО.
Большинство исследованных решений использовали привязку к уникальным идентификаторам персонального компьютера:
серийному номеру тома диска;
MAC-адресу сетевой карты;
серийному номеру жесткого диска (через интерфейсы контроллера);
аппаратным ключам (LPT или USB).
Анализ выявил ряд критических уязвимостей в архитектуре этих защит. В большинстве случаев функции проверки возвращали дискретный результат (логическое “истина” или “ложь”), что позволяло нейтрализовать защиту путем замены одного байта в инструкции ветвления. Аппаратные ключи также не обеспечивали достаточной надежности: часто они использовали внешние DLL, работу которых можно было эмулировать, создав заглушку с заранее сохраненными ответами на запросы в интерфейс ключа.
Дополнительно отмечалось практически полное отсутствие механизмов детектирования трассировки кода. Контроль пробного периода часто сводился к проверке наличия скрытого файла на диске или записи в реестре Windows, что легко отслеживалось мониторами системных вызовов.
На основе полученных данных мною была разработана библиотека TViorProtect. При ее проектировании были поставлены следующие задачи:
Отказ от явных проверок. Исключение любых операций сравнения лицензионных данных, возвращающих результат в виде логического флага.
Исключение строковых констант. Текстовые строки являются основными маркерами для быстрого поиска функций лицензирования при статическом анализе.
Скрытие системных вызовов (IAT Hiding). Прямые вызовы WinAPI-функций (например, GetVolumeInformation или DeviceIoControl) позволяют быстро определить методы привязки защиты к оборудованию.
Модульная архитектура. Разделение функционала (получение HWID, модуль регистрации) на отдельные исполняемые модули для затруднения комплексного анализа.
Использование скрытых флагов состояния. Применение механизмов межпроцессного взаимодействия для контроля целостности системы защиты.
Распределение логики по коду приложения. Внедрение переменных и функций, работоспособность которых напрямую зависит от успешного прохождения процедур верификации лицензии.
Метод вычисляемых переходов: реализация “Математического лабиринта”
В TViorProtect реализован принцип “непрерывных вычислений”, исключающий использование условных инструкций (JZ/JNZ) для проверки лицензии. Вместо проверки состояния флага программа вычисляет адрес следующей функции на основе набора входных параметров: контрольной суммы лицензии, уникального ID оборудования (HWID), целостности исполняемого файла и системной даты.
Любое несоответствие данных приводит к искажению результирующего адреса. Процессор совершает переход (call) по некорректному указателю, что вызывает исключение Access Violation и немедленную остановку процесса.
Четыре этапа динамической верификации
Алгоритм проверки разделен на каскад функций, где каждая последующая точка входа зависит от корректности вычислений на предыдущем шаге.
1. Проверка структуры лицензии и целостности EXE
В процедуре “Check” происходит чтение файла регистрации и вычисление CRC32 всей структуры “FSecurityRec”. Также на этом этапе рассчитывается дельта контрольной суммы самого исполняемого файла (“bsdt”):
CRC := inttostr(StrToInt64(CalCRC(FSecurityRec))+ (cMagicNumber)); // Вычисление разницы между эталонным CRC32 (из лицензии) и текущим файлом bsdt := inttostr(StrToInt64(trim(bsdt)) - GetFileCRC32(GExeFileName)); // Расчет адреса для первого перехода (ActionGood) i := integer(@TSecExtFileChkV2.Action) - integer(StrToInt64(CRC) - StrToInt64(trim(ValS))); i := RoutineAdress(@i, 1200); asm call Pointer(i); end;
2. Привязка к оборудованию (HWID)
В функции “ActionGood” происходит инициализация переменной “SecShift”. Её значение привязано к аппаратному обеспечению через дельту “ser” (разница между HWID в лицензии и HWID текущего компьютера). Если программа запущена на “чужом” железе, переменная “ser” будет ненулевой, что сделает невозможным переход на следующий этап: “ActionGood2”.
3. Защита от перевода системной даты (ActionGood2)
Этот блок реализует контроль “ватерлинии” времени. Дата последнего запуска скрыто сохраняется в системной директории Windows в специальном формате (количество символов пробела в файле соответствует дате).
В процедуре “ActionGood2” текущая дата сравнивается с сохраненной. Если пользователь попытался продлить триал-период путем перевода системных часов назад, переменная “DateShift” принимает значение, которое искажает итоговый адрес:
// Если дата корректна (не переводилась назад), разность в скобках дает 0 SecShift := int64(@TSecExtFileChkV2.ActionGoodLast) + BiosDateMagic + ((GetTickCountAdr - GTickCount) div 256) + (SecShiftIfDebug - 525) + ((DateShift - Round(CurrDate)) - 672);
При обнаружении отката даты слагаемое “((DateShift - Round(CurrDate)) - 672)” становится ненулевым, что уводит указатель “SecShift” от корректной точки входа в финальную процедуру.
4. Финальная активация и анти-трассировка
Для защиты от пошагового анализа используется функция “RoutineAdress”, которая вводит зависимость адреса от времени выполнения блока кода.
// Делитель 835 (APeriod) определяет чувствительность к паузам в отладчике l := (SecShift - StrToInt64(bsdt)) + ((GetTickCountAdr - GTickCount) div 835); asm call Pointer(l); end; // Переход в ActionGoodLast
Только при условии, что:
Контрольная сумма лицензии верна;
HWID компьютера совпадает с записанным;
Системная дата не переводилась назад;
Исполняемый файл не модифицирован (bsdt = 0);
Проверка пройдена на полной скорости процессора без пауз (GetTickCount),программа попадает в “ActionGoodLast”. В этой точке происходит финальная замена функций-заглушек (например, “IntToStrVP”) на их реальные реализации, и приложение становится полностью работоспособным.
Исключение строковых констант: защита от статического анализа
При исследовании программных защит первичным этапом анализа обычно является поиск текстовых строк. Обнаружение в бинарном файле фраз вроде “Registration Required”, “Trial Period Expired” или “Invalid License Key” позволяет взломщику мгновенно локализовать функции верификации лицензии и точки принятия решения в коде.
В TViorProtect реализован механизм полного исключения открытых строк, имеющих отношение к работе системы защиты. Все служебные сообщения, заголовки диалоговых окон и пути к системным файлам хранятся в исполняемом файле в виде зашифрованных массивов байт.
Механизм динамического декодирования строк
Для сокрытия текстовых данных используется таблица подстановок и константное смещение. В исходном коде строковые константы представлены набором нечитаемых символов:
// Пример зашифрованного заголовка окна "Registration" csCodeStroke_RegistCaptEN = #$41#$11#$15#$AE#$A#$10#$B2#$3B#$1A#$E#$14#$F3; // Пример сообщения об обнаружении попытки взлома csCodeStroke_DefaultActMsg = #$12#$C#$22#$0#$3#$8#$21#$10#$1#$12#$10#$10#$E8#$9#$29#$9...;
Декодирование происходит непосредственно перед использованием строки с помощью функции “RecodeConstantStrokes”. Данная функция выполняет преобразование каждого байта на основе массива “csCodedStrokesArr” и числового смещения “csCodedStrokesSymbShift”:
function RecodeConstantStrokes(ACodedStroke: String): String; var i: integer; begin Result := ""; for i := 1 to Length(ACodedStroke) do begin // Получение символа через таблицу подстановок и добавление смещения Result := Result + chr(csCodedStrokesArr[ord(ACodedStroke[i])] + csCodedStrokesSymbShift); end; end;
Результат применения методики
Применение данного подхода приводит к следующим результатам:
Неэффективность автоматизированного поиска: Утилиты для извлечения строк не находят в теле программы осмысленных фраз, связанных с регистрацией или проверкой ограничений.
Затруднение ориентации в коде: В отладчике или дизассемблере вместо понятных текстовых аргументов функций видны лишь ссылки на закодированные массивы.
Необходимость реверса алгоритма расшифровки: Чтобы понять логику работы конкретного модуля, исследователь вынужден сначала локализовать функцию “RecodeConstantStrokes”, восстановить таблицу “csCodedStrokesArr” и произвести обратное преобразование всех констант.
Такая методика скрывает семантику кода и значительно увеличивает время, необходимое на первичный анализ системы защиты, отсекая возможность быстрого поиска критических участков программы.
Скрытый импорт (IAT Hiding): минимизация системных вызовов в заголовке файла
Таблица импорта (Import Address Table, IAT) исполняемого файла содержит список всех внешних функций (WinAPI), которые программа запрашивает у операционной системы. Для исследователя этот список является картой функциональных возможностей защиты. Наличие в IAT таких функций, как “DeviceIoControl”, “GetVolumeInformation” или “EnumWindows”, однозначно указывает на механизмы привязки к оборудованию или поиск активных отладчиков.
В TViorProtect реализована технология “IAT Hiding”, которая позволяет исключить критические системные вызовы из таблицы импорта. Программа не импортирует функции статически, а находит их адреса в оперативной памяти непосредственно во время выполнения.
Механизм динамического разрешения адресов
Реализация скрытого импорта в библиотеке состоит из трех этапов:
1. Объявление процедурных переменных
Вместо использования стандартного подхода, для каждой WinAPI-функции объявляется переменная соответствующего процедурного типа:
var // Указатель на функцию управления устройствами DeviceIoControlAdr: function(hDevice: THandle; dwIoControlCode: Cardinal; lpInBuffer: Pointer; nInBufferSize: Cardinal; lpOutBuffer: Pointer; nOutBufferSize: Cardinal; var lpBytesReturned: Cardinal; lpOverlapped: POverlapped): LongBool; stdcall; // Указатель на функцию передачи сообщений PostMessageAdr: function(hWnd: HWND; Msg: Cardinal; wParam: Integer; lParam: Integer): LongBool; stdcall;
2. Использование зашифрованных имен библиотек и функций
Для поиска адреса функции необходимо передать ее имя в систему. Чтобы эти имена не были обнаружены при статическом анализе, они хранятся в зашифрованном виде (согласно методике, описанной в Пункте 2):
csCodeStroke_kernel32_dll = #$E0#$5#$A#$2#$8#$41#$15#$26#$C7#$8#$14#$4; // kernel32.dll csCodeStroke_DeviceIoControl = #$1D#$12#$C2#$1E#$2#$3#$1F#$1#$3#$4#$31#$11#$7#$15#$4;
3. Динамическая привязка через AssignAPIFunction
В процессе инициализации защиты вызывается вспомогательная функция “AssignAPIFunction”. Она расшифровывает названия, загружает необходимые библиотеки в память и получает адреса функций:
function AssignAPIFunction(ALibraryName, AFunctionName: String): Pointer; var h: THandle; begin Result := nil; // Декодирование имени DLL и получение дескриптора модуля h := GetModuleHandle(PChar(RecodeConstantStrokes(ALibraryName))); if (h = 0) then h := LoadLibrary(PChar(RecodeConstantStrokes(ALibraryName))); // Получение адреса функции по ее расшифрованному имени if (h <> 0) then Result := GetProcAddress(h, PChar(RecodeConstantStrokes(AFunctionName))); end; // Пример инициализации указателя DeviceIoControlAdr := AssignAPIFunction(csCodeStroke_kernel32_dll, csCodeStroke_DeviceIoControl);
Технические последствия для анализа
Применение скрытого импорта приводит к следующим результатам:
Пустая таблица импорта: Утилиты анализа PE-заголовков показывают минимальный набор базовых библиотек. Функции, отвечающие за криптографию, работу с железом и анти-отладочные приемы, в списке отсутствуют.
Непрямые вызовы (Indirect Calls): В дизассемблере вместо понятных команд “call DeviceIoControl” видны инструкции обращения по адресу переменной: “call [indirect]”. Это затрудняет автоматизированное построение графа вызовов и понимание логики работы программы.
Обфускация системного взаимодействия: Поскольку все вызовы WinAPI происходят через динамически вычисленные указатели, исследователь вынужден тратить время на ручное определение того, какая именно системная функция вызывается в данный момент времени.
В сочетании с защитой строковых констант, технология IAT Hiding делает исполняемый файл “непрозрачным” для инструментов статического анализа, вынуждая исследователя переходить к значительно более трудоемкому этапу, динамической отладке под защитой таймингов и контрольных сумм.
Модульная изоляция: разделение логики на внешние компоненты
В классических системах защиты весь функционал от получения серийных номеров оборудования до генерации лицензионных файлов, обычно находится внутри основного исполняемого файла. Это упрощает задачу исследователю, так как вся логика защиты доступна для анализа в рамках одного процесса.
В TViorProtect применен принцип модульной изоляции: критически важные блоки вынесены в отдельные исполняемые файлы, которые упакованы в ресурсы основной программы. Это не только уменьшает объем кода в основном EXE-файле, но и значительно усложняет восстановление алгоритма генерации кодов.
Механизм хранения и маскировки модулей
Библиотека оперирует двумя вспомогательными модулями:
BITMAP_KFC (Key From Computer): модуль для сбора HWID и генерации кода пользователя.
BITMAP_SRV2 (Registration Service): модуль, отвечающий за верификацию регистрационного кода и создание файла лицензии в системной директории.
Данные модули внедряются в ресурсы основной программы как объекты типа “RCData” под именами “BITMAP_KFC” и “BITMAP_SRV2”. Использование имен, характерных для графических ресурсов, является дополнительным приемом маскировки содержимого.
Шифрование ресурсов на этапе разработки
Для предотвращения обнаружения вложенных исполняемых файлов антивирусными сканерами или сигнатурными анализаторами используется процедура “ResourceAddCoding”. Она выполняется в среде разработки Delphi (Design-time) при установке или компиляции компонента:
// Кодирование ресурсов при дизайне или загрузке компонента if (csDesigning in Owner.ComponentState) then begin s := FindResourceDCRFile; // Процедура ResourceAddCoding шифрует вложенные EXE внутри .dcr файла i := ResourceAddCoding(s, FAddCodingStroke); end;
Динамическое извлечение и запуск
В процессе работы основной программы модули извлекаются, расшифровываются и запускаются только в момент необходимости (например, при вызове окна регистрации). Алгоритм работы включает следующие шаги:
Извлечение: Программа загружает ресурс в “TResourceStream”.
Декодирование: Поток расшифровывается в памяти функцией “CoderResBufferInp”.
Сохранение: Декодированный файл записывается во временную директорию ("%TEMP%") под динамически сгенерированным именем.
Запуск: Модуль запускается через “CreateProcess”.
// Пример извлечения модуля генерации кода пользователя (KFC) sFileName := GetKFCFileName(True); Res := TResourceStream.Create(Hinstance, RecodeConstantStrokes(csCodeStroke_BITMAP_KFC), RT_RCDATA); try iSize := Res.Size; // Расшифровка EXE-модуля в оперативной памяти CoderResBufferInp(Res.Memory, "", iSize); // Сохранение во временный файл для последующего запуска Res.SaveToFile(GetKFCFileName(False)); finally Res.Free; end;
Межпроцессное взаимодействие (IPC)
Обмен данными между основным приложением и запущенными модулями (передача кода пользователя или регистрационного ключа) реализован через проецируемые файлы (Memory Mapped Files). Для предотвращения мониторинга имен объектов в именах каналов связи используются временные метки (текущие секунды), что делает имя объекта уникальным для каждой сессии запуска.
Преимущества модульного подхода
Разделение ответственности: Основной исполняемый файл “не знает” алгоритма создания лицензионного файла, эта логика полностью изолирована в модуле SRV2.
Затруднение отладки: Попытка отладки основного процесса не дает доступа к коду модулей, так как они работают в собственных адресных пространствах.
Вынос ключевых узлов защиты во внешние компоненты превращает монолитную задачу взлома в исследование распределенной системы, где части мозаики скрыты и появляются только на короткое время в зашифрованном виде.
Скрытые флаги состояния
В TViorProtect активно применяется механизм глобальных атомов Windows (Global Atoms). Атомы представляют собой системную таблицу строк, доступную всем запущенным процессам. Поскольку атомы хранятся в памяти операционной системы, этот флаг остается доступным даже при перезапуске отдельных модулей защиты.
Скрытый счетчик инцидентов и “тихий” выход
Наиболее важная роль атомов реализована в процедуре, которая отвечает за реакцию системы на обнаружение взлома или некорректных данных. Вместо немедленного прекращения работы, библиотека ведет скрытый учет количества срабатываний защиты через цепочку атомов с именами-идентификаторами:
// Пример каскадной проверки и добавления атомов при срабатывании защиты if GlobalFindAtomAdr("45F282CEEF6FCE8") = 0 then GlobalAddAtomAdr("45F282CEEF6FCE8") else if (@TSecExtFileChkV2.TimerProc <> FSaveRedirForTimerProc.Addr) or (GlobalFindAtomAdr("45F282CEEF6FCE7") <> 0) then // При достижении критического состояния или подмене таймера - принудительное завершение работы ОС WinExit(EWX_SHUTDOWN or EWX_FORCE) else if GlobalFindAtomAdr("45F282CEEF6FCE6") = 0 then GlobalAddAtomAdr("45F282CEEF6FCE6") else if GlobalFindAtomAdr("45F282CEEF6FCE5") = 0 then GlobalAddAtomAdr("45F282CEEF6FCE5") // ... и далее по списку до FCE3 else GlobalAddAtomAdr("45F282CEEF6FCE7");
Технические особенности данного подхода:
Устойчивость к перезапуску приложения: Глобальные атомы сохраняются в системе до тех пор, пока не будут удалены явно или пока не произойдет перезагрузка Windows. Это означает, что если исследователь закроет программу и запустит ее снова, защита “вспомнит” о предыдущих инцидентах.
Скрытая логика “смерти” программы: Использование GUID-подобных строк (например, “45F282CEEF6FCE8”) в качестве имен атомов дезориентирует аналитика. Без анализа алгоритма невозможно понять, что эти записи в системной таблице являются счетчиком попыток взлома.
Радикальные меры защиты: Если цепочка атомов заполнена или обнаружена попытка перехвата адреса процедуры “TimerProc”, библиотека инициирует не просто закрытие приложения, а принудительное завершение работы всей операционной системы через вызов “WinExit” с флагами “EWX_SHUTDOWN” и “EWX_FORCE”.
Использование атомов в качестве межпроцессного семафора и регистратора инцидентов позволяет защите сохранять свое состояние независимо от жизненного цикла основного процесса, что значительно усложняет многократные попытки автоматизированного подбора ключей или трассировки кода.
Интеграция в бизнес-логику: распределенная проверка состояния системы
Одной из наиболее распространенных ошибок при использовании сторонних протекторов является локализация всей защиты в одной точке - процедуре проверки лицензии при старте программы. Опытный исследователь может найти эту точку и, применив патч, заставить программу считать, что проверка пройдена.
В TViorProtect реализована стратегия “распределенной защиты”, при которой работоспособность основных функций приложения напрямую зависит от текущего состояния модуля безопасности. Логика лицензирования “размазывается” по пользовательскому коду, делая невозможным полноценное использование программы даже после обхода первичных проверок.
1. “Отравленные” функции
Библиотека предоставляет разработчику аналоги стандартных функций преобразования типов, такие как “IntToStrVP” и “StrToInt64VP”. В обычном состоянии эти функции являются “заглушками” и возвращают некорректные данные или служебные строки.
Например, реализация “IntToStrVP” по умолчанию выглядит следующим образом:
function TViorProtect.IntToStrVP(Value: Int64): string; register; begin // До успешной регистрации функция возвращает строку "Registration Required" Result := RecodeConstantStrokes(csCodeStroke_RegistCaptEN); end;
Если исследователь просто перепрыгнет через процедуру “Check”, все вызовы “IntToStrVP” в его приложении будут возвращать текстовое сообщение вместо строкового представления числа. Это приведет к порче данных в отчетах, интерфейсе или к падению программы в случайных местах.
2. Динамическое восстановление через RedirectCall
Реальная работоспособность этих функций восстанавливается только в самой последней точке “математического лабиринта”, процедуре “ActionGoodLast”. Это происходит с помощью техники перехвата функций в оперативной памяти (Runtime Patching):
procedure TSecExtFileChkV2.ActionGoodLast; begin // Только на этом этапе "пустышка" заменяется на реальную функцию IntToStrVP_OK RedirectCall(@TViorProtect.IntToStrVP, @TViorProtect.IntToStrVP_OK, @FSaveRedirForIntToStr); // Установка финального значения проверочной константы ValLast99FF32 := RecodeConstantStrokes(csCodeStroke_99FF32); end;
Функция “RedirectCall” модифицирует начало функции-заглушки в памяти, устанавливая безусловный переход (JMP) на корректную реализацию. Таким образом, функции активации защиты и бизнес-логика приложения становятся неразрывными.
3. Проверочные константы (Validation Anchors)
Библиотека содержит несколько свойств (например, “ValueDF642R”, “Value659H1J”, “Value99FF32”), которые принимают валидные значения только после успешного прохождения всех этапов верификации. Разработчик может использовать их в критических узлах своей программы:
if ViorProtect1.Value99FF32 <> "99FF32" then PerformCriticalError; // Дополнительная проверка в произвольном месте кода
Свойство “Value659H1J” имеет дополнительную особенность: оно доступно для чтения только один раз, что предотвращает его использование в простых циклах проверки и затрудняет эмуляцию состояния защиты.
Результаты для исследователя
Данная методика создает эффект “отложенной ошибки”. Взломщик может успешно нейтрализовать стартовое окно регистрации, но столкнется с неработоспособностью базовых функций программы через неопределенное время. Для восстановления функциональности ему придется:
Найти все места использования “VP-функций” в коде.
Вручную эмулировать поведение “RedirectCall”.
Искать и подавлять все скрытые проверки констант-якорей.
Такой подход превращает взлом в трудоемкий процесс исправления множества вторичных ошибок, что делает создание полноценного “кряка” экономически нецелесообразным.
Эпилог
Программы, использующие данную библиотеку защиты, успешно эксплуатировались в течение длительного времени. На текущий момент представленные в ней методы не являются актуальными для современных систем, что позволило мне поделиться подробным описанием архитектуры TViorProtect. Несмотря на то, что в основе решения лежал принцип “security through obscurity”, для своего времени этого уровня защиты оказалось достаточно.
Эффективность выбранного подхода подтверждалась отзывами исследователей, пытавшихся запустить нелицензионную копию приложения: основным фактором раздражения для них становилось постоянное принудительное завершение работы операционной системы, заложенное в алгоритм реакции на вмешательство. Насколько мне известно, защита ни разу не была взломана полностью. Для повышения надежности и затруднения анализа алгоритмов на уровне импортов конечные исполняемые файлы дополнительно обрабатывались сторонними упаковщиками (EXE packers).
По понятным причинам я не выкладываю в открытый доступ полный исходный код библиотеки. Инструмент, хоть и ретро, но всё ещё рабочий и может быть полезен в будущем. Однако для тех, кто хочет не только прочитать теорию, но и применить свои навыки на практике, доступна скомпилированная демонстрационная программа: DemoViorProtect.zip (программа не содержит вредоносного кода и предназначена исключительно для образовательных целей, для запуска необходим режим Администратора, как впрочем и для любой программы с этой защитой). Критерием успешного взлома или создания валидного файла лицензии будет считаться запуск программы без какого-либо модального окна и вывод текста в надписях:
UserName: XXXXX
Version: A.B.C.D
Registrate date: DD.MM.YYYY
И также при вводе в текстовое поле цифрового значения, справа в надписи оно будет продублировано.
В заключение отмечу, что библиотека TViorProtect в свое время распространялась как коммерческий компонент для Delphi 7. Чтобы исключить возможность анализа алгоритмов по скомпилированным модулям (.dcu), весь исходный код проходил через процедуру обфускации. Поскольку на тот момент на рынке не было подходящих инструментов для обфускации кода на языке Pascal, мне пришлось разработать собственную утилиту для автоматической трансформации исходных текстов перед сборкой дистрибутива компонента.
