La importancia del lenguaje, binary diffing y otras historias de “día uno”

//

Este artículo lo publiqué originalmente en el blog de seguridad de INCIBE.

El papel del lenguaje en una profesión es importante. Cualquier disciplina genera su propio lenguaje técnico a medida que evoluciona y se hace más compleja, ya que es un mecanismo con el cual profesionales del mismo campo pueden intercambiar conocimientos e interactuar de forma concisa, precisa y sin ambigüedades.

Incluso dentro de una disciplina aparecen subdisciplinas, como el caso de la informática, que abarca tantas cosas diferentes que se hace necesario crear especialidades, cada una con su propio lenguaje técnico.

Nuestra especialidad es la seguridad de la información, y es un campo tan diverso que su lenguaje también lo es.

Sin embargo, esta especialidad ha tenido un boom en interés y complejidad en un espacio muy corto de tiempo, y esto genera un problema: se convierte en una moda, y ello afecta a una correcta evolución de la misma (donde el marketing se impone sobre el desarrollo tecnológico).

Al ser algo muy mediático (ataques informáticos, hackers, espionaje,.. son temas muy apetecibles para la prensa), se empieza a utilizar el lenguaje propio de la profesión de forma indiscriminada, y a menudo (muy a menudo) se hace de forma incorrecta, llegando incluso a inventarse términos que no existen o que son, cuanto menos, semánticamente incorrectos.

Del mismo modo, solo las técnicas más accesibles o más populares se destacan y promocionan ("conviertase en un especialista utilizando Metasploit", "aprenda a analizar malware sin esfuerzo"), relegando a un segundo plano los conocimientos fundamentales sobre computación o arquitectura de procesadores necesarios para comprender lo que se está haciendo ("la matemática no vende"). Y esto se hace incluso en cursos de formación.

Esto genera mucho ruido, y un profesional (especialmente los que empiezan) corre el riesgo de aprender conceptos de forma incorrecta o de estancarse en conocimientos superficiales si no es capaz de profundizar sin distraerse con toda esa información que está en candelero.

A esto ha contribuído el haber caído en el uso excesivo de buzzwords, o la reciente moda de poner nombre a vulnerabilidades (shellshock, heartbleed, o la reciente venom) para venderlas como un gran descubrimiento o una peligrosidad excepcional, cuando la realidad es que no son más importantes que cualquier otra vulnerabilidad o amenaza (de hecho, suelen ser bastante "simples").

El caso que vamos a tratar en este artículo, como ejemplo de como el mal uso del lenguaje técnico (o el uso del mismo para hacer marketing) puede distraer nuestra atención de lo importante, es el de las vulnerabilidades 0day, término utilizado para referirse a vulnerabilidades para las cuales no se conocen los detalles, y por tanto no existe todavía un parche.

Debido al tirón mediático que tiene la palabra 0day, tenemos la falsa percepción de que una vulnerabilidad de este tipo es muy peligrosa. Frente a esto, da la sensación de que una vulnerabilidad “común” no lo es tanto porque no se escucha hablar de ella hasta que aparece en el changelog de un parche, especialmente cuando se ha hecho responsible disclosure y solo el fabricante dispone de los detalles de la misma (en muchas ocasiones parcheando en silencio y ocultando su criticidad al público general, lo cual es una práctica, cuanto menos, discutible).

La realidad, es que cualquier vulnerabilidad es peligrosa. Y en muchas ocasiones las vulnerabilidades parcheadas (llamémosles “1-day” para ilustrar lo innecesario del término) son más peligrosas incluso que las desconocidas, ya que no se les presta la misma atención por considerarlas corregidas, pero un posible atacante ya conoce los detalles de la misma.

Inconscientemente, se les resta importancia porque no son 0day.

A continuación, veremos un ejemplo práctico de como un atacante puede aprovechar esa sensación de "menor prioridad" a la hora de parchear un sistema.

Binary diffing

La diferencia binaria, o más concretamente, diferencia de código (program diffing) es una técnica clásica de ingeniería inversa en la que se comparan dos archivos a nivel binario o a nivel de instrucciones, en busca de diferencias. O lo que es lo mismo, qué ha cambiado un archivo respecto al otro.

Para ello se emplean diferentes heurísticas de búsqueda que generarán unos resultados significativos dentro del contexto que queremos analizar, como pueden ser diferencias entre nombres de una misma función, diferencias entre series de instrucciones, prólogos de funciones, etc.

Existen diversas herramientas y plugins diseñados para esta tarea, como DarumGrim, Zynamics Bindiff, o el reciente Diaphora, un plugin open source para IDA Pro, desarrollado por el investigador Joxean Koret y que utilizaremos en la demo de este artículo.

Este tipo de técnicas son muy útiles para algunas tareas, como descubrir qué nuevas funcionalidades o mejoras se han realizado sobre un software, o para comprobar si el nuevo parche es compatible con la arquitectura que tengamos montada (y así evitar que el parche “rompa” algo), aunque el caso que nos atañe es la identificación de vulnerabilidades ya corregidas, comparando la antigua versión vulnerable con la nueva ya parcheada.

Esto tiene aplicaciones legítimas, como puede ser la generación de firmas para un IDS o un antivirus, pero también es un método muy utilizado para descubrir vulnerabilidades que no han sido reveladas públicamente y sacar ventaja de ello.

Para ilustrar cómo se hace esto, hemos creado un sencillo programa que consta de dos componentes, prog.exe y utils.dll.

Dicho programa toma como argumento la ruta a un archivo de texto, y muestra por pantalla los 8 primeros caracteres.

Más tarde, se nos proporciona un parche para el mismo, el cual incluye una nueva versión de utils.dll, y cuyo changelog reza:

"Versión 1.2: Se corrige un error que causaba inestabilidad en el programa"

Esto no nos dice mucho, y parece poco transparente. ¿A qué se refieren con inestabilidad?

Dado que el parche solo trae un nuevo utils.dll, está claro que la corrección se ha hecho sobre ese archivo, así que vamos a compararlo con el archivo original y comprobar qué se ha cambiado en el código.

Abrimos en IDA el utils.dll original, y lanzamos el script Diaphora, para generar la base de datos sqlite que utilizará el script para hacer su trabajo.

Diaphora

Ahora que ya tenemos la BBDD para el archivo original, abrimos el nuevo, y repetimos la operación, excepto que esta vez ya lanzaremos el proceso de diffing, proporcionando el sqlite del original.

Diff assembly

El script nos abrirá varias pestañas, y la que nos interesa en este caso es “Best matches”. Llama a atención que la función utils_1, concuerda en nombre para ambos archivos, pero solo en eso.

Best match

Ejecutando de nuevo el script, esta vez marcando “Ignore all function names”, podemos comprobar que efectivamente, para la función utils_1 no coincide el código entre ambos archivos.

Unmatched

Es el momento de comparar ambas funciones para analizar en detalle qué ha cambiado en el código cuando aplicamos el parche.

Diff assembly in a graph

Diff assembly in a graph A la izquierda la función original; a la derecha el código parcheado

Diff assembly

Diff assembly A la izquierda el código original, a la derecha el código parcheado

Ya que la función no es demasiado compleja, antes de estudiar el código modificado, vamos a ver qué es lo que hace la función, para así tener la foto completa, lo cual nos será útil para entender el porqué de la corrección.

Función

Con un vistazo rápido observamos que la función utils_1 prepara el stack frame para dos arrays de 8 bytes (var_8 y var_10) y abre un archivo en modo lectura (presumiblemente el archivo que pasamos al programa como argumento) para a continuación cargar 8 bytes (tomados del fichero) en var_8 con fread.

Si reconstruimos el código en C:

Código sin parchear

Veamos ahora el código que ha cambiado con el parche:

Código parcheado

No es más que una llamada a strncpy_s para copiar el contenido de var_8 en var_10. Esto ya nos da una pista importante de por dónde van los tiros acerca de la vulnerabilidad, aunque no adelantemos acontecimientos.

Código parcheado

Estudiemos ahora el código original (el que ha sido reemplazado por strcpy_s):

Código sin cambiar

Esta rutina va copiando byte a byte los valores del primer array en el segundo, hasta encontrar un byte nulo (‚0’), utilizado comúnmente como terminador de cadena.

O lo que es lo mismo, se trata de un strcpy (función conocida por carecer de mecanismos para prevenir desbordamientos), la cual si no encuentra un nulo en el segundo parámetro seguirá copiando bytes y sobrescribiendo valores de la pila más allá de sus límites, provocando eventualmente un clásico buffer overflow.

Hemos descubierto que lo que corrige este parche es una vulnerabilidad desencadenada por el uso incontrolado de la función fread. Dicha función copia los 8 primeros bytes del archivo en un array, pero no añade el caracter terminador (null) al final del mismo. Esto provocará un desbordamiento en strcpy al intentar copiar dicho array en el otro.

Conociendo esta información, escribir un exploit es relativamente trivial; utilizando un archivo cuidadosamente diseñado con un payload y pasándoselo como parámetro nos permitiría ejecutar nuestro propio código.

Para comprobar que efectivamente se produce un desbordamiento de pila, vamos a pasarle a prog.exe un archivo con un tamaño superior a los 8 bytes.

Programa parcheado

Programa parcheado

Programa sin parchear

Programa sin parchear

Este ejemplo es extremadamente simple; en un caso real nos encontraríamos con otras dificultades que hacen el trabajo un poco más complicado:

  • Mayor cantidad de diferencias en el código (optimizaciones diferentes según compilador,…)
  • Otros bugfixes ajenos a las vulnerabilidades, los cuales habrá que descartar
  • Código ofuscado, el cual requiere un esfuerzo extra para entender el código
  • Técnicas anti-disassembly o anti-diffing
  • ...

Además, por supuesto, de las protecciones habituales como DEP, ASLR, StackGuard (canary),… que han sido desactivadas para este experimento.

Sin embargo para un analista experimentado, encontrar la vulnerabilidad y crear un exploit disponiendo del archivo original y del parche, sigue siendo cuestión de horas, o en el peor de los casos unos pocos días.

La ventana de tiempo que transcurre desde que aparece un parche hasta que el usuario final lo aplica, es aprovechada por atacantes para diseccionar esos parches, crear un exploit y atacar a la víctima. Y dicha ventana se alarga en el tiempo por diversas razones (políticas de actualizaciones mal definidas, logística, no estar al día, …), pero sobretodo porque no se considera dichas vulnerabilidades críticas, y por tanto nos relajamos a la hora de darles importancia. Y ahí radica el peligro de las vulnerabilidades “normales”, no tanto por la falta de solución, sino por la percepción que se tiene de ellas frente a otras vulnerabilidades más publicitadas como los 0day.