Programación Segura: Problemas de cadena de formato

Continúa desde “Programación Segura: Desbordamientos del Búfer

Revisión General de los Problemas de cadena de formato.

Los problemas de cadena de formato contituyen uno de los pocos ataques realmente nuevos que surgieron en años recientes.
Al igual que con muchos problemas de seguridad, la principal causa de los errores de cadena de formato es aceptar sin validar la entrada proporcionada por el usuario. En C/C++ es posible utilizar errores de cadena de formato para escribir en ubicciones de memoria arbitrarias, y el aspecto mpas peligroso es que esto llega a suceder sin manipular bloques de memoria adyacentes. Esta capacidad de diseminación permite a un atacante eludir protecciones de pila, e incluso modificar partes my pequeñas de memoria. El problema también llega a ocurrir cuando las cadenas de formato se leen a partir de una ubicación no confiable que controla el atacante. Este último aspecto del problema tiende a ser más frecuente en sistemas UNIX y Linux. En sistemas Windows las tablas de cadena de aplicación suelen mantenerse dentro del progrma ejecutable o de las bibliotecas de vínculo dinámico (DLL, Dynamic Link Libraries) del recurso. Si un atacante reeescribe el ejecutable principal o de las DLL, tendrá la posibilidad de realizar ataques mucho más directos que con errores de cadena de formato.

Aunque no esté trabajando con C/C++, los ataques de cadena de formato quizá conduzcan a problemas importantes; el más obvio es engañar a los usuarios, pero bajo ciertas circustancias es posible que un atacante lance ataques de creación de script de sitio cruzado o de inyección de SQL, los cuales también se utilizan para corregir o transformar datos.

Lenguajes Afectados

El lenguaje más afectado es C/C++. Un ataque exitoso quizá conduzca de manera inmediata a la ejecución de código arbitrario y a revelación de información. Por lo general otros lenguajes no permiten la ejecución de código arbitario, pero, como ya se observó, son proporcionados por entrada del usuario, pero podría serlo si las cadenas de formato se leen a partir de datos manipulados.

Explicación del problema de cadena de formato

El formateo de datos para despliegue o almacenamiento tal vez represente una tarea un poco difícil; por tanto, en muchos lenguajes de computadora se incluyen rutinas para reformatear datos con facilidad. En casi todos los lenguajes la información de formato se describe a través de un tipo de cadena, denominada cadena de formato. En realidad, la cadena de formato se define con el uso de lenguaje de procesamiento de datos limitado que está diseñado para facilitar la descripción de formatos de salida. Sin embargo, muchos desarrolladores cometen un sencillo error: utilizan datos de usuarios no confiables como cadena de formato; el resultado es que los atacantes pueden escribir cadenas en el lenguaje de procesamiento de datos para causar muchos problemas.

El diseño de C/C++ hace que esto sea especialmente peligroso: dificulta la detección de problemas de cadena de formato, y entre las cadenas de formato se incluyen algunos comandos muy peligrosos (en particular %n) que no existen en lenguajes de cadena de formato de algunos otros lenguajes.

En C/C++ puede declararse una función para que tome un número de variable de argumentos al especificar puntos suspensivos (…) como el último (o único) argumento. El problema es que la función a la que se llama no tiene manera de saber cuántos argumentos se están pasando. El conjunto más común de funciones que toman argumentos de longitud variable es la familia de printf: printf, sprintf, snprintf, vprintf, etc. Las extensas funciones de carácter que realizan la misma función tienen el mismo problema. Veamos un ejemplo:

#include <studio.h>
int main (int argc, char* argv[])
{
if (argc > 1)
printf(argv[1]);
return 0;
}

Algo muy sencillo. Ahora veamos lo que está mal. El programador está esperando que el usuario introduzca algo benigno como Hola mundo. Si lo intenta, obtendrá Hola mundo. Ahora cambiemos un poco la entrada: pruebe %x %x. En un sistema Windows XP con la línea de comandos predeterminada (cmd. exe) , ahora obtendrá lo siguiente:

E:\proyectos\programación_segura\format_bug>format_bug.exe “%x %x”
12ffc0 4011e5

Observe que si está ejecutando un sistema operativo diferente o utilizando un interprete de línea de comandos diferente, tal vez necesite realizar algunos cambios para alimentar esta cadena exacta en su programa, y es posible que los resultados sean diferentes. Para facilitar su uso podría colocar los argumentos en una línea de comandos de shell o en un archivo de procesamiento por lotes.

¿Qúe sucedió? La función de printf tomó una cadena de entrada que hizo que esperara a que se colocaran dos argumentos en la pila antes de llamar a la función. Los especificadores %x le permiten leer la pila, de cuatro en cuatro bytes, hasta donde se desee. No es difícil imaginar que si tuviera una función más compleja que almacenara un secreto en una variable de pila, el atacante tendría la posibilidad de leerlo. Aquí la salida es la dirección de la ubicación de la pila (0×12ffc0), seguida de la ubicación del código en que se devolverá la función main() . Como se imagina, ambas son piezas de información muy importantes que se filtran al atacante.

Tal vez ahora se esté preguntando cómo utiliza el atacante un error de cadena de formato para escribir en la memoria. Uno de los especificadores de formato menos utilizados es %n, que escribe el número de caracteres que deben haberse escrito hasta el momento en la dirección de la variable que proporcionó como argumento correspondiente. He aquí cómo debe utilizarse:

unsigned int bytes;
printf (”%s%n\n”, argv[1], &bytes);
printf (su entrada fue de %d caracteres de largo\n, bytes”);

La salida sería:

E:\proyectos\programacion_segura\format_buf>format_bug2.exe “alguna salida aleatoria”
Alguna entrada aleatoria
Su entrada fue 17 caracteres de largo

En una plataforma con enteros de cuatro bytes el especificador %n escribirá cuatro bytes a la vez, y %hn escribirá dos bytes. Ahora los atacantes sólo tienen que imaginar cómo obtener la dirección que desean en la posición apropiada de la pila y ajustar los especificadores de ancho de campo hasta que el número de bytes escrito sea el que desean.

C/C++

A diferencia de muchos otros errores que examinaremos, éste es basatante fácil de localizar como defecto de código. Es muy sencillo:

printf (entrada_usuario);
Es incorrecto, y

printf (”%s”, entrada_usuario);
Es correcto .

Localización de problemas de cadena de formato

Cualquier aplicación que tome entrada del usuario y la pase a una función de formateo está en riesgo. Un ejemplo muy frecuente de este problema sucede junto con aplicaciones que registran la entrada de usuario. Además, algunas funciones tal vez implanten formateo de manera interna.
En C/C++ busque funciones de la familia printf. Entre los problemas que habrá de localizar se encuentran los siguientes:

printf (entrada_usuario) ;
fprintf (STDOUT, entrada_usuario);

Si se encuentra con una función con el siguiente aspecto:

fprintf (STDOUT, msg_format, arg1, arg2);

necesitará verificar dónde se almacena la cadena a la que hace referencia msg_formato y si está bien protegida.
Hay muchas otras llamadas de sistema y API que también son vulnerables, syslog es una de ellas. Cuando vea una definición de función que incluya en la lista de argumentos, esta viendo algo que posiblemente represente un problema.
Muchos escáneres de código fuente, aun los de léxico como RATS y Flawfinder, detectan esto. Incluso PSCAN se diseñó de manera especifica con este propósito.
Además, existen herramientas de respuesta que se integran en el proceso de compilación; por ejemplo FormatGuard de Crispin Cowan.

Técnicas de prueba para encontrar el problema de cadena de formato

Pase especificadores de formateo a la aplicación y vea si se regresan valores hexadecimales. Por ejemplo, si tiene una aplicación que espera un nombre de archivo y regresa un mensaje de error que contiene la entrada cuando no se encuentra el archivo, entonces trate de proporcionarle nombres de archivo como NoProbable%x%x.txt. Si obtiene un mensaje de error parecido a las siguientes líneas “NoProbable12fd234104587“, acaba de encontrar una vulnerabilidad de cadena de formato.

Ejemplos de Problemas de Cadena de Formato

Entradas en la lista Common Vulnerabilities and Exposures, CVE.

CVE-2000-0573
CVE-2000-0844

Métodos de prevención

El primer paso es nunca pasar entradas de usuario directamente a un función de formateo, además de asegurarse de hacerlo en cada nivel de manipulación de salida formateada. Como nota adicional, las funciones de formateo tienen una sobrecarga de trabajo importante. Si está interesado, busque _output en el archivo fuente, tal vez sea conveniente escribir:

fprintf (STDOUT, buf);

la anterior línea de código no es sólo peligrosa, sino que también consume muchos ciclos de CPU adicionales.

El segundo paso que habrá de darse consiste en asegurar que las cadenas de formato que utiliza su aplicación se lean desde lugares confiables, y que las rutas a las cadenas no sean controladas por el atacante.

Medidas defensivas adicionales

Verifique y limite la ubicación a valores válidos. No utilice la familia de funciones printf, si puede evitarlas. Por ejemplo, si está empleando C++, use operadores de flujo:

#include <iostream>
//…
std::cout <<entrada_usuario
//…

Resumen

  • Utilice cadenas de formato fijas o cadenas de formato provenientes de una fuente confiable.
  • Compruebe y limite los requerimientos del lugar a valores válidos.
  • No pase entrada de usuario directamente como la cadena de formato a funciones de formateo.
  • Tome en cuenta el uso de lenguajes de más alto nivel que tiendan a ser menos vulnerables a este problema.
Subir