Programación Segura: Desbordamientos del Búfer

Revisión General del Desbordamiento del Búfer

Durante mucho tiempo se ha reconocido que los desbordamientos del Búfer son un problema en lenguajes de bajo nivel. La esencia del problema es que los datos de usuario y la información de control de flujo del programa se mezclan en beneficio del desempeño, y los lenguajes de bajo nivel permiten acceso directo a la memoria de aplicación. C y C++ son los dos lenguajes más populares afectados por los desbordamientos del Búfer.

En sentido estricto, ocurren desbordamiento del Búfer cuando un programa permite la entrada de escritura más allá del final del Búfer asigando, pero hay varios problemas asociados que a menudo tienen el mismo efecto; uno de los más interesantes consiste en los errores de cadena de formato, que se analizarán en “Problemas de cadena de formato” . Otra personificación del problema ocurre cuando se permite que un atacante escriba en una ubicación arbitraria de memoria fuera de una matriz en la aplicación, y aunque no se trata, de manera estricta, de un desbordamiento clásico del Búfer, también lo estudiaremos aquí.

El efecto de un desbordamiento del Búfer es amplio, desde la caída del sistema hasta la apropiación completa del control de la aplicación por parte del atacante, y si la aplicación se ejecuta como usuario de alto nivel (raíz, administrador o sistema local), entonces el atacante entre sus manos el control de todo el sistema operativo y de todos los usuarios que hayan iniciado sesión o la iniciarán en el futuro. Si la aplicación en cuestión es un servicio de red, el resultado del error podría ser un gusano. El primer gusano bien conocido de Internet aprovechó un desbordamiento del servidor finger, y fue conocido como gusano finger de Robert T. Morris ( o sólo Morris). Aunque al parecer hemos aprendido a evitar desbordamientos del Búfer desde que uno casi colapsó Internet en 1998, aun vemos con frecuencia informes desbordamientos del Búfer en muchos tipos de software.

Aunque podría pensarse que sólo programadores perezosos y descuidados serían víctimas de los desbordamientos del Búfer, el problema es complejo; muchas de las soluciones no son simples y cualquier persona que el escrito suficiente código C/C++ ha cometido, casi sin lugar a dudas, este error.

Lenguajes afectados

C el lenguaje que se usa con más frecuencia para crear desbordamientos del Búfer, seguido de cerca por C++. Es fácil crear desbordamientos del búfer cuando se escribe en ensamblador, porque no cuenta con ningún tipo de protección. Aunque inherentemente C++ es tan peligroso como C porque se trata de un superconjunto de éste, el uso cuidadoso de la Biblioteca Estándar de Plantillas (STL, Standar Template Library) llega a reducir gran medida la posibilidad de manejar erróneamente las cadenas. El aumento en el rigor del compilador de C++ ayudará al programador a evitar algunos errores. nuestro consejo es que aunque esté escribiendo código C puro, el uso del compilador de C++ dará como resultado un código más limpio.

Los lenguajes de más alto nivel inventados en fecha reciente abstraen del programador el acceso directo a la memoria, por lo general con un coste importante para el desempeño. Lenguajes como Java, C# y Visual Basic tienen tipos de cadenas nativos, matrices de límites verificados y, por lo general, prohiben el acceso directo a la memoria. Aunque algunos dirían que esto hace que los desbordamientos del Búfer sean imposibles, es más preciso decir que son menos probables. En realidad, casi todos estos lenguajes se implementan en C/C++, y es posible que las fallas en la implementación produzcan desbordamientos del Búfer. Otra fuente posible de desbordamientos del Búfer en código de más alto nivel se debe a que el código hace interfaz en última instancia con un sistema operativo, ya que éste se encuentra escrito casi con toda seguridad en C/C++. C# le permite trabajar sin red al declarar secciones inseguras; sin embargo, aunque proporciona una interoperabilidad más fácil con el sistema operativo y bibliotecas escritas en C/C++, tal vez cometa los mismos errores que en C/C++. En caso de que programen primordialmente en lenguajes del más alto nivel, la principal acción que deberá realizar es seguir validando los datos pasados a bibliotecas externas, o puede actuar como conducto para sus errores.

Aunque vamos a proporcionar una lista de lenguajes afectados, casi todos los lenguajes más antiguos son vulnerables a desbordamientos del Búfer.

Explicación del desbordamiento del Búfer.

A la personificación clásica de un desbordamiento del Búfer se le conoce como “rompimiento de la pila”. En un programa compilado la pila se utiliza para contener la información de control, como argumentos, a la que debe regresar la aplicación una vez que haya terminado con la función y, debido a la pequeña cantidad de registros disponibles en procesadores X86, muy a menudo los registros se almacenan de manera temporal en la pila. Por desgracia, las variables que se asigna de manera local también se almacenan en la pila. Algunas veces a estas variables de pila se les identifica en forma imprecisa como si se les asigna de manera estática, en oposición a la asignación de memoria dinámica. Si escucha a alguien hablar sobre un desbordamiento del Búfer estático, lo que en realidad quiere decir es desbordamiento del Búfer de pila. la raíz del problema es que si la aplicación escribe más allá de los límites de una matriz asignada la pila, el atacante tiene la posibilidad de especificar información de control. Y esto es crítico para lograr su objetivo; el atacante busca la modificación de la información de control por valores que el controla.

Uno se preguntaría por qué seguir utilizando un sistema tan evidentemente peligroso. Tenemos una oportunidad de evitar el problema, cuando menos en parte, con la migración al chip Itanium de 64 bits de Intel, donde las direcciones de regreso se almacenan en un registro. El problema es que hemos tenido que tolerar una pérdida de compatibilidad importante con versiones anteriores; al momento de escribir esto, es posible que el chip X64 termine convirtiéndose en el chip más popular.

Tal vez también se esté preguntando por qué no todos miramos a un código que realice verificación de matriz estricta y desactive el acceso directo a la memoria. El problema es que, en muchos tipos de aplicaciones, las características de desempeño de lenguajes de más alto nivel no son adecuadas. Una solución intermedia consiste en utilizar lenguajes de más alto nivel para las interfases de alto nivel que interactúan con elementos peligrosos (¡como los usuarios! ), y lenguajes de más bajo nivel para el código principa. Otra solución es utilizar plenamente las opciones que ofrece C++ y y emplear bibliotecas de cadena y clases de colección. Por ejemplo, el servidor web Internet Information Server (IIS) 6.0 cambió por completo una clase de cadena C++ para manejo de entrada, y un desarrollador valiente afirmó que se amputaría su dedo meñique si encontraba algún desbordamiento del Búfer en su código. Al momento de escribir esto, el desarrollado aún tenía su dedo y no se habían publicado boletines de seguridad contra el servidor web en los casi dos años desde su lanzamiento. Los compiladores modernos se relacionan de manera adecuada en clases de plantillas, y es posible escribir código C++ de muy elevado desempeño.

C/C++
Hay demasiadas maneras de desbordar un búfer en C/C++. He aquí lo que causó el gusano finger de Morris:

char buf [20];
gets (buf);

No hay manera de utilizar gets para leer entrada desde stdin sin arriesgarse a un sobreflujo de búfer (es mejor utilizar fgets). Quizás la segunda manera más popular de desbordar bufers es mediante el uso de strcpy. A continuación se muestra otra forma de causar problemas:

char buf [20];
char prefix [] = “http://”;
strcpy (buf, prefix);
strncat (buf, path, sizeof (buf));

¿Qué fue lo que estuvo mal? El problema aquí es que strncar tiene una interfaz diseñada de manera deficiente. La función busca el número de caracteres del búfer disponible, o espacio restante, no el tamaño total del búfer de destino. He aquí otra manera predilecta de causar sobreflujos:

char buf [MAX_PATH];
sprintf(buf, “%s - %d\n”, path, errno);

Resulta casi imposible, excepto en unos cuantos casos de esquina, utilizar sprintf de manera segura. Se publicó un boletín de seguridad crítico para Microsoft Windows debido a que se empleó sprintf en una función de inicio de sesión de depuración. Consulte el boletín MS04-011 para conocer más información.
He aquí otro favorito:

char buf [32];
strncpy (buf, data, strlen(data));

¿Qué estuvo mal aquí? El último argumento es la longitud del búfer entrante, ¡no el tamaño del búfer de destino!

Otra manera de causar problemas es confundir el conteo de caracteres con el de bytes. Si está trabajando con caracteres ASCII, son iguales, pero si lo hace con Unicode, hay dos bytes por cada caracter. He aquí un ejemplo:

_snwprintf(wbuf, sizeof(wbuf), “%s\n”, entrada);

El siguiente desbordamiento es un poco más interesante:

bool CopyStructs(InputFile* pInfile, unsigned long count)
{
unsigned long i;
m_pStructs = new Structs [count];
for(i = 0; i <count; i++)
{
if(!ReadFronFile(pInFile, &(m_pStructs[i])))
break;
}
}

¿Cómo puede fallar esto? Tome en cuenta que cuando llama el operador new[] de C++, es similar al siguiente código:
ptr = malloc (sizeof(type)) * count);

Si el usuario suministra count, es difícil especificar un valor que desborde la operación de multiplicación de manera interna. Debido a esto, asiganará un búfer mucho más pequeño de lo que necesita, con lo que el atacante podrá escribir sobre su búfer. El compilador de C++ de Microsoft Visual Studio 2005 contiene una comprobación interna para evitar este problema. El mismo problema llega a ocurrir de manera interna en muchas implementaciones de calloc, que realizan la misma operación. Éste es el meollo de muchos errore de sobreflujo de enteros: no es el sobreflujo de enteros lo que causa el problema de seguridad, sino que el desbordamiento del búfer que se origina con rapidez es el causante de los dolores de cabeza.

Localización del patrón del desbordamiento del Búfer

  • Entrada, si se lee de la red, un archivo o la línea de comandos
  • Transferencia de datos de la entrada mencionada a estructuras internas
  • Uso de llamadas inseguras a manejo de cadeas
  • Uso de aritmética para calcular un tamaño de asignación o el tamaño del búfer restante

Ejemplos de desbordamiento del Búfer

Las siguientes entradas, que se obtuvieron directamente del la lista Common Vulnerabilities and Exposures (Vulnerabilidades y exposiciones comunes), o CVE, son ejemplos de desbordamientos del búfer.

CVE-1999-0042
CVE-2000-0389-CVE-2000-0392
CVE-2002-0842, CVE-2003-0095, CAN-2003-0096
CAN-2003-0352

Metodos de prevención

El camino a la prevención de los debordamientos del búfer es largo y está lleno de obstáculos. Analizaremos una amplia varidad de técnicas que le ayudarán a evitar desbordamientos del búfer y varias otras para reducir el daño que llegan a causar dichos desbordamientos.

  • Reemplace funciones peligrosas de manejo de cadenas
  • Deberá, por lo menos, reemplazar funciones inseguras como strcpy, strcat con las versiones validas de cada una de estas funciones. Tiene varias opciones a su disposicón para reemplazarlas. Tenga en mente que las funciones válidas más antiguas tienen problemas de interfaz, y en muchos casos se le csolicitará que realice operaciones aritméticas para determinar parámetros.

Audite asignaciones
Otra fuente de desbordamientos del búfer proviene de errores aritméticos.

Verifique bucles y accesos a matrices
Una tercera manera en que se originan desbordamientos del búfer es cuando no se verifica adecuadamente la terminación en bucles ni los límites de matriz antes de escribir el acceso. Se trata de una de las áreas más dificiles, y encontrará que, en algunos casos, el problema y su solución están en módulos por completo diferentes.

Reemplace bufers de cadena de C con cadenas de C++
Esto es más eficaz que sólo reeplazar las llamadas de C habituales, pero es posible que cause gran cantidad de cambios en código existente, en particular si el código no está ya compilado como C++.

Reemplace matrices estáticas con contenedores de STL
Todos los probelmas mencionados se aplican a contenedores de STL como vector, pero un problema adicional es que no todas las implementaciones de la contrucción vector::iterator verifican el acceso fuera del límite.

Utilice herramiemtas de análisis
En el mercado están disponibles algunas buenas herramientas que analizan el código de C/C++ en busca de defectos de seguridad; entre los ejemplos se encuentran Coverty, PREfast y Klocwork.

Medidas defensivas adicionales
Protección de pila
La protección de pila fué incluida por primera vez por Crispin Cowan en su producto Stackguard y fue implementada de manera independiente por Microsoft como conmutador del compilador /GS. En su forma más básica, la protección de pila coloca un valor conocido como canary en la pila entre las variables locales y la dirección de regreso.

Pila y heap no ejecutables
Esta medida ofrece protección importante contra un atacnate, pero tal vez tenga impacto considerable en la compatibilidad de la apliación. Algunas aplicaciones compilan y ejecutan de manera legítima código al vuelo, como es el caso de muchas aplicaciones escritas en Java y C#. Además, es importante obervar que si el atacante puede causar que su aplicación sea presa de un regreso en un ataque de libc, donde se realiza un llamada legítima a función para finales dañinos, entonces puede eliminarse la protección de ejecución en la página de memoria.

Resumen

  • Verifique con mucho cuidado sus accesos al búfer a través de funciones de manejo de cadena y búfer seguras.
  • Utilice defensas basadas en el compilador, como /GS y ProPolice.
  • Utilice defensas contra el desbordamiento del búfer para el sistema operativo, como DEP y PaX.
  • Determine cuáles son los datos que el atacante controla y administre esos datos de manera segura en su código.
  • No suponga que las defensas del compilador y el sistema operativos son suficientes; no lo son; sólo se trata de defensas adicionales.
  • No cree nuevo código que utilice funciones inseguras.
  • Tome en cuenta la actualización de su compilador C/C++, por que los autores del compilador agregan más defensa al código generado.
  • Tome en cuenta la eliminación de funciones inseguras de código antiguo con el tiempo.
  • Tome en cuenta el empleo de clases de cadena y contenedor de C++ en lugar de las funciones de cadena de C de más bajo nivel.
Subir