Resolviendo el CTF de ROBOT

Tras la publicación del post anterior sobre ROBOT (https://www.s21sec.com/es/blog/2017/12/robot-el-retorno-de-bleichenbacher/), algunas personas querían ver una prueba de concepto del ataque, así que empecé a desarrollar una serie de herramientas que demostraban el ataque usando el oráculo de Bleichenbacher. Tras un primer prototipo con un oráculo muy sencillo (un método que devolvía true/false) y comprobar que el ataque en sí funcionaba, me puse a modificarlo para conectar a un servidor TLS vulnerable y ejecutar el mismo ataque basado en su comportamiento ante valores de pre-master-secret con relleno incorrecto. Tras comprobarlo satisfactoriamente contra YAWS (un servidor web basado en Erlang, y por tanto vulnerable según el artículo de ROBOT), recordé que los autores del artículo de ROBOT habían preparado un CTF que en un primer momento había descartado, ya que no pensaba que fuera capaz de resolverlo… ¿Sería posible su resolución con la herramienta que había desarrollado?

El primer paso del CTF era descifrar un mensaje en base64 que estaba en formato CMS. Este mensaje se encontraba encriptado usando una clave compartida con un servidor vulnerable (target.robotattack.org, en el puerto 7777). Al no saber exactamente qué era CMS, mi primera idea fue descodificar los datos en base64, a ver si aparecía algún texto reconocible. Mala suerte, el contenido era binario.. Sin embargo, tras echar un rápido vistazo al RFC de CMS, vi que el formato estaba basado en ASN.1. Usando un visor de ASN.1 (por ejemplo,  https://lapo.it/asn1js/) los resultados fueron prometedores.

Dentro del ASN.1 se podía ver una sección cifrada con RSA (el OCTET STRING de 256 bytes del bloque superior) que muy probablemente contiene la clave AES, y una sección de datos binarios (la parte etiquetada como [0] en el bloque inferior) que aparece identificada como datos cifrados mediante AES-128-CBC (tal y como indica su OBJECT IDENTIFIER), junto con algo que parece ser el IV asociado (el OCTET STRING de 16 bytes en el mismo bloque).

El siguiente paso era comprobar que el servidor (recordemos que era target.robotattack.org en el puerto 7777) es vulnerable. Esto se consigue enviando pre-master-secrets con relleno correcto e incorrecto, y comprobando que la respuesta obtenida es diferente en ambos casos. La buena noticia es que con las respuestas obtenidas era posible identificar cualquier pre-master-secret cuyo relleno empezara por 0x00 0x02, sin importar el resto de los valores. Es decir, se trataba de un oráculo de buena calidad.

Con pequeñas modificaciones a mi herramienta (los códigos de alerta exactos no coincidían aunque los cambios eran menores), pude enviar los datos RSA encriptados. Como en este caso también se usaba PKCS#1, el ataque se podía realizar sin modificaciones adicionales. Tras 2 ó 3 horas, los cálculos se completaron.

El resultado del descifrado es lo que parece ser un valor correctamente relleno (es decir, empezando por 0x00 0x02) y de una longitud de 16 bytes (subrayado en blanco en la imagen), lo que se corresponde con 128 bits y por tanto muy probablemente sea la clave AES. Usando este valor como clave y el IV que aparece en claro en el mensaje, usé openssl para intentar descifrar los datos y el resultado es…

Basura.

Tampoco es que me sorprendiera demasiado. Me habría sorprendido que todo hubiera funcionado a la primera. Sin embargo, todo parecía encajar tan bien… Tanto los datos cifrados como el IV se podían extraer fácilmente del mensaje ASN.1, y el uso del oráculo había devuelto una clave del tamaño correcto (debido a que el ataque no asume cuál debe ser el tamaño del valor a descifrar, las posibilidades de obtener un valor incorrecto pero de longitud correcta eran extremadamente bajas). Mirando los argumentos que le pasaba a openssl, todo parecía estar correcto… Aquí va el algoritmo, aquí la clave, el IV, los datos… Todo correcto, ¿no?

Pues bien, obviamente no lo era… y seguramente algunos de vosotros ya lo habréis visto (aunque he de reconocer que a mí me llevó un rato). En ningún sitio le decía a openssl que DESCIFRARA los datos, y la operación por defecto es encriptar. Le estaba diciendo que volviera a encriptar los datos ya encriptados, así que basura (técnicamente, datos encriptados dos veces con la misma clave) era lo único que iba a ver.

Así que añadí el argumento correspondiente (-d) para descifrar los datos y ejecuté openssl de nuevo. Tras el primer fracaso no las tenía todas conmigo, así que estaba preparado para otra decepción…

¡¡¡¡¡El resultado era texto ASCII!!!!! ¡El nivel 1 del CTF estaba resuelto! El contenido del mensaje eran en realidad las instrucciones para el nivel 2 del CTF. Aparentemente, había que firmar un mensaje de tal modo que se pudiera verificar usando la clave pública incluida en el mensaje del nivel 1.

El problema era que las instrucciones indicaban que había en algún lugar un servidor web HTTPS vulnerable… y que tenía que encontrarlo por mí mismo. Por tanto, ni nombre de host ni puerto como en el nivel 1. En cualquier caso supuse que el servidor vulnerable debería encontrarse en uno de los dominios de robotattack.org que había visto durante el CTF (“ctf”, “target” o “final”) así que encontrarlo iba a ser bastante sencillo.

Los siguientes tres días fueron los más frustrantes del CTF. Por supuesto, ninguno de los servidores HTTPS de los dominios anteriores devolvía la clave pública esperada. Ejecuté nmap en los servidores y me conecté a todos los puertos abiertos (ignorando convenientemente esa vocecilla interior que me recordaba que ERA un servidor HTTPS). Todos los puertos o bien cerraban la conexión directamente o devolvían texto plano o usaban SSL con una clave pública diferente. Incluso probé diferentes nombres de servidores usando SNI, asumiendo que el servidor web compartía puerto con alguno de los otros, pero en un dominio diferente.

No es necesario decir que esta línea de razonamiento no me llevó a ninguna parte. Tras probar montones de posibles nombres de host no estaba más cerca de encontrar el servidor. Llegó un momento en que me dediqué a ampliar las imágenes de ROBOT intentando encontrar texto oculto y a preguntarme si debería probar alguna herramienta de esteganografía.

Afortunadamente, en ese momento recuperé el juicio. A pesar de que había alguna posibilidad de que la información para encontrar el servidor se encontrara escondida de forma tan rebuscada, sería completamente diferente al nivel 1. Lo único que se necesitaba para resolver el nivel 1 era COMPRENDER el ataque. Por tanto, toda la información necesaria debía encontrarse en dos puntos concretos: el mensaje descifrado del nivel 1 y el artículo sobre ROBOT. Si estaba escondido de forma retorcida en cualquier otro lugar, bueno… no tenía esperanzas de encontrarla para el 1 de febrero, cuando terminaba el CTF.

Así que me puse a hacer lo que tenía que haber hecho desde el principio… Volver a leer el artículo de ROBOT. Más específicamente, la parte relativa al firmado de mensajes… Vamos a ver… Para verificar que realmente han conseguido cifrar el mensaje en su prueba de concepto hay que crear un fichero con la firma que proporcionan, descargar el certificado desde el motor de búsquedas y usar openssl para verificar…  Un momento, ¿MOTOR DE BÚSQUEDA? Vamos a ver… Tengo una clave pública y tengo que encontrar un certificado que use esa clave… ¿y no me he dado cuenta de que podía BUSCARLO? En fin… menos mal que no me vio nadie porque la cara de tonto que se me quedó debió de ser impresionante. Sólo tenía que calcular el SHA256 de la clave pública y usar el motor de búsqueda para obtener el certificado asociado a esa clave pública.

Bueno, es un avance… Al menos hay un certificado, firmado por “Let’s Encrypt” como el resto de certificados del CTF y válido desde el 16 de diciembre… Bastante prometedor a priori, y en cuanto al contenido del certificado en sí…

Efectivamente contenía un nombre de host y no tenía nada que ver con robotattack.org (así que nunca habría sido capaz de encontrarlo por mí mismo). Tras conectar con el servidor y comprobar que el certificado usaba la clave especificada estuve a punto de llorar.

Vale, manos a la obra… firmar el mensaje. En este caso, “firmar”, tal y como lo hace rsautl, quiere decir encriptar el mensaje completo con la clave privada de quien lo firma. En el ataque original (el nivel 1) teníamos un valor cifrado con la clave pública y obteníamos el valor descifrado. Pero el proceso de descifrar un valor encriptado con una clave pública es equivalente a cifrar un texto cualquiera con la clave privada. Por tanto, el ataque es perfectamente válido, pero en vez de usar el pre-master-secret cifrado era necesario usar el mensaje a cifrar, con el relleno correcto, y como resultado obtendríamos el mensaje “firmado”. El mensaje incluyendo el relleno queda como sigue (sin relleno, y en formatos binario y hexadecimal).

Pero un momento… el ataque asume que el valor descifrado (o en este caso, la firma) empieza por 0x00 0x02. Lo más probable es que el resultado de la operación de cifrado no va a empezar con esos dos valores. ¿Seguro que puede usarse el ataque?

En realidad, es bastante sencillo. Sólo es necesario aplicar un paso adicional que transforme el mensaje con el relleno en un pre-master-secret correctamente cifrado que sea aceptado por el servidor. Algo similar a las primeras operaciones que se realizan durante el ataque normal. Una vez que conseguí este pre-master-secret válido, pude aplicar el ataque sobre este valor y descifrarlo. Como la relación entre el mensaje original y este pre-master-secret es conocida, se puede calcular la firma a partir de este valor descifrado, revirtiendo la transformación.

Por tanto, tuve que modificar el código para primero calcular un pre-master-secret válido a partir del mensaje con relleno. En este caso la velocidad del ataque era bastante baja. Como el tamaño de clave (4096 bits) era mayor que en el nivel 1, cada intento requería más tiempo y llevó bastantes horas obtener el resultado, pero finalmente devolvió un valor  correcto.

¿Qué es toda esta información? El valor “padded-message” es el mensaje a firmar con el relleno apropiado. Tras un montón de ejecuciones del oráculo, el ataque encontró un valor correcto para “s” (en este caso, 37398) que transformaba este mensaje en un pre-master-secret válido (en este caso, “value”). Tras efectuar el ataque, se obtiene el valor descifrado que en este caso corresponde a “decrypted”. Y para transformarlo en la firma, es necesario calcular el inverso de “s” (que aparece representado como “s_1”) y aplicarlo al valor descifrado para obtener “result”. Si todo ha ido correctamente, este valor es el mensaje firmado. Usando xxd lo grabé en un fichero (y usando xxd de nuevo comprobé su contenido).

Si todo había ido bien, openssl debería verificar correctamente el mensaje.

Y el último paso era comprobar que efectivamente el valor era aceptado como solución para el CTF, así que tras subirlo al servidor…

 

¡CTF resuelto!

Recent Posts

Leave a Comment