Защита MQL5-программ: пароли, ключи, ограничение по времени, удаленная проверка лицензий
Введение
Большинство разработчиков нуждаются в защите своих кодов. В этой статье будет представлено несколько различных способов защиты MQL5-программ. Все примеры статьи относятся к советникам, однако те же способы могут быть применены к индикаторам и скриптам. Статья начинается с простой защиты при помощи пароля, затем рассматриваются генераторы ключей, лицензирование заданных счетов и защита при помощи ограничения по времени.
Затем вводится понятие удаленного сервера лицензий (remote licence server). Реализация удаленного вызова процедур любого XML-RPC сервера из MetaTrader 5 описана в статье "MQL5-RPC - Удаленный вызов процедур из MQL5: доступ к Web-сервисам и анализ данных Automated Trading Championship 2011". Этим решением я воспользуюсь для реализации удаленного лицензирования. Также будет приведен способ расширения данного решения за счет кодирования base64, рассмотрены аспекты PGP-шифрования для построения сверхнадежной защиты советников и индикаторов на MQL5.
Мне известно, что MetaQuotes Software Corp. реализовала некоторые варианты лицензирования программ в сервисе "Маркет". Это очень хорошо для всех разработчиков, однако не снижает актуальность идей, представленных в этой статье. Только совместное использование обоих решений позволит усилить защиту и обеспечить надежную защиту программ от кражи.
1. Защита при помощи пароля
Начнем с самого простого. Наиболее часто используется защита программного обеспечения при помощи пароля или лицензионного ключа. При первом запуске после установки пользователь получает диалоговое окно с запросом ввода пароля данной копии программы (например, серийные номера Microsoft Windows или Microsoft Office) и если введенный пароль соответствует, то пользователь может использовать одну зарегистрированную копию программы. Для ввода пароля можно использовать входной параметр или текстовое поле для ввода пароля. Пример такой заглушки приведен ниже.
В коде инициализируется поле CChartObjectEdit, которое используется для ввода пароля. Пароль, введенный пользователем, сравнивается со значениями заданного массива разрешенных паролей. Проверка пароля производится в методе OnChartEvent() после получения события CHARTEVENT_OBJECT_ENDEDIT.
//+------------------------------------------------------------------+ //| PasswordProtectedEA.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <ChartObjects/ChartObjectsTxtControls.mqh> CChartObjectEdit password_edit; const string allowed_passwords[] = { "863H-6738725-JG76364", "145G-8927523-JG76364", "263H-7663233-JG76364" }; int password_status = -1; string password_message[] = { "ПАРОЛЬ НЕПРАВИЛЬНЫЙ. Торговля запрещена.", "Пароль верный." }; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- password_edit.Create(0, "password_edit", 0, 10, 10, 260, 25); password_edit.BackColor(White); password_edit.BorderColor(Black); password_edit.SetInteger(OBJPROP_SELECTED, 0, true); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- password_edit.Delete(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if (password_status>0) { //--- пароль правильный } } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- if (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "password_edit" ) { password_status = -1; for (int i=0; i<ArraySize(allowed_passwords); i++) if (password_edit.GetString(OBJPROP_TEXT) == allowed_passwords[i]) { password_status = i; break; } if (password_status == -1) password_edit.SetString(OBJPROP_TEXT, 0, password_message[0]); else password_edit.SetString(OBJPROP_TEXT, 0, password_message[1]); } } //+------------------------------------------------------------------+
Этот метод простой, однако он не будет работать в случае, если его опубликуют на веб-сайте с взломанными ключами. Автор советника ничто не сможет сделать до выпуска новой версии и занесения украденного пароля в черный список.
2. Генератор ключа
Генераторы ключей (key generators) - это механизм, позволяющий использовать набор паролей на базе предопределенных правил. Рассмотрим его работу на примере кода заглушки. В примере, приведенном ниже, ключ должен состоять из трех чисел, разделенных двумя знаками "-", т.е. допустимый формат пароля имеет вид XXXXX-XXXXX-XXXXX.
Первое число должно без остатка делится на 3, второе число должно быть делителем 4, а третье число должно делиться на 5. Таким образом, разрешенными паролями могут быть 3-4-5, 18000-20000-20000 или более сложный 3708-102792-2844770.
//+------------------------------------------------------------------+ //| KeyGeneratorProtectedEA.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <ChartObjects/ChartObjectsTxtControls.mqh> #include <Strings/String.mqh> CChartObjectEdit password_edit; CString user_pass; const double divisor_sequence[] = { 3.0, 4.0, 5.0 }; int password_status = -1; string password_message[] = { "ПАРОЛЬ НЕПРАВИЛЬНЫЙ. Торговля запрещена.", "Пароль верный." }; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- password_edit.Create(0, "password_edit", 0, 10, 10, 260, 25); password_edit.BackColor(White); password_edit.BorderColor(Black); password_edit.SetInteger(OBJPROP_SELECTED, 0, true); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- password_edit.Delete(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if (password_status==3) { //--- пароль правильный } } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- if (id == CHARTEVENT_OBJECT_ENDEDIT && sparam == "password_edit" ) { password_status = 0; user_pass.Assign(password_edit.GetString(OBJPROP_TEXT)); int hyphen_1 = user_pass.Find(0, "-"); int hyphen_2 = user_pass.FindRev("-"); if (hyphen_1 == -1 || hyphen_2 == -1 || hyphen_1 == hyphen_2) { password_edit.SetString(OBJPROP_TEXT, 0, password_message[0]); return; } ; long pass_1 = StringToInteger(user_pass.Mid(0, hyphen_1)); long pass_2 = StringToInteger(user_pass.Mid(hyphen_1 + 1, hyphen_2)); long pass_3 = StringToInteger(user_pass.Mid(hyphen_2 + 1, StringLen(user_pass.Str()))); // PrintFormat("%d : %d : %d", pass_1, pass_2, pass_3); if (MathIsValidNumber(pass_1) && MathMod((double)pass_1, divisor_sequence[0]) == 0.0) password_status++; if (MathIsValidNumber(pass_2) && MathMod((double)pass_2, divisor_sequence[1]) == 0.0) password_status++; if (MathIsValidNumber(pass_3) && MathMod((double)pass_3, divisor_sequence[2]) == 0.0) password_status++; if (password_status != 3) password_edit.SetString(OBJPROP_TEXT, 0, password_message[0]); else password_edit.SetString(OBJPROP_TEXT, 0, password_message[1]); } } //+------------------------------------------------------------------+
Разумеется, количество знаков числа может быть каким угодно, а расчеты могут быть более сложными. Также можно добавить переменную, связанную со свойствами железа (серийный номер HDD или информация о CPUID). В этом случае для запуска советника придется запускать дополнительный генератор, использующий аппаратную привязку.
Результат может служить в качестве входного параметра для генератора и сгенерированный пароль будет корректным только для определенного железа. Это ограничивает тех, кто изменил конфигурацию компьютера или использует VPS для работы советника, но это можно решить раздачей двух или трех действительных паролей. Такой способ используется в сервисе "Маркет".
3. Привязка к счету
Поскольку номер счета трейдера у конкретного брокера является уникальным, этот факт можно использовать для разрешения работы советника на одном или нескольких счетах.
В этом случае для получения сведений о счете достаточно воспользоваться функциями AccountInfoString(ACCOUNT_COMPANY) и AccountInfoInteger(ACCOUNT_LOGIN) и сравнить их результаты с предварительно заданными значениями разрешенных счетов:
//+------------------------------------------------------------------+ //| AccountProtectedEA.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" const string allowed_broker = "MetaQuotes Software Corp."; const long allowed_accounts[] = { 979890, 436290, 646490, 225690, 279260 }; int password_status = -1; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- string broker = AccountInfoString(ACCOUNT_COMPANY); long account = AccountInfoInteger(ACCOUNT_LOGIN); printf("Сервер = %s", broker); printf("Номер счета = %d", account); if (broker == allowed_broker) for (int i=0; i<ArraySize(allowed_accounts); i++) if (account == allowed_accounts[i]) { password_status = 1; Print("Пароль верный"); break; } if (password_status == -1) Print("Работа советника на данном счете не разрешена."); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if (password_status == 1) { //--- пароль верный } }
Это простая, но вполне мощная защита. Недостатком данного способа является необходимость перекомпиляции советника для каждого нового счета, добавляемого в список счетов.
4. Ограничение работы программы по времени
Ограничение по времени (time-limit protection) удобно для временных лицензий, например, для ознакомительных (trial) версий программ или при выдаче лицензий на месяц или год. Этот способ может быть использован в советниках и индикаторах.
Первая идея состоит в том, чтобы проверять серверное время, и на его основе давать пользователю возможность работы с индикатором или советником в течение заданного периода времени. После истечения срока функциональность программы может быть полностью или частично заблокирована.
//+------------------------------------------------------------------+ //| TimeLimitProtectedEA.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" datetime allowed_until = D'2012.02.11 00:00'; int password_status = -1; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- printf("Работа советника разрешена до %s", TimeToString(allowed_until, TIME_DATE|TIME_MINUTES)); datetime now = TimeCurrent(); if (now < allowed_until) Print("Проверка советника прошла успешно. Текущая дата: " + TimeToString(now, TIME_DATE|TIME_MINUTES)); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if (TimeCurrent() < allowed_until) { } else Print("Срок работы советника истек."); }
Единственным недостатком данного способа является необходимость отдельной компиляции советника для каждого пользователя.
5. Удаленная проверка лицензий
Было бы неплохо иметь полный контроль над лицензиями - возможность их блокировки или расширения ознакомительного периода для конкретных пользователей. Это легко сделать при помощи вызовов MQL5-RPC, которые посылали бы на сервер запросы с номером счета и получали бы значение, разрешающее работу скрипта в trial-режиме или запрещающее работу.
Пример реализации:
from SimpleXMLRPCServer import SimpleXMLRPCServer from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler class RequestHandler( SimpleXMLRPCRequestHandler ): rpc_path = ( '/RPC2', ) class RemoteLicenseExample(SimpleXMLRPCServer): def __init__(self): SimpleXMLRPCServer.__init__( self, ("192.168.2.103", 9099), requestHandler=RequestHandler, logRequests = False) self.register_introspection_functions() self.register_function( self.xmlrpc_isValid, "isValid" ) self.licenses = {} def addLicense(self, ea_name, broker_name, account_number): if ea_name in self.licenses: self.licenses[ea_name].append({ 'broker_name': broker_name, 'account_number' : account_number }) else: self.licenses[ea_name] = [ { 'broker_name': broker_name, 'account_number' : account_number } ] def listLicenses(self): print self.licenses def xmlrpc_isValid(self, ea_name, broker_name, account_number): isValidLicense = False ea_name = str(ea_name) broker_name = str(broker_name) print "Request for license", ea_name, broker_name, account_number try: account_number = int(account_number) except ValueError as error: return isValidLicense if ea_name in self.licenses: for license in self.licenses[ea_name]: if license['broker_name'] == broker_name and license['account_number'] == account_number: isValidLicense = True break print "License valid:", isValidLicense return isValidLicense if __name__ == '__main__': server = RemoteLicenseExample() server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024221) server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024223) server.listLicenses() server.serve_forever()
Этот простой XML-RPC сервер, реализованный на языке Python, содержит две предопределенных лицензии для MetaTrader 5. Лицензии установлены для советника с именем "RemoteProtectedEA", запущенного на счетах 1024221 и 1024223 демо-сервера MetaQuotes (access.metatrader5.com:443). В промышленных масштабах хранение лицензий лучше осуществлять в базе данных Postgresql или любой другой БД. Приведенный выше пример хорошо иллюстрирует работу системы удаленного лицензирования.
Если требуется помощь в установке языка Python, прочитайте статью "MQL5-RPC - Удаленный вызов процедур из MQL5: доступ к Web-сервисам и анализ данных Automated Trading Championship 2011".
В советнике, который использует удаленную проверку лицензии, требуется подготовить удаленный MQL5-RPC вызов метода isValid(), который возвращает значение true или false в зависимости от статуса лицензии.
В примере, приведенном ниже, показан советник, основанный на привязке к счету:
//+------------------------------------------------------------------+ //| RemoteProtectedEA.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <MQL5-RPC.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayInt.mqh> #include <Arrays\ArrayString.mqh> #include <Arrays\ArrayBool.mqh> bool license_status=false; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- адрес сервера проверки лицензий CXMLRPCServerProxy s("192.168.2.103:9099"); if(s.isConnected()==true) { CXMLRPCResult *result; //--- получение данных о счете string broker= AccountInfoString(ACCOUNT_COMPANY); long account = AccountInfoInteger(ACCOUNT_LOGIN); printf("Сервер = %s",broker); printf("Номер счета = %d",account); //--- получение статуса лицензии с сервера CArrayObj* params= new CArrayObj; CArrayString* ea = new CArrayString; CArrayString* br = new CArrayString; CArrayInt *ac=new CArrayInt; ea.Add("RemoteProtectedEA"); br.Add(broker); ac.Add((int)account); params.Add(ea); params.Add(br); params.Add(ac); CXMLRPCQuery query("isValid",params); result=s.execute(query); CArrayObj *resultArray=result.getResults(); if(resultArray!=NULL && resultArray.At(0).Type()==TYPE_BOOL) { CArrayBool *stats=resultArray.At(0); license_status=stats.At(0); } else license_status=false; if(license_status==true) printf("Лицензия действительна."); else printf("Лицензия недействительна."); delete params; delete result; } else Print("Ошибка соединения с сервером лицензий."); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(license_status==true) { //--- лицензия действительна } } //+------------------------------------------------------------------+
Выполнив оба скрипта, аналогичным образом вы сможете использовать удаленное лицензирование для ваших счетов. Метод удаленного лицензирования может использоваться как для лицензий с ограничением во времени, так и для парольных лицензий, которые могут быть деактивированы после истечения ознакомительного периода. Например, вы предоставили кому-нибудь советника на 10 дневный тестовый период. Если продукт ему не понравился, вы деактивируете лицензию, если понравился, вы можете активировать лицензию на любой заданный период времени.
6. Безопасное шифрование лицензий
В методе, представленном в предыдущем разделе, использовались вызовы удаленных процедур (Remote Procedure Calls) для обмена информацией между сервером лицензий и клиентским терминалом. В этом подходе существует возможность взлома при помощи перехвата пакетов трафика зарегистрированной копии советника. При помощи программы-сниффера хакер способен захватить все TCP-пакеты, пересылаемые между двумя машинами. Эту проблему мы решим с помощью кодировки base64 для отправляемых данных о счете и принимаемых зашифрованных сообщений.
Специалисты также могли бы использовать PGP-шифрование и/или разместить весь код в DLL для дополнительной защиты. Фактически это будет другое RPC-сообщение (как в русской матрешке), которое затем будет конвертировано в данные MQL5.
Первый шаг - добавить поддержку кодирования/декодирования в кодировку base64 для MQL5-RPC. К счастью, для MetaTrader 4 есть решение https://www.mql5.com/ru/code/8098, поэтому нужно лишь cконвертировать код в MQL5.
//+------------------------------------------------------------------+ //| Base64.mq4 | //| Copyright © 2006, MetaQuotes Software Corp. | //| MT5 version © 2012, Investeo.pl | //| https://www.metaquotes.net | //+------------------------------------------------------------------+ #property copyright "Copyright © 2006, MetaQuotes Software Corp." #property link "https://www.metaquotes.net" static uchar ExtBase64Encode[64]={ 'A','B','C','D','E','F','G','H','I','J','K','L','M', 'N','O','P','Q','R','S','T','U','V','W','X','Y','Z', 'a','b','c','d','e','f','g','h','i','j','k','l','m', 'n','o','p','q','r','s','t','u','v','w','x','y','z', '0','1','2','3','4','5','6','7','8','9','+','/' }; static uchar ExtBase64Decode[256]={ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -2, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }; //+------------------------------------------------------------------+ //| Кодирование данных в base64 | //+------------------------------------------------------------------+ void Base64Encode(string in,string &out) { int i=0,pad=0,len=StringLen(in); //---- пройдемся и закодируем while(i<len) { //---- извлекаем байты int b3,b2,b1=StringGetCharacter(in,i); i++; if(i>=len) { b2=0; b3=0; pad=2; } else { b2=StringGetCharacter(in,i); i++; if(i>=len) { b3=0; pad=1; } else { b3=StringGetCharacter(in,i); i++; } } //---- int c1=(b1 >> 2); int c2=(((b1 & 0x3) << 4) | (b2 >> 4)); int c3=(((b2 & 0xf) << 2) | (b3 >> 6)); int c4=(b3 & 0x3f); out=out+CharToString(ExtBase64Encode[c1]); out=out+CharToString(ExtBase64Encode[c2]); switch(pad) { case 0: out=out+CharToString(ExtBase64Encode[c3]); out=out+CharToString(ExtBase64Encode[c4]); break; case 1: out=out+CharToString(ExtBase64Encode[c3]); out=out+"="; break; case 2: out=out+"=="; break; } } //---- } //+------------------------------------------------------------------+ //| Декодирование данных из Base64 | //+------------------------------------------------------------------+ void Base64Decode(string in,string &out) { int i=0,len=StringLen(in); int shift=0,accum=0; //---- идем до конца while(i<len) { //---- извлечем код int value=ExtBase64Decode[StringGetCharacter(in,i)]; if(value<0 || value>63) break; // конец или неверная кодировка //---- развернем код accum<<=6; shift+=6; accum|=value; if(shift>=8) { shift-=8; value=accum >> shift; out=out+CharToString((uchar)(value & 0xFF)); } i++; } //---- } //+------------------------------------------------------------------+
Подробное описание кодировки base64 можно найти в Википедии.
Тестовый пример скрипта кодирования и декодирования в base64 приведен ниже:
//+------------------------------------------------------------------+ //| Base64Test.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <Base64.mqh> //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- string to_encode = "<test>Abrakadabra</test>"; string encoded; string decoded; Base64Encode(to_encode, encoded); Print(encoded); Base64Decode(encoded, decoded); Print(decoded); } //+------------------------------------------------------------------+
Результат работы скрипта:
DK 0 Base64Test (EURUSD,H1) 16:21:13 Original string: <test>Abrakadabra</test> PO 0 Base64Test (EURUSD,H1) 16:21:13 Base64 encoded string: PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+ FM 0 Base64Test (EURUSD,H1) 16:21:13 Base64 decoded string: <test>Abrakadabra</test>
Правильность кодирования может быть легко проверена при помощи 4 строк на языке Python:
import base64 encoded = 'PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+' decoded = base64.b64decode(encoded) print decoded <test>Abrakadabra</test>
Вторым шагом является шифрование результата работы XML-RPC в кодировку base64 (по принципу матрешки):
import base64 from SimpleXMLRPCServer import SimpleXMLRPCServer from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler class RequestHandler( SimpleXMLRPCRequestHandler ): rpc_path = ( '/RPC2', ) class RemoteLicenseExampleBase64(SimpleXMLRPCServer): def __init__(self): SimpleXMLRPCServer.__init__( self, ("192.168.2.103", 9099), requestHandler=RequestHandler, logRequests = False) self.register_introspection_functions() self.register_function( self.xmlrpc_isValid, "isValid" ) self.licenses = {} def addLicense(self, ea_name, broker_name, account_number): if ea_name in self.licenses: self.licenses[ea_name].append({ 'broker_name': broker_name, 'account_number' : account_number }) else: self.licenses[ea_name] = [ { 'broker_name': broker_name, 'account_number' : account_number } ] def listLicenses(self): print self.licenses def xmlrpc_isValid(self, ea_name, broker_name, account_number): isValidLicense = False ea_name = str(ea_name) broker_name = str(broker_name) print "Request for license", ea_name, broker_name, account_number try: account_number = int(account_number) except ValueError as error: return isValidLicense if ea_name in self.licenses: for license in self.licenses[ea_name]: if license['broker_name'] == broker_name and license['account_number'] == account_number: isValidLicense = True break print "License valid:", isValidLicense # additional xml encoded with base64 xml_response = "<?xml version='1.0'?><methodResponse><params><param><value><boolean>%d</boolean></value></param></params></methodResponse>" retval = xml_response % int(isValidLicense) return base64.b64encode(retval) if __name__ == '__main__': server = RemoteLicenseExampleBase64() server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024221) server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", 1024223) server.listLicenses() server.serve_forever()
После того как лицензия зашифрована, мы можем использовать метод MQL5-RPC для преобразования расшифрованного сообщения обратно в данные MQL5:
//+------------------------------------------------------------------+ //| RemoteProtectedEABase64.mq5 | //| Copyright 2012, Investeo.pl | //| http://www.investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2012, Investeo.pl" #property link "http://www.investeo.pl" #property version "1.00" #include <MQL5-RPC.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayInt.mqh> #include <Arrays\ArrayString.mqh> #include <Arrays\ArrayBool.mqh> #include <Base64.mqh> bool license_status=false; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- адрес сервера проверки лицензий CXMLRPCServerProxy s("192.168.2.103:9099"); if(s.isConnected()==true) { CXMLRPCResult *result; //--- получение данных о счете string broker= AccountInfoString(ACCOUNT_COMPANY); long account = AccountInfoInteger(ACCOUNT_LOGIN); printf("Сервер = %s",broker); printf("Номер счета = %d",account); //--- получение статуса лицензии с сервера CArrayObj* params= new CArrayObj; CArrayString* ea = new CArrayString; CArrayString* br = new CArrayString; CArrayInt *ac=new CArrayInt; ea.Add("RemoteProtectedEA"); br.Add(broker); ac.Add((int)account); params.Add(ea); params.Add(br); params.Add(ac); CXMLRPCQuery query("isValid",params); result=s.execute(query); CArrayObj *resultArray=result.getResults(); if(resultArray!=NULL && resultArray.At(0).Type()==TYPE_STRING) { CArrayString *stats=resultArray.At(0); string license_encoded=stats.At(0); printf("encoded license: %s",license_encoded); string license_decoded; Base64Decode(license_encoded,license_decoded); printf("decoded license: %s",license_decoded); CXMLRPCResult license(license_decoded); resultArray=license.getResults(); CArrayBool *bstats=resultArray.At(0); license_status=bstats.At(0); } else license_status=false; if(license_status==true) printf("Лицензия действительна."); else printf("Лицензия недействительна."); delete params; delete result; } else Print("Ошибка соединения с сервером лицензий."); //--- return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- if(license_status==true) { //--- лицензия действительна } } //+------------------------------------------------------------------+
Результат запуска скрипта показывает, что сервер RemoteLicenseExampleBase64 запущен:
KI 0 RemoteProtectedEABase64 (EURUSD,H1) 19:47:57 Сервер = MetaQuotes Software Corp. GP 0 RemoteProtectedEABase64 (EURUSD,H1) 19:47:57 Номер счета = 1024223 EM 0 RemoteProtectedEABase64 (EURUSD,H1) 19:47:57 <?xml version='1.0'?><methodResponse><params><param><value><string>PD94bWwgdmVyc2lvbj0nMS4wJz8+PG1ldGhvZFJlc3BvbnNlPjxwYXJhbXM+PHBhcmFtPjx2YWx1ZT48Ym9vbGVhbj4xPC9ib29sZWFuPjwvdmFsdWU+PC9wYXJhbT48L3BhcmFtcz48L21ldGhvZFJlc3BvbnNlPg==</string></value></param></params></methodResponse> DG 0 RemoteProtectedEABase64 (EURUSD,H1) 19:47:57 encoded license: PD94bWwgdmVyc2lvbj0nMS4wJz8+PG1ldGhvZFJlc3BvbnNlPjxwYXJhbXM+PHBhcmFtPjx2YWx1ZT48Ym9vbGVhbj4xPC9ib29sZWFuPjwvdmFsdWU+PC9wYXJhbT48L3BhcmFtcz48L21ldGhvZFJlc3BvbnNlPg== FL 0 RemoteProtectedEABase64 (EURUSD,H1) 19:47:57 decoded license: <?xml version='1.0'?><methodResponse><params><param><value><boolean>1</boolean></value></param></params></methodResponse> QL 0 RemoteProtectedEABase64 (EURUSD,H1) 19:47:57 Лицензия действительна.
Как видите, тело XML-RPC сообщения содержит строку, которая фактически является XML-RPC сообщением в кодировке base64. Это шифрованное сообщение дешифруется в XML-строку, которая затем преобразовывается в данные MQL5.
7. Рекомендации против декомпиляции
Если код MQL5 будет декомпилирован, даже самые безопасные средства защиты станут уязвимы для опытных взломщиков. После некоторых поисков я нашел в сети сайт, который предлагает декомпилятор MQL5, но я подозреваю, что это подделка, сделанная с целью забрать деньги наивных людей, которые хотят украсть чей-то код. Во всяком случае, я не пробовал его в деле и могу ошибаться. Даже если такое решение и существует, вы должны быть готовыми сделать более надежную защиту, передавая в зашифрованном виде входные параметры или индексы передаваемых объектов.
Хакеру будет очень сложно получить правильные входные параметры защищенного советника или увидеть правильные параметры защищенного индикатора, что делает его использование бесполезным. Также можно отправлять правильные параметры в случае, если счет имеет лицензию или передавать заведомо неверные данные в открытом виде в случае, если счет не имеет лицензии. Для этого можно воспользоваться PGP-шифрованием. Даже если код будет декомпилирован, будут переданы данные, зашифрованные с помощью PGP-ключа, а параметры советника будут расшифрованы только в случае совпадения номера счета и PGP-ключа.
Выводы
В этой статье я представил несколько способов защиты MQL5-программ. Также изложена идея и реализация удаленной проверки лицензий посредством вызовов MQL5-RPC с поддержкой кодирования base64. Надеюсь, что статья послужит основой для новых идей обеспечения защиты программ на MQL5. Исходные коды всех примеров прилагаются к статье.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/359
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Кстати, мы скоро выпускаем большую систему рейтингов для всех пользователей. Это будет очень интересное новшество - "даешь трейдера 80 уровня!".
"Также можно добавить переменную, связанную со свойствами железа (серийный номер HDD или информация о CPUID). В этом случае для запуска советника придется запускать дополнительный генератор, использующий аппаратную привязку.
Результат может служить в качестве входного параметра для генератора и сгенерированный пароль будет корректным только для определенного железа. Это ограничивает тех, кто изменил конфигурацию компьютера или использует VPS для работы советника, но это можно решить раздачей двух или трех действительных паролей. Такой способ используется в сервисе "Маркет"."
А где можно узнать про привязку к железу поподробнее или найти пример исходника? И что значит -" Такой способ используется в сервисе "Маркет"."? То есть, если я выкладываю скомпилированный файл эксперта на продажу в сервисе "Маркет", то в него как то автоматически включается привязка к железу? Или нужно отдавать исходники а сотрудники Метаквотов туда дополнительно включают привязку к железу?
А можно ли привязку к железу реализовать на MQL4? (Большинство ДЦ до сих пор работают на MT4)
А где можно узнать про привязку к железу поподробнее или найти пример исходника? И что значит -" Такой способ используется в сервисе "Маркет"."? То есть, если я выкладываю скомпилированный файл эксперта на продажу в сервисе "Маркет", то в него как то автоматически включается привязка к железу? Или нужно отдавать исходники а сотрудники Метаквотов туда дополнительно включают привязку к железу?
А можно ли привязку к железу реализовать на MQL4? (Большинство ДЦ до сих пор работают на MT4)
Опубликована статья Защита MQL5-программ: пароли, ключи, ограничение по времени, удаленная проверка лицензий:
Автор: investeo
3. Привязка к счету- пробовал привязать работу советника к счёту. Защита не работает, выдает сообщение /работа советника на данном счёте не разрешена/ , а советник всё равно торгует. При компиляции ошибок нет. Не пойму в чём дело.
Здравствуйте, у меня вопрос, хочу продавать советника на своем сайте, но не знаю как это сделать, есть кто, кто может помочь?
Заранее спасибо!