- Получение общего списка свойств терминала и программы
- Номер сборки терминала
- Тип и лицензия программы
- Режимы работы терминала и программы
- Разрешения
- Проверка сетевых подключений
- Вычислительные ресурсы: память, диск, процессор
- Характеристики экрана
- Строковые свойства терминала и программы
- Настраиваемые свойства: лимит баров и язык интерфейса
- Привязка программы к свойствам среды исполнения
- Проверка состояния клавиатуры
- Проверка статуса и причины остановки MQL-программы
- Программное закрытие терминала и код возврата
- Обработка ошибок времени исполнения программы
- Пользовательские ошибки
- Управление отладкой
- Предопределенные переменные
- Предопределенные константы языка MQL5
Привязка программы к свойствам среды исполнения
В качестве примера работы со свойствами, описанными в предыдущих разделах, рассмотрим востребованную задачу привязки MQL-программы к аппаратному окружению, чтобы защитить её от копирования. Когда программа распространяется через MQL5 Маркет, привязку обеспечивает сам сервис. Однако если программа разрабатывается в заказном порядке, привязать её можно либо к номеру счета, либо к имени клиента, либо к доступным свойствам терминала (компьютера). Первое не всегда удобно, потому что многие трейдеры имеют несколько реальных счетов (вероятно, у разных брокеров), не говоря уже о демо-счетах с ограниченным сроком существования. Второе может быть вымышленным или слишком расхожим. Поэтому мы реализуем прототип алгоритма для привязки программы к выбранному набору свойств окружения. Более серьезные схемы защиты, вероятно, могли бы использовать DLL и напрямую читать аппаратные метки устройств из системы Windows, однако не каждый клиент согласится запускать потенциально небезопасные библиотеки.
Наш вариант защиты представлен в скрипте EnvSignature.mq5. Его суть заключается в том, чтобы вычислить хеши от заданных свойств среды и создать на их основе уникальную сигнатуру (оттиск).
Хешированием называется специальная обработка произвольной информации, в результате чего создается новый блок данных, обладающий следующими характеристиками (их гарантирует используемый алгоритм):
- совпадение значений хешей для двух исходных наборов данных означает практически со 100% вероятностью, что данные идентичны (вероятность случайного совпадения пренебрежимо мала);
- если исходные данные изменятся, значение их хеша также изменится;
- по значению хеша невозможно математически восстановить исходные данные (они остаются в секрете), если только не выполнить полный перебор возможных исходных значений (с увеличением их исходного размера и при отсутствии знаний о структуре, задача нерешаема в обозримом будущем);
- размер хеша фиксирован (не зависит от объема исходных данных).
Предположим, одно из свойств среды описано строкой: "TERMINAL_LANGUAGE=Russian". Его можно получить простой инструкцией вроде следующей (упрощенно):
string language = EnumToString(TERMINAL_LANGUAGE) +
|
Разумеется, фактический язык будет соответствовать настройкам. Имея гипотетическую функцию хеширования Hash, мы можем вычислить сигнатуру.
string signature = Hash(language); |
Когда свойств будет больше, мы просто повторим процедуру для всех из них или запросим хеш от сложенных строк (пока это псевдо-код, а не часть реальной программы).
string properties[];
|
Полученную сигнатуру пользователь может сообщить разработчику программы, который её особым образом "подпишет", получив строку валидации, подходящую только для этой сигнатуры. "Подпись" также базируется на хешировании и требует знания некоего секрета (пароля-фразы), известного только разработчику и зашитого в программу (для фазы проверки).
Затем разработчик передаст строку валидации пользователю, и тот сможет запускать программу, указав эту строку в параметрах.
При запуске без строки валидации программа должна генерировать новую сигнатуру для текущего окружения, выводить её в журнал и завершать работу (эту информацию полагается передать разработчику). С неверной строкой валидации программа должна выводить сообщение об ошибке и также завершать работу.
Для самого разработчика можно предусмотреть несколько режимов запуска: с сигнатурой, но без строки валидации (чтобы сгенерировать последниюю), или с сигнатурой и строкой валидации (здесь программа подпишет сигнатуру заново и сравнит с заданной строкой валидации — просто для проверки).
Прикинем, насколько избирательной получится такая защита. Ведь все-таки привязка здесь выполняется не к уникальному идентификатору чего-либо.
В следующей таблице приведена статистика по двум характеристикам: размеру экрана и объему оперативной памяти. Очевидно, что значения со временем будут меняться, но примерное распределение будет оставаться таким же: несколько значений характеристик будет наиболее популярно, а некоторые "новые" продвинутые и "старые", уходящие из оборота, составят уменьшающиеся "хвосты".
Screen |
1920x1080 |
1536x864 |
1440x900 |
1366x768 |
800x600 |
---|---|---|---|---|---|
RAM |
21% |
7% |
5% |
10% |
4% |
4Gb 20% |
4.20 |
1.40 |
1.00 |
2.0 |
0.8 |
8Gb 20% |
4.20 |
1.40 |
1.00 |
2.0 |
0.8 |
16Gb 15% |
3.15 |
1.05 |
0.75 |
1.5 |
0.6 |
32Gb 10% |
2.10 |
0.70 |
0.50 |
1.0 |
0.4 |
64Gb 5% |
1.05 |
0.35 |
0.25 |
0.5 |
0.2 |
Наибольшую обеспокоенность для нас должны представлять ячейки с наибольшими величинами, потому что они означают совпадение сигнатур (если не ввести в них элемент случайности, о чем пойдет речь ниже). В данном случае наиболее вероятны два сочетания характеристик в верхнем левом углу, каждое по 4.2%. Но это только две характеристики. Если добавить в оцениваемое окружение язык интерфейса, таймзону, количество ядер, рабочий путь к данным (желательно общий, так как он содержит имя пользователя Windows), то количество потенциальных совпадений заметно снизится.
Для хеширования воспользуемся встроенной функцией CryptEncode (она будет описана в разделе Криптография), поддерживающей метод хеширования SHA256. Согласно его имени, он производит хеш длиной 256 бит, то есть 32 байта. Если бы нам потребовалось показать его пользователю, то при переводе в текст шестандцатеричного представления мы получили бы строку длиной 64 символа.
Чтобы сделать сигнатуру короче, мы преобразуем её с помощью кодировки Base64 (она также поддерживается функцией CryptEncode и парной ей CryptDecode), что даст строку длиной 44 символа. В отличие от односторонней операции хеширования, кодирование в Base64 обратимо, то есть из него можно восстановить исходные данные.
Основную работу выполняет класс EnvSignature. В нем определена строка data, где должны накапливаться некие фрагменты, описывающие среду. Публичный интерфейс состоит из нескольких перегруженных версий функции append для добавления строк со свойствами среды. По сути, они состыковывают название запрошенного свойства и его значение, используя некий абстрактный элемент, возвращаемый виртуальным методом pepper, в качестве связующего звена. Производный класс определит его как конкретную строку (но она может быть и пустой).
class EnvSignature
|
Для добавления в объект произвольной строки имеется универсальный метод append, который вызывается в вышеприведенных методах.
bool append(const string s)
|
При желании, разработчик может добавить в хешируемые данные так называемую "соль". Это массив со случайно генерируемыми данными, необходимый для усложнения подбора хешей. Иными словами, каждая генерация сигнатуры будет отличаться от предыдущей, несмотря на то, что окружение остается постоянным. Реализация данной возможности оставлена для самостоятельного изучения, как и другие более специфические аспекты защиты (такие как применение симметричного шифрования, динамическое вычисление "секрета").
Так как окружение состоит из известных свойств (их список ограничен константами MQL5 API), и не все они обладают достаточной уникальностью, наша защита, как мы подсчитали, может генерировать одинаковые сигнатуры для разных пользователей, если не использовать "соль". Совпадение сигнатур не позволит идентифицировать источник утечки лицензии, если она случилась.
Поэтому повысить эффективность защиты можно за счет изменения способа представления свойств перед хешированием под каждого заказчика. Разумеется, сам способ не должен раскрываться. В рассматриваемой реализации, это сводится к изменению содержимого метода pepper и перекомпиляции продукта. Это может быть накладно, но зато позволяет отказаться от случайной "соли".
При заполненной строке свойств, мы можем сгенерировать сигнатуру. Этим занимается метод emit.
string emit() const
|
Суть её работы сводится к тому, чтобы добавить к данным некий "секрет" (последовательность байтов, известную только разработчику и внутри программы) и вычислить для общей строки хеш. "Секрет" получается из виртуального метода secret, который также определит производный класс.
Полученный байтовый массив с хешем кодируется в строку с помощью Base64.
Теперь у нас на очереди самая главная функция класса — check. Именно она "подписывает" сигнатуру у разработчика и проверяет её у пользователя.
bool check(const string sig, string &validation)
|
При штатной работе (у пользователя) метод считает хеш от полученной сигнатуры, дополненной "секретом", и сравнивает его со значением из валидационной строки (её предварительно нужно раскодировать из Base64 в "сырое" бинарное представление хеша). Если два хеша совпали, проверка пройдена успешно: валидационная строка соответствует набору свойств. Очевидно, что пустая валидационная строка (или строка, введенная наобум) проверку не пройдет.
На компьютере разработчика, в исходном коде утилиты для "подписи", должен быть определен макрос I_AM_DEVELOPER, в результате чего пустая валидационная строка обрабатывается иначе. В этом случае полученный хеш кодируется с помощью Base64, и эта строка передается наружу через тот же параметр validation. Таким образом, утилита сможет вывести разработчику готовую валидационную строку для заданной сигнатуры.
Для создания объекта необходим конкретный производный класс, в котором определены строки с "секретом" и "перчиком".
// ПРЕДУПРЕЖДЕНИЕ: измените макро на собственный набор случайных байтов
|
Выберем навскидку несколько свойств для заполнения сигнатуры.
void FillEnvironment(EnvSignature &env)
|
Теперь все готово для проверки нашей схемы защиты в функции OnStart. Но сначала обратим внимание на входные переменные. Поскольку одна и та же программа будет компилироваться в двух вариантах — для конечного пользователя и для разработчика — существует два набора входных переменных: для ввода регистрационных данных пользователем и для генерации этих данных на основе сигнатуры у разработчика. Входные переменные, предназначенные для разработчика, были описаны выше с помощью макроса INPUT. А для пользователя доступна только строка валидации.
input string Validation = ""; |
Когда строка пуста, программа соберет данные об окружении, сгенерирует новую сигнатуру и выведет её в журнал. На этом работа скрипта заканчивается, так как доступ к полезному коду пока не подтвержден.
void OnStart()
|
Если переменная Validation заполнена, проверяем её соответствие сигнатуре и завершаем работу в случае неудачи.
if(StringLen(Validation) == 0)
|
Если расхождений нет, алгоритм "проваливается" к рабочему коду программы.
На стороне разработчика (в той версии программы, что собрана с макросом I_AM_DEVELOPER) может быть введена сигнатура, и мы по ней восстанавливаем состояние объекта MyEnvSignature и рассчитываем строку валидации.
void OnStart()
|
Разработчик может указать не только сигнатуру, но и подпись: это приведет к продолжению исполнения кода, как у пользователя (в целях отладки).
При желании вы можете сымитировать изменение окружения, например, таким образом:
FillEnvironment(env);
|
Рассмотрим несколько тестовых логов.
При первом запуске скрипта EnvSignature.mq5 "пользователь" увидит примерно следующий лог (значения будут отличаться из-за различий среды):
Hash bytes:
|
Он "отправляет" сгенерированную сигнатуру "разработчику" (в ходе теста это мы сами, поэтому все роли "пользователя" и "разработчика" взяты в кавычки), который вводит её в утилиту подписи (скомпилированную с макросом I_AM_DEVELOPER), в параметр Signature. В результате, программа выдаст строку валидации:
Validation:YBpYpQ0tLIpUhBslIw+AsPhtPG48b0qut9igJ+Tk1fQ= |
"Разработчик" "отправляет" её обратно "пользователю", и тот, введя её в параметр Validation, получит активированный скрипт:
Hash bytes:
|
Для демонстрации действенности защиты давайте продублируем скрипт в виде сервиса: для этого скопируем файл в папку MQL5/Services/MQL5Book/p4/ и в исходном коде заменим строку:
#property script_show_inputs |
на
#property service |
Откомпилируем сервис, создадим и запустим его экземпляр, а во входных параметрах укажем полученную ранее строку валидации. В результате, сервис прервет работу (не доходя до инструкций с "полезным" кодом) с сообщением:
Hash bytes:
|
Дело в том, что среди свойств окружения у нас использована строка MQL_PROGRAM_TYPE. Поэтому выписанная лицензия на один тип программы не подойдет для программы другого типа, даже если она выполняется на компьютере того же пользователя.