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?
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:
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?
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.
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.
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:
Para ver una representación del AST producido por Clang de ese programa, ejecutamos el siguiente comando:
clang -Xclang
-ast-dump programa.c
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:
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 |
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.
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.
No hay comentarios:
Publicar un comentario