Las vulnerabilidades bien conocidas de los sistemas implementados en el lenguaje de programación C no son sorprendentes si consideras la programación en C desde una perspectiva de seguridad.
La falta de seguridad de la memoria de C esencialmente significa que cualquier pieza del código puede modificar cualquier parte del espacio de direcciones. Además, por razones de rendimiento, las bibliotecas no suelen verificar los argumentos de la función.
El principio del menor privilegio establece que ninguna entidad debería tener más derechos de los necesarios para completar su tarea.
El conflicto inherente entre los dos puntos anteriores es obvio y empeora a medida que construimos sistemas cada vez más grandes en C: la mayoría del código puede hacer mucho más de lo que debería, particularmente con respecto a la modificación de la memoria. En un nivel alto, ataques de cadena de formato, así como muchos otros exploits, aprovechan esta debilidad de seguridad.
Aquí te proponemos un nuevo enfoque para prevenir ataques de cadena de formato. Combinamos la precisión y la seguridad de los enfoques de tiempo de ejecución con la facilidad de uso de la electricidad estática, el análisis y transformaciones automáticas de fuentes.
Indice
¿Qué es el error de formato de cadena?
Antes de explicar el ataque de cadena de formato, necesitamos saber cuál es el error de cadena de formato.
El lenguaje más afectado es C/C++. Un ataque exitoso puede originar inmediatamente la ejecución de código arbitrario y el descubrimiento de información. Por lo general otros lenguajes no permiten la ejecución de código arbitario, pero son proporcionados por entrada del usuario. Sin embargo, podrían permitirlo si las cadenas de formato se leen a partir de datos manipulados.
El error de formato de cadena es un error que ocurre cuando la cadena de formato printf(%d, %s) utilizada en la printf()función se usa de forma incorrecta.
Código vulnerable:
#include <stdio.h>
int main (int argc, char ** argv) {
printf (argv [1]);
}
Código de seguridad:
#include <stdio.h>
int main (int argc, char ** argv) {
printf («% s», argv [1]);
}
Esto se debe a que el ordenador reconoce el valor de entrada como un carácter de formato en lugar de un carácter.
El ataque de formato de cadena genera un error cuando un desarrollador escribe accidentalmente un printf()código sin una variable, y el hacker puede usar este error para robar la raíz.
Aunque no estés trabajando con C/C++, los ataques de cadena de formato quizá conduzcan a problemas importantes. El más normas es engañar a los usuarios, pero también pueden realizarse ataques de creación de cross site scripting o de inyección SQL bajo determinadas circunstancias. Esos errores también se usarán para corregir o modificar datos.
Dos vulnerabilidades utilizadas en el ataque de formato de cadena
- Si no hay un factor de formato de cadena después de la última cadena de formato ingresada, en términos de pila, desde el momento en que se llama a la función printf (), printf () considera en orden desde el contenido de la parte superior de la pila como factores de printf ().
- Estas cadenas de formato almacenan el número de bytes impresos por printf () al puntero de tipo int. %nalmacenar como 4bytes y %hnalmacenar como 2bytes.
Por lo tanto, si damos los valores adecuados frente a %n/ %hn, %n%hn consideraríamos ese valor como dirección y almacenaríamos el número de bytes impresos en la dirección correspondiente en la memoria.
¿Qué podemos hacer con los errores de formato de cadena?
Dado que una vulnerabilidad de formato de cadena nos da la capacidad de escribir un valor arbitrario en una dirección arbitraria, podemos hacer muchas cosas con ella.
Por lo general, lo más fácil es escribir en un puntero de función en alguna parte y convertir nuestra primitiva de escritura arbitraria en ejecución de código arbitrario. En programas vinculados dinámicamente, estos son fáciles de encontrar.
Cuando un programa intenta ejecutar una función en una biblioteca compartida, no necesariamente conoce la ubicación de esa función en tiempo de compilación. En cambio, salta a una función de código auxiliar que tiene un puntero a la ubicación correcta de la función en la biblioteca compartida. Este puntero (ubicado en la tabla de desplazamiento global, o GOT) se inicializa en tiempo de ejecución cuando se llama por primera vez a la función de código auxiliar.
¿Cómo podemos evitar el ataque de formato de cadena?
Existen varios métodos de prevención que podemos usar:
- Siempre especifica una cadena de formato como parte del programa, no como una entrada.
- Si es posible, convierte la cadena de formato en una constante. Extrae todas las partes variables como otros argumentos para la llamada.
- Usa defensas como Format_Guard. Raro en tiempo de diseño.
- Utiliza el sistema de parches. El desarrollo del kernel y la configuración de seguridad son más acerca de SetUID y complementan estas vulnerabilidades
- El uso normal de la función printf no causa ningún problema.
- Una vulnerabilidad de ataque de cadena de formato que no debe usarse en respuesta a un ataque de cadena de formato es la siguiente:
# 1
fp = fopen (* / dev / null «,» w «);
fprint (fp,» decimal =% d octal% o «, 123,123);
La fprintf función difiere de la printf función porque no envía los resultados a una salida estándar, sino a un archivo. En el ejemplo, fp es el valor del puntero para el archivo.
# 2
sprint (a, «decimal =% d octal =% o», 123,123);
print («% s», a);
La sprintf() función también genera el resultado como una cadena en lugar de como una salida estándar, que difiere de la printf() función.
Listado blanco
Proponemos una forma simple, flexible y directa de controlar la memoria modificada por una función (como printf): Una lista blanca explícita y dinámica de rangos de direcciones puede controlar las escrituras que pueden ser inseguras, como las explotadas por ataques de cadena de formato.
Podemos agregar y eliminar rangos de direcciones de la lista blanca en tiempo de ejecución, y podemos exigir que la escritura que deseamos proteger primero compruebe que hay una dirección en la lista blanca antes de escribir.
Se pueden ver las listas blancas como una representación directa de una simple pero fácil de entender política de seguridad. Cierto código debe modificar solo ciertas ubicaciones. Por supuesto, la lista blanca en sí no debe sucumbir a una modificación maliciosa, eso solo puede ocurrir si una aplicación ya está comprometida.
Para ver la flexibilidad de la lista blanca, ten en cuenta que puedes codificar fácilmente muchos enfoques estáticos:
- Predeterminado: una lista blanca que contiene el rango [0, 2 n – 1] (para un espacio de direcciones de n bits) permite cualquier escritura, desactivando efectivamente la protección. Esto puede ser necesario por ejemplo, para garantizar que el código heredado se ejecute sin cambios.
- Solo lectura: una lista blanca vacía garantiza que la escritura vulnerable que protege nunca se ejecute correctamente. Para cadenas de formato, la lista blanca vacía es equivalente para prohibir el carácter de formato% n.
- Sandboxing: una lista blanca que contiene solo el rango [2j 2 (j + 1) – 1] limita las escrituras a un rango 2j alineado, reflejando algunos enfoques para fallos de aislamiento de software.
Además, debido a que nuestras listas blancas son dinámicas, podemos expresar políticas dinámicas más interesantes y podemos cambiar la política en tiempo de ejecución si queremos.
En resumen, las listas blancas dinámicas son herramientas poderosas para aumentar la seguridad del código C al restringir la escritura en la memoria. En particular, previene ataques de cadena de formato. Es efectivo, fácil de usar y eficiente.
Prevención basada en lista blanca
Usar una lista blanca para evitar ataques de cadena de formato es sencillo:
- Necesitamos una lista blanca en tiempo de ejecución que contenga la dirección rangos que las funciones de impresión pueden escribir.
- Las funciones de impresión deben consultar la lista blanca antes de ejecutar el código para un calificador% n.
- Las personas que llaman a las funciones de impresión deben registrar (agregar a lista blanca) cualquier ubicación que pueda escribirse legalmente.
- Por rendimiento y seguridad, las personas que llaman deben anular el registro de ubicaciones después de que se invocan las funciones de impresión.
Representamos la lista blanca con una matriz de rangos de direcciones. En cualquier momento durante la ejecución del programa, espera que la lista blanca deba contener como máximo algunas direcciones. Almacenamos la lista blanca en una variable global.
Para verificar la lista blanca, antes de ejecutar un calificador% n debes verificar que la ubicación a punto de escribirse está en un rango de direcciones registradas.
Por lo tanto, la función verifica la matriz de la lista blanca como una pila, comenzando con el rango registrado más recientemente. Podemos ya sea modificar (o reimplementar) las funciones de impresión, o podemos envolverlas con una función que verifica la lista blanca. El primero tiene la ventaja del rendimiento (análisis la cadena de formato solo una vez), pero la desventaja es que
cambia intrusivamente o evita la biblioteca estándar. Si una verificación de la lista blanca falla, elegimos abortar el programa, pero son posibles otras opciones.
Proporcionamos una API muy simple para que los usuarios ajusten lista blanca:
- __register (x, y) agrega el rango [x, y) a la lista blanca.
- __register_word (x) agrega el rango [x, x] a la lista blanca. Podemos usar esta función para registrar direcciones señalando por argumentos que están destinados a ser utilizados como % n objetivos.
- __unregister () elimina el rango agregado más recientemente pero aún no eliminado de la lista blanca (es decir, aparece
la pila). Esto es más eficiente que buscar en la lista blanca para una dirección específica, y ha sido suficiente para nuestros propósitos.
Para los programas que nunca usan __register, podemos implementar una lista blanca más simple simplemente deshabilitando el modificador% n.
Llamar a __unregister () en una lista blanca vacía no tiene ningún efecto. Por seguridad, los clientes deben registrar la menor cantidad de rangos posible y anular el registro lo antes posible. Eso
significaría registrar los argumentos correctos justo antes de llamar a una función de impresión y anulándolas inmediatamente después. Sin embargo, las funciones de contenedor que pasan
va_lists a funciones como vfprintf no puede hacer el registro: no conocen el número o tipos de argumentos en la lista. Por lo tanto, debemos registrarnos en el sitio de llamada de la función cuyos argumentos son señalados por la lista_va).
Resumen
En resumidas cuentas, hemos podido observar como a través de una aplicación mal diseñada se puede inyectar trozos de código malicioso. En este caso una shell que nos devuelva el control como root del equipo comprometido, encontrándonos ante un problema de seguridad muy sencillo de detectar y explotar por cualquier atacante.
A día de hoy, muchas son las medidas que se han implementado a nivel de sistemas para aumentar la protección del mismo frente a este tipo de ataques, técnicas como ASLR, DEP y los stack canary surgen como contramedida a este tipo de amenazas.
Las medidas que debes adoptar para evitar estos errores son:
- Utilizar cadenas de formato fijas o cadenas de formato provenientes de una fuente confiable.
- Comprobar y limitar los requerimientos del lugar a valores válidos.
- No pasar entrada de usuario directamente como la cadena de formato a funciones de formateo.
- Tener en cuenta el uso de lenguajes de más alto nivel que tiendan a ser menos vulnerables a este problema.