miércoles, abril 11, 2018

Taint Análisis y la detección de bugs en código fuente

Hace unas semanas participé junto con mi compañero Pablo González en el “How to hack”, un nuevo evento que ha surgido este año y que tuvo un resultado muy positivo. Puesto que algunas personas me pidieron al final de la charla la documentación y el código, hoy os traigo una parte de lo que conté allí, Taint Analysis: Fast Static Analysis technique for detecting dangerous patterns in source code.

Figura 1: Taint Análisis y la detección de bugs en código fuente

Los resultados que se pueden obtener con estas técnicas son siempre jugosos, como ya vimos en el trabajo que hizo mi compañero con el proyecto OSB-Rastreator, donde buscaba funciones inseguras en el código fuente de los paquetes que podías instalarte en un sistema GNU/Linux, algo muy importante si quieres tener un servidor GNU/Linux fortificado. Una buena forma de buscar ejemplos para practicar la construcción de exploits para GNU/Linux.

¿Qué es el análisis estático de código fuente?

El análisis estático de código fuente, de forma simplificada, consiste en el uso de herramientas de análisis del código que permiten encontrar determinados patrones en función del caso de uso en el que se este aplicando sin llegar a ejecutar dicho código. Y, ¿para qué sirve este tipo de análisis? Los usos del análisis estático son muy numerosos, y seguramente muchos de nosotros lo utilizamos en nuestro día a día sin ni siquiera saberlo. Aquí os dejo algunos de los más importantes:
  • Comprobar si un lenguaje de programación cumple con su especificación formal
  • Comprobar si hay errores de sintaxis en el código fuente de un programa antes de ejecutarlo/compilarlo
  • Realizar comprobaciones de sintaxis al vuelo (errores que te muestra un IDE)
  • Medir la calidad del código fuente en un ciclo de desarrollo
  • Auditar código fuente en busca de patrones inseguros o posibles vulnerabilidades
Una vez visto esto, lo que nosotros vamos a hacer en este artículo, es construir nuestro propio analizador de manera muy sencilla para el último de los puntos que se presentan en el apartado anterior, detectar patrones inseguros en código fuente. Probablemente, muchos de los que hayáis leído esta última frase os preguntaréis, ¿por qué querríamos programar nuestro propio analizador? ¿No existen herramientas?

Figura 2: Detección con OSB-Rastreator de función insegura strcpy en un programa en C

Por supuesto, existen herramientas que probablemente cumplirán con nuestros casos de uso de una manera mucho más completa que un analizador que programemos nosotros en una tarde, pero hay algunos casos específicos para los que puede ser útil construir uno propio, aquí os enumero algunos:
  • Analizar mucho código (miles de ficheros). Necesitamos que sea rápido.
  • Queremos buscar patrones muy concretos.
  • No queremos invertir tiempo en la curva de aprendizaje que suponen las herramientas existentes.
  • Queremos realizar procesamientos adicionales cuando se encuentra un patrón sospechoso.
Construcción del analizador

Una vez visto el motivo por el que podría interesarnos construir nuestro propio analizador, vamos a meternos en materia y ver cómo podemos hacerlo. Antes de comenzar con los métodos, vamos a ver que cosas buscamos en la construcción de nuestra herramienta, básicamente, hay dos características importantes:
  • Que sea rápido de construir.
  • Que sea efectivo. Entendiendo la efectividad como la reducción de los falsos negativos y falsos positivos y el aumento de los verdaderos positivos y verdaderos negativos.

Figura 3: Tabla de posibilidades con los resultados del analizador

Teniendo claro el objetivo que queremos alcanzar, vamos a ver los métodos que podemos utilizar. Vamos a comenzar por uno muy sencillo, las expresiones regulares.

Análisis con expresiones regulares

Realmente las expresiones regulares no se pueden considerar un tipo de análisis estático de código fuente, pero probablemente, es el método más usado cuando queremos detectar determinados patrones de forma rápida en un conjunto de ficheros. Una estrategia que podríamos utilizar con expresiones regulares es la siguiente:
  • Seleccionamos un conjunto funciones peligrosas.
  • Seleccionamos un conjunto de variables peligrosas (ej. argv, $_GET…)
  • Construimos un conjunto de expresiones regulares.
  • Leemos el código fuente del programa y aplicamos las expresiones regulares.
De esta forma, quizá seamos capaces de detectar determinados patrones inseguros en el código, pero ¿qué sucede si encontramos patrones como los siguientes?

Figura 4: ¿Son vulnerabilidades?
En el primer recuadro, se produce una vulnerabilidad conocida como buffer overflow, pero debido a que la variable que se le pasa a la función peligrosa no es directamente la variable peligrosa argv, nuestro analizador basado en expresiones regulares no sería capaz de detectarlo, hablaríamos de un falso negativo

Por otra parte, en el segundo recuadro, vemos el caso contrario, se le pasa la variable peligrosa argv a la función peligrosa strcpy, con lo cual, nuestro analizador sacaría un patrón vulnerable, pero realmente, la condición que hay justo encima de la función peligrosa evitaría la vulnerabilidad, convirtiéndose en un falso positivo.

Como podemos observar, las expresiones regulares cumplen solo una de las condiciones que hemos puesto para nuestro analizador, son rápidas de construir, pero, por otra parte, su efectividad es muy reducida.

Taint Analysis: Nueva estrategia de análisis

Puesto que la estrategia que hemos seguido anteriormente no ha sido muy efectiva, vamos a empezar por cambiarla, y esta vez vamos a aplicar una estrategia conocida como taint analysis. A continuación, os dejo las características de esta estrategia:
  • Identificamos un conjunto de funciones peligrosas (sinks)
  • Identificamos variables o funciones que acepten datos del usuario.
  • Identificamos todas las declaraciones de variables y las inicializamos con el valor untaint.
  • Identificamos todas las asignaciones entre variables y todas las llamadas a funciones de manera que:
    • var_a(untaint) = var_b(taint) -> var_a = taint
    • var_a(untaint) = taint_func() -> var_a = taint
·      Si una función peligrosa recibe una variable taint, mostramos un aviso de posible vulnerabilidad. Si utilizamos expresiones regulares con esta estrategia, ya somos capaces de detectar fallos como los que se mostraban en el primer recuadro del ejemplo anterior, pero los problemas del segundo recuadro, siguen apareciendo, y la complejidad para detectar todas las declaraciones de variables y estructuras similares en el código fuente mediante expresiones regulares complican mucho el sistema.

Llegados a este punto, vamos a formularnos la siguiente pregunta, ¿y si no analizamos el código fuente en su representación de alto nivel?

AST y Clang Python Bindings

Uno de los problemas más grandes que tenemos en este punto, es determinar algunas estructuras del lenguaje que pueden variar mucho, como funciones condicionales o declaraciones variables. Mediante el uso de representaciones intermedias del código fuente podemos solventar esto, vamos a empezar por el árbol de sintaxis abstracta (AST). El AST es una representación en forma árbol de la estructura sintáctica abstracta (simplificada) del código fuente de un determinado lenguaje de programación.

Figura 5: Árbol AST de un código

Utilizando una representación de este estilo, conseguimos generalizar muchas estructuras del código fuente y que dejen de depender de la sintaxis específica de alto nivel. Por ejemplo, cualquier tipo de declaración de un variable, será identificada mediante un token con el mismo nombre, de forma que es muy sencillo identificarlas.

Al principio del artículo hemos dicho que uno de nuestros objetivos principales es que nuestro analizador sea fácil de construir, el AST no es sencillo de construir, pero como forma parte del proceso que realizan diferentes interpretes y compiladores, hay módulos que te lo construyen en casi todos los lenguajes de programación:
  • C -> Clang
  • C++ -> Clang
  • Python -> ast – Abstract Syntax Trees
  • PHP -> PHP-Parser
  • Java -> javaparser
En este caso, vamos a utilizar como ejemplo concreto Clang, y sus Python bindings, que nos permitirán acceder a los distintos nodos del árbol a través del lenguaje de programación Python, tan útil para los pentesters.

Construyendo el analizador

Recapitulando, para construir nuestro analizador, vamos a utilizar las Python bindings de Clang, para convertir el código en una representación intermedia que abstraiga los detalles de alto nivel (espacios, saltos de línea…), en este caso un árbol de sintaxis abstracta, y sobre esta representación aplicaremos la técnica de análisis conocida como Taint analysis. Vamos a comenzar por ver qué forma tiene el AST producido por Clang a partir de un programa sencillo escrito en Lenguaje C:

Figura 6: Código en Lenguaje C con llamada a strcpy

Para ver una representación del AST producido por Clang de ese programa, ejecutamos el siguiente comando:
clang -Xclang -ast-dump programa.c

El resultado tiene que ser algo similar a lo siguiente:

Figura 7: AST producido por Clang

La explicación de cada uno de los campos, la podéis encontrar muy detallada en el siguiente vídeo, aunque la mayoría pueden determinarse con facilidad a simple vista.

Figura 8: Tutorial de Clang AST


Una vez que sabemos la forma que tiene el AST producido por Clang, lo siguiente que tenemos que hacer el instalar las Python Bindings y recorrerlo con nuestra técnica de análisis. Su instalación puede hacerse de la siguiente forma:

Figura 9: Instalación de Python Bindings

Una vez que tenemos instaladas las Python bindings, vamos a ver como podemos utilizarlas, a continuación, os muestro un ejemplo de como recorrer el AST y sacar por pantalla las llamadas a funciones que se encuentren en el código. Hay que tener en cuenta que, si hay includes, también se analizarán, los includes se pueden quitar sin que afecte a la construcción del AST. El programa no tiene que compilar para que el AST se genere.

Figura 10: Código para recorrer el árbol

Ahora que sabemos cómo recorrer el árbol y como buscar diferentes expresiones vamos a ver un pequeño script que nos permitiría recorrerlo aplicando Taint analysis sobre el código y sacar patrones vulnerables en tres casos de uso distintos, el script es el siguiente:



Figura 11: Código con Taint Analysis

El programa que os he mostrado es muy básico y busca muy pocos patrones, sin embargo, sirve para detectar patrones inseguros en todos los casos que hemos expuesto anteriormente, vamos a verlo.

El primero es el caso más básico, y consisten en detectar el buffer overflow que se produce en el ejemplo que hemos presentado anteriormente, sobre el que hemos generado el AST con Clang, la ejecución de la herramienta devuelve el siguiente resultado:

Figura 12: Patrón peligroso en ejemplo 1

Fijaos como el script es capaz de determinar cual es el patrón inseguro y la línea donde se produce, lo que veis entre corchetes es el conjunto de variables detectadas (muchas son de los includes) y su estado, TAINT (si han tenido contacto con el usuario) o UNTAINT.

El segundo caso es más complejo, y es el que producía un falso negativo cuando utilizábamos expresiones regulares y técnicas de análisis muy rudimentarias, se produce una asignación y no es la variable peligrosa la que produce el overflow, sino otra a la que se le ha asignado su valor.

Figura 13: Código en Lenguaje C con strcpy

El resultado del script es el siguiente:


Figura 14: Detección de patrón como peligroso en ejemplo 2

La herramienta es capaz de detectar el patrón vulnerable y su línea, además, si nos fijamos en las variables y su estado, podemos ver como buf2 tiene el valor (T)AINT como consecuencia del taint analysis. Esto es lo que permite la detección, ya que en este momento no solo argv es una variable peligrosa, sino que buf2 también lo es por haber tenido contacto con ella.

Vamos a ver un último caso, y probablemente el más complejo de determinar. En este caso, tenemos el siguiente programa:

Figura 15: Ejemplo 3 con strcpy

Como puede observarse, aunque aparentemente se trata de un buffer overflow, hay una condición justo antes de la sentencia insegura que controla el tamaño de la variable que se copia, evitando así el overflow. En el caso de las expresiones regulares, este código producía un falso positivo. El resultado de la herramienta es el siguiente:

Figura 16: No da falso positivo

El script no saca ningún patrón vulnerable, y por lo tanto no produce el falso positivo. Gracias al análisis sobre el AST hemos podido determinar que la variable peligrosa se comprobaba en la sentencia condicional (if) y de esta forma automáticamente tomó el valor UNTAINT, al llegar a la función peligros strcpy con valor UNTAINT, el script no lo considera un patrón peligroso.

Conclusiones

Como conclusión, deciros que utilizar Taint Analysis + AST para soluciones rápidas y con mucho volumen de información, puede ser una solución rápida de programar, rápida de ejecutarse y relativamente efectiva, pero si lo que buscáis es construir una herramienta de análisis estático que funcione para todos los casos de uso, probablemente el AST no sea la mejor opción.

Esto es debido a que los nodos del árbol derivan directamente de las reglas de producción de la gramática, y por lo tanto puede introducir símbolos que existen solo con el propósito de hacer el proceso de parsing más sencillo o eliminar ambigüedades, además incluye bastante sintactic sugar procedente del código fuente del programa. En estos casos lo conveniente es ir transformando el AST en otro conjunto de representaciones intermedias del código fuente que abstraigan aún mas la representación del código a alto nivel.

Autor: Santiago Hernández, Security Researcher en ElevenPaths

No hay comentarios:

Publicar un comentario