El cifrado XOR
Entendiendo las operaciones con bits
Introducción
Para poder entender el cifrado XOR veremos primero como funcionan algunas operaciones a nivel de bits. Se debe de entender esto como un pequeño inciso. Las operaciones de bits no se limitan exclusivamente a estas operaciones que se mencionan y se puede profundizar más.
Hay que tener en cuenta, también, que el cifrado XOR es un sistema muy simple y sencillo y que hay otros sistemas de cifrado más complejos, a la vez que seguros. Esto es solo una guía didáctica para entender un concepto y una serie de procesos usados en la programación en lenguaje C.
Operaciones lógicas: AND, OR y XOR
Veamos a continuación las operaciones lógicas que, probablemente, los que estén leyendo este artículo ya conozcan:
Partimos de que un bit tiene dos valores: 1 (cierto) y 0 (falso). Le asignamos valores cierto y falso para que en la lógica de la mente de una persona asimile de manera más natural el resultado. Puede darse el hecho, en algunos sistemas, que se invierta el resultado, es decir 1 falso, 0 cierto, aunque por norma se asume la premisa inicial.
Las operaciones lógicas que usaremos necesitan siempre dos operandos. En la tabla los denominaremos A y B. Con estos operandos se realiza una de las operaciones lógicas y se obtiene un resultado. Las operaciones lógicas son AND, OR y XOR.
La operación AND dará como resultado 1 (cierto), siempre que los dos operandos sean 1 (cierto), en cualquier otro caso el resultado será 0 (falso). La operación OR dará como resultado 1 (cierto), siempre que uno de los dos operandos sea cierto, y solo será 0 (falso) cuando los dos operandos sean 0 (falso). Y finalmente, la operación XOR solamente será 1 (cierto), cuando exclusivamente uno de los dos operandos sea 1 (cierto), pero no los dos. Es decir, si los dos son 1 (cierto) o los dos son 0 (falso) el resultado será siempre 0 (falso).
La siguiente tabla resume dichas operaciones:
Cifrando un BIT
Antes de pasar al cifrado de todo un fichero, o incluso una cadena de texto, vamos a empezar por el nivel más bajo. El cifrado de un BIT, y para ello nos vamos a centrar en la operación XOR, conocida también como “OR exclusivo”.
¿Se puede cifrar un bit?
Es muy probable que al leer el título de este apartado nos hagamos esta pregunta. Podríamos pensar que es un poco atrevido hablar del cifrado de un bit, o que es una afirmación absurda. De hecho, el cifrado se verá mejor tratando los bits a gran escala, es decir, con un gran volumen de estos. Pero para entender esta operación prefiero hacer una visión microscópica y centrarme en 1 solo bit, para posteriormente, ampliar el campo de visión donde con una perspectiva mayor se verá el resultado del cifrado.
Para ello, imaginemos lo siguiente: tenemos unos bits de mensaje, que los identificaremos con la letra “m”, y a continuación tendremos otros bits que serán nuestra clave de cifrado e identificaremos con la letra “k”.
Si hacemos una operación XOR de m con k obtenemos el resultado de la columna C, el cual podríamos considerar el bit cifrado. Recordemos, k es la clave.
Al aplicar de nuevo una operación XOR de los datos cifrados (columna C) con la clave (columna k), obtenemos el resultado de la columna D, que es el mensaje descifrado.
Cifrando un BYTE
Ahora que hemos visto la operación con un bit, observemos como se haría con un BYTE. Para el que no lo sepa, un BYTE es un conjunto de 8 bits. Y estos, tradicionalmente, se interpretan también como 1 carácter si nos atenemos a la codificación estándar ASCII.
Supongamos, pues, que el mensaje es la letra “A” mayúscula, cuyo código ASCII en hexadecimal es el 41 y en binario corresponde a la secuencia de 8 bits 01000001.
Queremos cifrar este mensaje, de una letra, con una clave, que también será una letra. En este caso, nuestra clave es la letra “t” minúscula, cuyo código ASCII en hexadecimal es el 74 y en binario corresponde a la secuencia de 8 bits 01110100. He elegido esta letra para que el resultado cifrado nos dé un valor que corresponda a un carácter imprimible. Podría ser cualquier otro y funcionaría exactamente de la misma manera.
Si aplicamos la operación XOR por cada uno de los bits, el resultado es la secuencia 00110101 que corresponde al carácter “5” según la tabla ASCII. Este será nuestro mensaje cifrado.
Si a este mensaje cifrado, le volvemos a aplicar la operación XOR usando la misma clave, podremos observar que obtenemos de nuevo el mensaje original.
El programa de ejemplo para realizar esto es el siguiente:
Cifrado simétrico
El cifrado que hemos visto hasta ahora se considera simétrico por el hecho de que se utiliza la misma clave para cifrar que para descifrar. Por supuesto, no es el sistema de cifrado más seguro, todo lo contrario, pues se podría llegar a obtener la clave analizando diferentes mensajes cifrados.
Tampoco se le ocurriría a nadie cifrar un contenido importante empleando como clave un solo carácter (o byte), por lo que lo habitual es emplear una palabra, de la cual se va cogiendo cada uno de los caracteres de manera rotativa para ir cifrando los diferentes caracteres del mensaje.
El siguiente ejemplo muestra cómo se cifraría la cadena de texto “ESTAMOS FRENTE A UN GRAVE PROBLEMA” empleando la clave “barcelona”.
La elección de los caracteres de la clave está hecha aposta en este ejemplo con tal de obtener una cadena cifrada con el máximo de caracteres imprimibles. Cabe recordar que al hacer una operación con bits podríamos obtener resultados con valores inferiores al código 20 hex que, en la tabla ASCII, corresponden en su mayoría a caracteres no imprimibles. O incluso se podría generar algún byte a 00 hex, con lo que el problema estaría en que sería interpretado como fin de cadena.
Cifrando la frase en lenguaje C
Visto el ejemplo anterior, la función para cifrar la cadena de texto es muy sencilla. Se trata de recorrer la frase carácter a carácter y cifrar cada uno de estos caracteres con el carácter correspondiente de la palabra clave. Al agotar los caracteres de la palabra clave se vuelve a empezar por el primero.
Para ello, la solución más práctica es usar el operador de módulo (%) que nos da el resto de una división entera, con lo que podemos obtener directamente la posición del carácter correspondiente de la clave.
En la siguiente captura de pantalla podemos ver el resultado del programa aplicado a la frase anterior utilizando dicha clave. Para el caso de caracteres no imprimibles, la función mostrará simplemente un punto.
El programa de ejemplo que muestra el resultado anterior es el que se muestra a continuación:
Cifrando un archivo
Llegados a este punto, poca cosa nueva voy a explicar sobre el cifrado. De hecho, nada. La operativa es exactamente la misma. Simplemente, voy a mostrar los detalles para cifrar el contenido de un archivo sobre otro de una manera muy simple y sin entrar profundas optimizaciones porque se trata solo de un ejemplo orientado al nivel más básico.
Los argumentos
El programa recibirá 3 argumentos: nombre de archivo a cifrar, clave de cifrado y nombre de archivo de salida.
Para leer los argumentos de la línea de comandos, en lenguaje C, tenemos dos variables que se reciben en la función main(). Tradicionalmente, se les llama argc y argv, pero pueden tener cualquier otro nombre.
Argc, es un entero que contiene el número de argumentos recibidos por línea de comandos, incluido el propio nombre del programa. Por lo tanto, argc tendrá el valor 1 si únicamente se llama el programa sin argumentos.
Argv es un array de punteros a caracteres. Si has programado en lenguaje C, sabrás que las cadenas de caracteres como variable no existen como tal. En realidad, dicha variable es un puntero (guarda una dirección de memoria) que apunta al primer carácter de la cadena, y sabe cuándo esta cadena finaliza al encontrar un carácter nulo (‘\0’), es decir, un 0.
Por lo tanto, el primer paso es comprobar que se han pasado el número de parámetros correcto y, si no es así, informar de ello mostrando, por ejemplo, una ayuda de uso del programa.
Seguidamente, ya llamamos a la función que se encargará de abrir el archivo y cifrar su contenido sobre otro archivo.
Como estamos tratando archivos que pueden contener datos binarios (no nos centramos solo en texto), debemos abrir el archivo en modo de lectura binaria.
También debemos abrir el archivo destino, en modo escritura y binario. De hecho, es lo mismo que la apertura del archivo anterior y simplemente cambia las opciones “rb” por “wb”. En los dos casos si no se puede abrir el archivo muestra mensaje y sale de la función devolviendo 0.
Finalmente, únicamente queda hacer el bucle que ira leyendo byte a byte del archivo origen, lo cifra y lo escribe al archivo destino.
Cabe remarcar que este es únicamente un ejemplo didáctico y sencillo, pues no está nada optimizado al estar leyendo un carácter del archivo por cada operación de cifrado. Lo ideal sería leer el contenido del archivo por bloques, por ejemplo, de 512 bytes o 1024 bytes (o más en función de necesidades y disponibilidad de memoria), de manera que se minimize el número de lecturas a disco. Y posteriormente cifrar cada bloque por completo y grabarlo por completo en el archivo destino.
Una alternativa al cifrado de cada carácter rotativamente, y aprovechando la lectura por bloques, podría ser cifrar cada bloque con uno de los caracteres de la contraseña. Es decir, cada carácter (byte) cifra todo un bloque, y al cambiar de bloque se cambia de carácter.
El código fuente de ejemplo que cifra todo un archivo se muestra a continuación:
Uso de XOR en Malware
A pesar de su simplicidad, o debido a ello, el cifrado XOR es ampliamente usado en muchos malwares para ocultar (no tanto como cifrar) su código y evadir las medidas de detección de los antivirus y otras herramientas. Y digo ocultar porque la misma contraseña que se usa para descifrar el código está dentro del mismo código.
Muestra de ello es el ejemplo de la siguiente captura de pantalla que corresponde a un fragmento del malware Turla Keylogger el cual ocultaba los nombres de las funciones con un cifrado XOR con el valor 55 hex.
A partir de ahora cuando oigas una noticia sobre un malware o un ransomware, no dejes de pensar en el cifrado XOR.
Para facilitar la descarga del código he creado un repositorio en GitHub.