- Obtener una lista general de las propiedades del terminal y del programa
- Número de versión del terminal
- Tipo de programa y licencia
- Modos de funcionamiento del terminal y del programa
- Permisos
- Comprobación de las conexiones de red
- Recursos informáticos: memoria, disco y CPU
- Especificaciones de la pantalla
- Propiedades del terminal y de la cadena de programa
- Propiedades personalizadas: límite de barras e idioma de la interfaz
- Vincular un programa a propiedades en tiempo de ejecución
- Comprobar el estado del teclado
- Comprobar el estado del programa MQL y motivo de finalización
- Cierre programático del terminal y establecimiento de un código de retorno
- Tratamiento de errores en tiempo de ejecución
- Errores definidos por el usuario
- Gestión de depuración
- Variables predefinidas
- Constantes predefinidas del lenguaje MQL5
Vincular un programa a propiedades en tiempo de ejecución
Como ejemplo de trabajo con las propiedades descritas en las secciones anteriores, veamos la popular tarea de vincular un programa MQL a un entorno de hardware para protegerlo de la copia. Cuando el programa se distribuye a través de MQL5 Market, la vinculación la proporciona el propio servicio. Sin embargo, si el programa se desarrolla a medida, puede vincularse al número de cuenta, al nombre del cliente o a las propiedades disponibles del terminal (ordenador). La primera no siempre es conveniente, porque muchos operadores de trading tienen varias cuentas reales (probablemente con distintos brokers), por no hablar de las cuentas demo con un periodo de validez limitado. La segunda puede ser ficticia o demasiado corriente. Por lo tanto, implementaremos un algoritmo prototipo para vincular un programa a un conjunto seleccionado de propiedades del entorno. Los esquemas de seguridad más serios probablemente podrían utilizar una DLL y leer directamente las etiquetas de hardware del dispositivo desde Windows, pero no todos los clientes aceptarán ejecutar bibliotecas potencialmente inseguras.
Nuestra opción de protección se presenta en el script EnvSignature.mq5. El script calcula hashes a partir de las propiedades dadas del entorno y crea una firma única (huella) basada en ellas.
El hashing es un procesamiento especial de información arbitraria, como resultado del cual se crea un nuevo bloque de datos que tiene las siguientes características (están garantizadas por el algoritmo utilizado):
- La coincidencia de los valores hash de dos conjuntos de datos originales significa, con una probabilidad de casi el 100 %, que los datos son idénticos (la probabilidad de una coincidencia aleatoria es insignificante).
- Si los datos originales cambian, su valor hash también cambiará.
- Es imposible restaurar matemáticamente los datos originales a partir del valor hash (permanecen secretos) a menos que se realice una enumeración completa de los posibles valores iniciales (si su tamaño inicial aumenta y no hay información sobre su estructura, el problema es irresoluble en un futuro previsible).
- El tamaño del hash es fijo (no depende de la cantidad de datos iniciales).
Supongamos que una de las propiedades del entorno está descrita por la cadena «TERMINAL_LANGUAGE=German». Se puede obtener con una simple sentencia como la siguiente (simplificada):
string language = EnumToString(TERMINAL_LANGUAGE) +
|
El idioma real coincidirá con la configuración. Teniendo una hipotética función Hash, podemos calcular la firma.
string signature = Hash(language); |
Si hay más propiedades, simplemente repetimos el procedimiento para todas ellas, o solicitamos un hash a partir de las cadenas combinadas (hasta ahora esto es pseudocódigo, no forma parte del programa real).
string properties[];
|
La firma recibida puede ser comunicada por el usuario al desarrollador del programa, que la «firmará» de forma especial, al recibir una cadena de validación adecuada sólo para esta firma. La firma también se basa en hashing y requiere el conocimiento de algún secreto (frase de contraseña), conocido sólo por el desarrollador y codificado en el programa (para la fase de verificación).
El desarrollador pasará la cadena de validación al usuario, que podrá ejecutar el programa especificando esta cadena en los parámetros.
Cuando se lanza sin una cadena de validación, el programa debe generar una nueva firma para el entorno actual, imprimirla en el registro y salir (esta información debe pasarse al desarrollador). Con una cadena de validación no válida, el programa debe mostrar un mensaje de error y salir.
Se pueden proporcionar varios modos de lanzamiento para el propio desarrollador: con una firma, pero sin una cadena de validación (para generar la última), o con una firma y una cadena de validación (aquí el programa volverá a firmar la firma y la comparará con la cadena de validación especificada sólo para comprobar).
Calculemos cuán selectiva será dicha protección. Al fin y al cabo, la vinculación aquí no se realiza a un único identificador de nada.
En la siguiente tabla se ofrecen estadísticas sobre dos características: el tamaño de la pantalla y la memoria RAM. Obviamente, los valores cambiarán con el tiempo, pero la distribución aproximada seguirá siendo la misma: unos pocos valores característicos serán los más populares, mientras que algunos «nuevos» avanzados y otros «antiguos» que van saliendo de la circulación conformarán «colas» decrecientes.
Pantalla |
1920x1080 |
1536x864 |
1440x900 |
1366x768 |
800x600 |
---|---|---|---|---|---|
RAM |
21 % |
7 % |
5 % |
10 % |
4 % |
4 Gb 20 % |
4.20 |
1.40 |
1.00 |
2.0 |
0.8 |
8 Gb 20 % |
4.20 |
1.40 |
1.00 |
2.0 |
0.8 |
16 Gb 15 % |
3.15 |
1.05 |
0.75 |
1.5 |
0.6 |
32 Gb 10 % |
2.10 |
0.70 |
0.50 |
1.0 |
0.4 |
64 Gb 5 %. |
1.05 |
0.35 |
0.25 |
0.5 |
0.2 |
Preste atención a las celdas con los valores más grandes, porque significan las mismas firmas (a menos que introduzcamos un elemento de aleatoriedad en ellas, que se discutirá más adelante). En este caso, las dos combinaciones de características de la esquina superior izquierda son las más probables, con un 4.2 % cada una. Pero estas son solo dos características. Si añade el idioma de la interfaz, la zona horaria, el número de núcleos y la ruta de datos de trabajo (preferiblemente compartida, ya que contiene el nombre de usuario de Windows) al entorno evaluado, el número de posibles coincidencias disminuirá notablemente.
Para el hashing, utilizamos la función integrada CryptEncode (se describirá en la sección Criptografía) que admita el método hash SHA256. Como su nombre indica, produce un hash de 256 bits, es decir, 32 bytes. Si necesitáramos mostrárselo al usuario, entonces lo traduciríamos a texto en representación hexadecimal y obtendríamos una cadena de 64 caracteres de longitud.
Para que la firma sea más corta, la convertiremos utilizando la codificación Base64 (también es compatible con la función CryptEncode y su homóloga CryptDecode), lo que dará como resultado una cadena de 44 caracteres de longitud. A diferencia de una operación hash unidireccional, la codificación Base64 es reversible, es decir, los datos originales pueden recuperarse a partir de ella.
Las operaciones principales se implementan mediante la clase EnvSignature, que define la cadena data que debe acumular ciertos fragmentos que describen el entorno. La interfaz pública consiste en varias versiones sobrecargadas de la función append para añadir cadenas con propiedades de entorno. Esencialmente, unen el nombre de la propiedad solicitada y su valor utilizando como enlace algún elemento abstracto devuelto por el método virtual 'pepper'. La clase derivada lo definirá como una cadena específica (pero puede estar vacía).
class EnvSignature
|
Para añadir una cadena arbitraria a un objeto, existe un método genérico append, que se llama en los métodos anteriores.
bool append(const string s)
|
Opcionalmente, el desarrollador puede añadir, por así, «sal» a los datos hash. Se trata de un array con datos generados aleatoriamente, lo que complica aún más la inversión del hash. Cada generación de la firma será diferente de la anterior, aunque el entorno permanezca constante. La implementación de esta característica, así como de otros aspectos de protección más específicos (como el uso de cifrado simétrico y el cálculo dinámico del secreto) se dejan para un estudio independiente.
Dado que el entorno se compone de propiedades bien conocidas (su lista está limitada por las constantes de la API de MQL5), y no todas ellas son suficientemente únicas, nuestra defensa, tal y como la hemos calculado, puede generar las mismas firmas para distintos usuarios si no utilizamos la sal. La coincidencia de firmas no permitirá identificar el origen de la fuga de licencias si ésta se ha producido.
Por lo tanto, puede aumentar la eficacia de la protección cambiando el método de presentación de las propiedades antes del hash para cada cliente. Por supuesto, el método en sí no debe divulgarse. En el ejemplo analizado, esto implica cambiar el contenido del método pepper y volver a compilar el producto, lo cual puede resultar caro, pero permite evitar el uso de sal al azar.
Con la cadena de propiedades rellenada, podemos generar una firma. Para ello se utiliza el método emit.
string emit() const
|
El método añade un cierto secreto (una secuencia de bytes que sólo conoce el desarrollador y que se encuentra dentro del programa) a los datos y calcula el hash de la cadena compartida. El secreto se obtiene del método virtual secret, que también definirá la clase derivada.
El array de bytes resultante con el hash se codifica en una cadena utilizando Base64.
Ahora viene la función de clase más importante: check. Es esta función la que implementa la firma desde el desarrollador y la comprueba desde el usuario.
bool check(const string sig, string &validation)
|
Durante el funcionamiento normal (para el usuario), el método calcula el hash a partir de la firma recibida, complementada con el secreto, y lo compara con el valor de la cadena de validación (primero debe descodificarse de Base64 en la representación binaria en bruto del hash). Si los dos hashes coinciden, la validación es correcta: la cadena de validación coincide con el conjunto de propiedades. Obviamente, una cadena de validación vacía (o una cadena introducida al azar) no pasará la prueba.
En la máquina del desarrollador, la macro I_AM_DEVELOPER debe definirse en el código fuente de la utilidad de firma, lo que provoca que una cadena de validación vacía se trate de forma diferente. En este caso, el hash resultante se codifica en Base64, y esta cadena se pasa a través del parámetro validation. De este modo, la utilidad podrá mostrar al desarrollador una cadena de validación ya preparada para la firma dada.
Para crear un objeto, se necesita una determinada clase derivada que defina cadenas con el secreto y con «pepper».
// WARNING: change the macro to your own set of random bytes
|
Escojamos rápidamente algunas propiedades para rellenar la firma.
void FillEnvironment(EnvSignature &env)
|
Ahora todo está listo para probar nuestro esquema de protección en la función OnStart. Pero primero, veamos las variables de entrada. Dado que el mismo programa se compilará en dos versiones, para el usuario final y para el desarrollador, existen dos conjuntos de variables de entrada: para la introducción de los datos de registro por parte del usuario y para la generación de estos datos a partir de la firma del desarrollador. Las variables de entrada destinadas al desarrollador se han descrito anteriormente utilizando la macro INPUT. Sólo la cadena de validación está disponible para el usuario.
input string Validation = ""; |
Cuando la cadena esté vacía, el programa recogerá los datos del entorno, generará una nueva firma y la imprimirá en el registro. Esto completa el trabajo del script, ya que aún no se ha confirmado el acceso al código útil.
void OnStart()
|
Si se rellena la variable Validation, se comprueba su conformidad con la firma y se finaliza el trabajo en caso de fallo.
if(StringLen(Validation) == 0)
|
Si no hay discrepancias, el algoritmo pasa al código de trabajo del programa.
Por parte del desarrollador (en la versión del programa que se diseñó con la macro I_AM_DEVELOPER), se puede introducir una firma. Restauramos el estado del objeto MyEnvSignature utilizando la firma y calculamos la cadena de validación.
void OnStart()
|
El desarrollador no sólo puede especificar la firma, sino también validarla: en este caso, la ejecución del código continuará en modo usuario (a efectos de depuración).
Si lo desea, puede simular un cambio en el entorno, por ejemplo, de la siguiente manera:
FillEnvironment(env);
|
Veamos algunos registros de prueba.
Cuando ejecute por primera vez el script EnvSignature.mq5, el «usuario» verá algo como el siguiente registro (los valores variarán debido a las diferencias de entorno):
Hash bytes:
|
Envía la firma generada al «desarrollador» (no hay usuarios reales durante la prueba, por lo que se citan todos los roles de «usuario» y «desarrollador»), que la introduce en la utilidad de firma (compilada con la macro I_AM_DEVELOPER), en el parámetro Signature. Como resultado, el programa generará una cadena de validación:
Validation:YBpYpQ0tLIpUhBslIw+AsPhtPG48b0qut9igJ+Tk1fQ= |
El «desarrollador» lo devuelve al «usuario», y éste, al introducirlo en el parámetro Validation, obtendrá el script activado:
Hash bytes:
|
Para demostrar la eficacia de la protección, dupliquemos el script como servicio: para ello, copiemos el archivo en la carpeta MQL5/Services/MQL5Book/p4/ y sustituyamos la siguiente línea en el código fuente:
#property script_show_inputs |
con la siguiente línea:
#property service |
Vamos a compilar el servicio, crear y ejecutar su instancia, y especificar la cadena de validación recibida previamente en los parámetros de entrada. Como resultado, el servicio abortará (antes de llegar a las sentencias con el código requerido) con el siguiente mensaje:
Hash bytes:
|
La cuestión es que entre las propiedades del entorno hemos utilizado la cadena MQL_PROGRAM_TYPE. Por lo tanto, una licencia emitida para un tipo de programa no funcionará para otro tipo de programa, aunque se esté ejecutando en el ordenador del mismo usuario.