En ocasiones anteriores he recomendado el curso de Software Exploits de Open Security Training, así como el libro The Shellcoder’s Handbook: Discovering and Exploiting Security Holes. Tomando los ejemplos presentados en el libro, me pareció una buena idea revisar cómo se cumple la teoría en vulnerabilidades reales.

Copia de datos

Una de las vulnerabilidades más sencillas de ver a simple vista está relacionada con la copia de datos a búferes con funciones como strcpy, sin control del tamaño de la copia.

vulnerabilidad strcpy IMAP

Arriba vemos parte del código vulnerable del servidor IMAP de la Universidad de Washington, el cual fue corregido en 1998. Vemos que nunca se comprueba el tamaño de los datos en mechanism antes de hacer la copia a tmp, lo cual puede llevar al desbordamiento del búfer. El error se resuelve fácilmente con una comprobación de strlen(mechanism) antes de la copia, o con el uso de funciones de copia de n bytes, como strncpy.

Como podrás imaginar, es muy difícil que hoy en día se encuentre este tipo de vulnerabilidades en aplicaciones de código abierto y, de existir, son rápidamente corregidas.

Índices incorrectos en bucles

Cuando los bucles iterativos no tienen sus índices o condiciones de corte bien programadas, puede ocurrir la copia de más bytes de los deseados: un byte (off-by-one) o unos cuantos (off-by-a-few). Veamos esta vieja versión del demonio de FTP de OpenBSD:

vulnerabilidad ftp OpenBSD

Si bien en el código se trata de reservar un byte para el carácter nulo de terminación de string, cuando el tamaño de name es mayor o igual al de npath, y el último byte a copiar es “ (comillas dobles), vemos que el índice i se incrementa de más en la instrucción resaltada, produciendo que el carácter nulo sea insertado un byte después del límite, generando un desbordamiento.

Los bucles que parsean strings o que manejan inputs del usuario suelen ser buenos lugares para buscar vulnerabilidades. Otro ejemplo se presenta nuevamente en el servidor IMAP de la Universidad de Washington (CVE-2005-2933):

vulnerabilidad bucle IMAP

En la línea 20 se busca un carácter de comillas dobles entre la string que se está parseando. Si se encuentra, el bucle de la línea 22 va a copiar hasta que se encuentre un segundo carácter de comillas dobles. Es claro que si se ingresa una string con uno solo de estos caracteres, el bucle va a continuar copiando, produciendo un desbordamiento en el stack.

Desbordamiento de números enteros

Es común que, al intentar evitar la copia excesiva de datos a un búfer mediante la comprobación del tamaño a copiar, el número de bytes a copiar supere el número más grande que puede ser representado por el tipo de datos, rotando hacia valores pequeños. La comprobación se cumple porque el tamaño es interpretado como un número pequeño, pero la copia es de un número muy grande de elementos, produciendo un desbordamiento del búfer de destino.

Un ejemplo es cuando se utiliza una multiplicación que produce resultados muy grandes, como en este código de OpenSSH, en sus versiones anteriores a la 3.4:

vulnerabilidad enteros OpenSSH

Se observa que nresp almacena el tamaño de un paquete ingresado por el usuario. En la instrucción resaltada, nresp es multiplicado por el tamaño de un puntero (4 bytes). Luego, si nresp almacena un valor mayor a 1073741823, la multiplicación va a superar el tamaño máximo de unsigned int (4294967295). En otras palabras, malloc va a reservar una cantidad pequeña de memoria y el bucle va a copiar una gran cantidad de datos, produciendo un desbordamiento. Este tipo de vulnerabilidades sigue siendo relevante y común en nuestros días.

Estos son solo algunos tipos de vulnerabilidades; recomiendo la consulta del libro mencionado para ver más ejemplos. Si bien algunas, como las de strcpy, ya son muy difíciles de ver en aplicaciones de código abierto en la actualidad, aún siguen presentes en aplicaciones cerradas, propietarias, o que no hayan visto auditorías de código.

Como regla general, si vamos a buscar vulnerabilidades en aplicaciones de código abierto, es recomendable revisar aquellas porciones de código que raramente son ejecutadas, con funcionalidades especiales, ya que son más propensas a errores. Además, como ya mencionamos, cuando los desarrolladores implementan sus propias funciones de parseo o manejo de strings, tramas de datos y demás, es más probable que cometan errores.