Documente Academic
Documente Profesional
Documente Cultură
Codificación segura
Tema 3: Codificación segura
En sitio Web CVE Detail1, se muestra un gráfico con los tipos de vulnerabilidades más
comunes registradas, desde el año 1999 hasta el momento en que se visualiza la web, y la
proporción que cada uno de ellos representa sobre el total de vulnerabilidades
registradas. La mayoría de ellas son debidas a errores de codificación por no seguir
prácticas adecuadas, ni tener los conocimientos pertinentes.
Figura 1. Vulnerabilidades más comunes por tipo desde el año 1999 hasta el 2017.
Cabe destacar que gran parte de las vulnerabilidades como, denegación de servicio,
inyección de código, cross-site scripting (explotados en aplicaciones Web), inyección de
código y SQL (explotados en todo tipo de aplicaciones), están presentes
mayoritariamente en el gráfico, aunque otro muy común como la gestión de la memoria
sigue presente desde hace muchos años. Otro tipo de vulnerabilidad que es cada vez más
popular, es la escalada de privilegios.
En este tema se van a explicar los defectos más comunes que se pueden cometer al
codificar en lenguajes como C y Java. Si se entienden bien los defectos que se pueden
cometer se estará en disposición de poder analizar el código para encontrarlos y
posteriormente corregirlos. La formación por tanto en esta materia de los
programadores es muy importante, porque si se conoce que tipo de defectos
de seguridad se pueden cometer en un lenguaje de programación se pueden
evitar muchos de ellos desde el principio. Además se hacen una serie de
recomendaciones a tener en cuenta a la hora de la implementación, tanto de buenas
como de malas prácticas.
1
http://www.cvedetails.com/vulnerabilities-by-types.php
El encontrar bugs tempranamente conlleva una reducción del coste económico, en varios
órdenes, con respecto a su descubrimiento en fases posteriores. Este hecho tiene que
incitar a las organizaciones a poner especial interés en esta actividad y encuadrarla en
los procesos y procedimientos de desarrollo.
Buenas prácticas:
o Formarse uno mismo.
o Formación continúa.
o Manejo de los datos con precaución:
– Comprobar los datos de entrada al sistema.
– Comprobar los límites de las estructuras de datos.
– Comprobar los ficheros de configuración.
– Comprobar los parámetros de los comandos codificados en programas.
– No confiar en URLs.
– Tener cuidado con el contenido WEB.
– Comprobar las variables de entorno.
– Comprobar cookies.
– Inicializar las variables correctamente.
– Identificar las referencias a ficheros correctamente y utilizarlas correctamente.
– Poner especial atención en el almacenamiento de información de carácter
confidencial.
Malas prácticas:
o Usar en el código nombres relativos de ficheros.
o Referirse a un fichero con el mismo nombre dos veces en el mismo programa.
o Invocar programas en los que no se confía desde otros en los que se confía.
o Asumir que los usuarios no son maliciosos.
o No utilizar librerías seguras que generan números aleatorios como ramdom.
o Invocar Shell desde líneas de comandos.
o Utilizar direcciones IP, MAC o direcciones de correo para identificar usuarios.
o Utilizar zonas de memoria accesibles por todos los usuarios.
o Almacenar información sensible en una base de datos sin protección.
o Visualizar passwords en las pantallas de los usuarios.
o Codificar passwords en la aplicación.
o Almacenar passwords en disco sin cifrar.
o Transmitir passwords sin cifrar.
o Confiar solamente en los mecanismos de protección de ficheros del sistema
operativo, se debe implementar mecanismos adicionales en la aplicación.
o Tomar decisiones de acceso basadas en variables de entorno o parámetros de línea
de comando pasados en tiempo de ejecución.
o Almacenar la aplicación en un sistema de ficheros NFS.
o Confiar en software de terceros en operaciones críticas.
Estos pueden ser solo algunos de los aspectos más generales a tener en cuenta a la hora
de desarrollar y como se verá más adelante, sobre todo a la hora de hablar de defectos de
implementación de código, hay que tener en cuenta los defectos particulares que se
pueden cometer con el lenguaje de programación que se está utilizando, el compilador
que se utiliza, y otro software de terceros que seguramente no se tendrá posibilidad de
verificar, pero que formará parte del sistema, y por tanto podría introducir errores en el
sistema que se está implementando.
Desbordamiento de Buffer
Errores y excepciones
Privacidad y confidencialidad
Programas privilegiados
Introducción
Una de las medidas de defensa más importantes que un programador puede llevar a
cabo, es validar todas las entradas que el sistema recibe. Son la fuente de
algunas de las peores vulnerabilidades de las aplicaciones, como son el
desbordamiento de buffer, inyección de SQL y otras.
Los atacantes pueden manipular los diferentes tipos de datos de entrada con el
fin de entregar cargas maliciosas a las aplicaciones. Los tipos de entrada siempre deben
ser probados por un sistema de validación de entrada, para evitar este tipo de
ataques.
El programador es el individuo más cualificado para definir las clases de entrada que son
válidas en el contexto de su código. No puede ser responsable de saber si todas la
entradas que se reciben son correctas, sin embargo, si lo es de asegurar que la
entrada que se acepta no es obviamente incorrecta. No hay que esperar que la
entrada sea formateada correctamente, hay que tener sentido común, ser coherente,
seguir convenciones de codificación y estándares.
Qué validar
o Validar toda la entrada. Validar cualquier entrada que utilice el programa.
Hacer fácil verificar que toda la entrada sea validada antes de que sea usada.
o Validar la entrada de todas las fuentes. Incluyendo parámetros de línea de
comandos, archivos de configuración, consultas de base de datos, variables de
entorno, servicios de red, valores de registro, propiedades de sistema, archivos
temporales, y cualquier otra fuente externa de entrada.
o Establecer fronteras de confianza. Almacenar datos en los que se confía
separadamente de los que no se confía, para asegurar que la validación siempre se
realiza.
Cómo validar
o Usar una fuerte validación de entrada. Usar la forma más fuerte de
validación de entrada aplicable en un contexto dado.
o Evitar poner en la lista negra. No echar mano de la lista negra solamente
porque una validación más fuerte de la entrada es difícil de realizar.
o No confundir usabilidad y seguridad.
o No confundir la validación de funcionalidad con la validación de
entrada para la seguridad.
La mayoría de los programas aceptan entradas de múltiples fuentes y operan con los
datos que aceptan de muchas maneras. La validación de entrada juega un papel crítico
en la seguridad. Un programa primero lee datos de una fuente exterior y después los usa
en cualquier contexto relevante de seguridad.
Estas tres palabras serán repetidas durante todo el apartado: validar toda la entrada.
Hay que definir la entrada detalladamente y pensar más allá de los datos que un usuario
puede enviar. Si una aplicación consiste en más de un proceso, validar la entrada de
cada uno, incluso si aquella entrada llega de otra parte de la aplicación.
Validar la entrada incluso si llega por una conexión segura, llega de una fuente confiable
o está protegida según permisos de archivo estrictos.
Validar la entrada incluso para los programas que son accedidos por usuarios validados
o en los que se confía. No hacer suposiciones optimistas sobre ninguna parte de la
entrada heredada del entorno, incluyendo valores de registro y nombres de ruta de
ficheros. Cada comprobación que se realiza niega a su adversario una oportunidad y
proporciona un grado añadido de aseguramiento.
No hay que permitir que la seguridad de su software dependa del gran intelecto, la
perspicacia profunda, o la buena voluntad de las personas que lo configuran, desplieguan
y mantienen. Realizar la validación de entrada no solo sobre la entrada de
usuario, sino también sobre datos de cualquier fuente externa al código del
sistema que se está desarrollado. Esta lista debería incluir, pero sin limitarse a ella,
lo siguiente:
Parámetros de línea de comando.
Archivos de configuración.
Datos recuperados de una base de datos.
Subida de archivos.
Variables de entorno.
Importaciones de archivos planos.
Servicios de red.
Valores de registro.
Propiedades de sistema.
Archivos temporales.
Las variables de entorno.
Localizadores de recursos universales (direcciones URL) e identificadores (URI).
Referencias de otros nombre de archivo.
Encabezados Hyper Text Transfer Protocol (HTTP).
Parámetros HTTP GET.
Campos de formulario (especialmente los ocultos).
Las listas de selección, listas desplegables.
Cookies.
Comunicaciones Java applets.
Con frecuencia los desarrolladores realizan suposiciones del tipo: «No espero que nadie
me ataque en aquellos puntos. ¿Por qué debería gastar tiempo y esfuerzo para hacerlos
seguros?» Esta actitud conduce exactamente a los puntos ciegos que hacen que
el trabajo de un atacante sea fácil. Un programador miope podría pensar, «Si alguien
puede cambiar una propiedad del sistema, ellos ya habrán ganado». Entonces un
La opción -- delimiter existe para que un usuario pueda especificar el separador que
debería aparecer entre sentencias SQL. Valores típicos podrían ser un punto y coma o un
retorno de carro.
Pero el programa no coloca ninguna restricción contra el valor del argumento, por tanto
a través de un parámetro de línea de comando, se puede escribir cualquier cadena que se
quiera en la sentencia SQL generada, incluyendo órdenes de SQL adicionales.
Por ejemplo, si una simple SELECT fue generada con -- delimiter ';', esto generaría un
script para ejecutar la sentencia siguiente:
status = request.getParameter("status");
if (status != null && status.length() > 0) {
session.setAttribute("USER_STATUS", status);
}
Figura 4. Ejemplo de violación de los límites de confianza. [1]
Cuando se escribe el código, hay que hacer fácil indicar en qué lado del límite de
confianza de cualquier conjunto dado de datos se está. Cuando se realiza una revisión
de código, no esperar hasta encontrar una situación, en la cual, los datos que no han sido
validados puedan utilizarse. Si las fronteras de confianza no están claramente
delimitadas, los errores de validación son inevitables. En vez de gastar el tiempo
Al final se pone:
Lista negra (Blacklinting): intento de enumerar todas las entradas posibles
inaceptables.
#include <string.h>
#include <unistd.h>
char *validGames[] = { "moria", "fortune", "adventure", "zork",
"rogue", "worm", "trek", NULL };
#define MAXSIZE 40
void runGame(char *str) {
char buf[MAXSIZE];
int x;
for(x = 0; validGames[x] != NULL; x++) {
if (strcmp(str, validGames[x]) == 0) {
break;
}
}
if (validGames[x] == NULL) {
return;
}
snprintf(buf, sizeof buf, "/usr/games/%s", validGames[x]);
buf[MAXSIZE-1] = 0;
/* user input affects the exec command only indirectly */
execl(buf, validGames[x], 0);
}
int main(int argc, char **argv)
{
char *userstr;
if(argc > 1) {
userstr = argv[1];
runGame(userstr);
}
return 0;
}
Figura 6. Uso de indirección a los datos de entrada. Extraída de [1].
El programa podría haber aceptado la petición del usuario en forma de una ruta al
ejecutable, pero requeriría la comprobación de la entrada de usuario para asegurarse que
la ruta indicada es aceptable. Se tendría que comenzar a pensar en permisos de sistema
de ficheros, enlaces simbólicos y en la posibilidad de ataques de escalada de privilegios.
En muchas situaciones, la indirección no es factible porque el conjunto de
valores legítimos es demasiado grande o demasiado difícil de definir
explícitamente. Si se tiene que aceptar un número de teléfono como entrada, mantener
una lista de todos los números de teléfono legítimos no es una opción realista. La mejor
solución en tales casos es de crear una whitelist de valores de entrada aceptables.
#include <stdio.h>
#include <string.h>
#include <pcre.h>
int main(int argc, char* argv[]) {
pcre* re;
const char* err;
int errOffset;
char* regex = "^[0-9\\-\\. ]+$";
int i;
re = pcre_compile(regex, 0, &err, &errOffset, NULL);
if (re == NULL) {
printf("PCRE compilation failed\n");
return 1;
}
for (i = 1; i < argc; i++) {
char* str = argv[i];
2 http://www.pcre.org/
No confundir la validación que una aplicación realiza por objetivos de utilidad con la
validación de entrada por seguridad.
void changePassword() {
char* pass1 = getPasswordFromUser("enter new password: ");
char* pass2 = getPasswordFromUser("re-enter new password: ");
if (strcmp(pass1, pass2)) {
printf("passwords do not match\n");
} else {
setPassword(pass1);
}
bzero(pass1, strlen(pass1)); /* don't leave in memory */
bzero(pass2, strlen(pass2)); /* don't leave in memory */
free(pass1);
free(pass2);
}
char* getPasswordFromUser(char* prompt) {
char* tmp = getpass(prompt);
int len = strlen(tmp);
char* ret = (char*) malloc(len+1);
strncpy(ret, tmp, len+1);
bzero(tmp, len); /* don't leave passwd copy in memory */
return ret;
}
Figura 8. La comprobación de la contraseña 2 veces no impide a un atacante introducir datos
maliciosos. [1].
Se podrían parchear ciertas clases de fracaso de validación de entrada. Una omisión del
campo requerido podría ser restaurada poniéndolo a un valor por defecto, un campo de
contraseña que excede una longitud máxima podría ser truncado, o código de JavaScript
en un campo de entrada podría ser substituido por caracteres de escape.
No convertir una clase de entrada maliciosa en otra para el atacante. Desechar la entrada
que falla en la validación rotundamente.
Esto quiere decir que, siempre, los programadores tienen que añadir código para
recuperar alguna nueva clase de entrada y afrontar la validación de la gama de trampas
de entrada. En vez de codificar una nueva solución al problema de validación de entrada
cada vez, diseñar el programa de modo que haya un lugar claro, constante, y
obvio para la validación de entrada. La aplicación debería hacer pasar toda la
entrada por esta lógica de validación y ésta debería rechazar cualquier entrada que no
pueda ser validada
Esto requiere la creación de una capa de abstracción sobre las bibliotecas del sistema en
la que el programa suele ser introducido. Hacer una buena validación de entrada por
defecto creando una capa de funciones o métodos, se denomina seguridad API. La figura
siguiente, muestra cómo se interpone el API de seguridad entre el programa y las
librerías de sistema.
Lógica de programa
API seguridad
Figura 9. APIs de seguridad implican un amplio contexto para hacer la validación de entrada.
Extraída de [1]
de pruebas y verificacines que hay que realizar. Con esto conseguinos el realizar una
tarea difícil aún más difícil.
Las APIs para mejora de la seguridad también pueden hacer más fácil mantener las
fronteras de confianza haciendo obvia la validación cuando los datos cruzan el límite.
Considerar el límite de confianza hablado anteriormente, el contenido del objeto de
HttpSession por lo general es considerado confiable porque no puede ser cambiado nada
más que por la aplicación misma. Como HttpSession a menudo lleva la información que
entra en el programa vía una petición no validada en http, el método
HttpSession.setAttribute() (método que permite que los datos sean almacenados en un
HttpSession) forma un límite natural de confianza en una aplicación Web Java.
Cuanto más contexto de programa pueda ser tenido en cuenta durante la validación de
la entrada, mejor. Si el programa tiene que validar un campo de entrada, cuanto más
sepa la lógica de validación de los valores legales para ese campo, más rigurosa
puede ser la misma.
Por ejemplo, si hay un campo de entrada para contener la parte de abreviatura estatal
de una dirección postal, la lógica de validación puede usar selección indirecta para
comprobar el valor de la misma contra una lista de abreviaturas postales válidas para
estados. Un esquema de validación de entrada más sofisticado podría la comprobar la
parte de prefijo local de un campo de número de teléfono contra la abreviatura estatal.
En C y C++, como ya se verá más adelante hay varias funciones que pueden hacer
fácilmente que ocurran desbordamientos de buffer. Java hace la comprobación de
límites y comprueba la seguridad de tipos, de esta forma el desbordamiento de
buffer no es problema en Java de la manera que lo es en C y C ++.
programa debe incluir comprobaciones internas para asegurarse de que los valores no
causan un desbordamiento.
Java ofrece varios tamaños diferentes de valores enteros: char (8 bits), short (16 bits), int
(32 bits), y long (64 bits). Sin embargo, no ofrece tipos de entero sin signo, por lo que no
hay ningún modo de evitar comprobar la cota inferior como la superior. Si se quiere
manejar números no negativos en Java se debe comprobar que los valores que se reciben
son mayores o iguales que cero explícitamente. Por defecto el compilador de java
advertirá si un valor de un tipo más grande es asignado a una variable de un tipo más
pequeño.
En C y C++ leer campos de entrada usando enteros sin signo, obvia la necesidad de
comprobar el límite inferior, pero hay que tener cuidado porque hay que ser conscientes
de que las operaciones que mezclan enteros con y sin signo pueden acarrear resultados
negativos y convertir resultados con signo en otro sin signo podría producir un resultado
inesperado demasiado grande.
Ejemplo: malloc() acepta un entero y realiza una conversión implícita a entero si signo,
si doAlloc() recibe un número negativo como argumento puede resultar en un
intento de reservar una gran cantidad de memoria.
Ataque Implicaciones
SQL Injection Accede/modifica datos en la base de datos
XPath Injection Accede/modifica datos en formato XML
SSI Injection Ejecuta comandos sobre el servidor y accede a datos
sensibles
LDAP Injection Bypass de autenticación
MX Injection Usa el servidor de correos como una máquina de spam
HTTP Injection Modifica o intoxica el caché de las web
Tabla 1 Defectos de inyección.
Inyección de SQL
La realización de sentencias SQL donde se combinan palabras clave con datos tiene el
problema de que alguien malintencionadamente pueda alterar las estructuras de
control y por tanto el significado de la sentencia, cuando la intención era solamente
suministrar datos a la sentencia sin alterar las estructuras de control. Un atacante
explota este tipo de vulnerabilidades especificando meta-caracteres que tienen un
significado especial como:
o Simples comillas (‘).
o Dos puntos (..), peligroso en sistema de ficheros.
Para comandos de Shell:
o Punto y coma (;).
o (&&).
o Carácter de nueva línea (/n) para los ficheros de log.
En el ejemplo de la figura siguiente, se muestra este tipo de vulnerabilidad en una
sentencia SQL que se forma concatenando strings de tal forma que se deja abierta a
un ataque conocido como inyección de SQL:
Pero un atacante puede asignar al campo itemname la cadena "name' OR 'a'='a" con
lo que la sentencia se convierte en:
SELECT * FROM items WHERE owner = 'wiley' AND itemname = 'name'
OR 'a'='a';
Manipulación de rutas
Este tipo de error tiene lugar cuando se permite en las entradas de un usuario incluir
meta-caracteres de sistemas de ficheros como:
o slash (/).
o backslash (\).
o punto (.).
donde se espera una ruta relativa un atacante puede convertirla en una ruta absoluta
o recorrer el sistema de ficheros a una posición no planeada subiendo en el árbol de
directorio. Se denomina a un acceso al sistema de ficheros no autorizado
de este tipo manipulación de ruta. El código del ejemplo de la figura siguiente,
muestra la entrada a la aplicación desde una petición HTTP para crear un nombre de
archivo. El programador no ha considerado la posibilidad de que un atacante podría
proporcionar un nombre del archivo como ../../tomcat/conf/server.xml, que hace que
la aplicación suprima uno de sus propios archivos de configuración.
Inyección de comandos
Si se permite al usuario, especificar comandos de sistema que su programa
ejecuta, los atacantes podrían ser capaces de hacer que el sistema ejecute
comandos maliciosos en su nombre. Si en la entrada se pueden incluir rutas del
sistema de ficheros o shell metacharacters, un atacante podría especificar una ruta
absoluta donde se espera una ruta relativa o añadir un segundo comando malévolo
después del comando que el programa tiene la intención de ejecutar. Se denomina
inyección de comandos a la ejecución de comandos no autorizada.
Falsificación de logs
Los logs son un objetivo para atacantes pues son un recurso valioso para
administradores de sistema y desarrolladores. Si los atacantes pueden falsificar el
valor que es escrito en los log, podrían ser capaces de fabricar eventos en el sistema
mediante la inclusión de entradas corrompidas. Si se permite a un atacante inyectar
entradas en los logs debido a una carencia de validación, la interpretación de los
archivos de logs podría ser dificultosa o mal dirigida, disminuyendo su valor.
En el caso más benigno un atacante podría insertar entradas falsas de log incluyendo
caracteres especiales. Si el archivo de log se procesa automáticamente, el atacante
puede dejar el archivo inutilizable corrompiendo su formato. Un ataque más sutil
podría implicar el sesgar la estadística del archivo de log.
Es prudente codificar cualquier carácter que no sea un carácter de ASCII. Una forma
de prevenir falsificación de log es codificar los datos antes de registrarlos en el log.
Resumen
Una validación correcta de la entrada requiere todo lo siguiente:
o Seguir la pista de qué valores han sido validados y qué propiedades aquella
validación comprobó. Si el programa tiene fronteras de confianza bien
establecidas, esto es fácil.
o No dejar pasar por alto el modo en que los diferentes componentes interpretan los
datos que pasan por el programa. No permitir que los atacantes secuestren las
peticiones al sistema de ficheros, la base de datos, o el navegador de Web, entre
otros. Esto por lo general implica cuidadosamente pensar en codificaciones de
datos y en meta-caracteres.
Introducción
El MITRE define este error como [14]:«Un programa intenta poner más datos en un
búfer de lo que realmente puede almacenar, o cuando un programa trata de poner los
datos en un área de memoria fuera de los límites de un búfer… la causa más común de
desbordamientos de búfer, es el típico caso en el que el programa copia el búfer sin
restringir el número de bytes a copiar.»
Desde que Morris Worm hizo público el primer uso de un ataque de desbordamiento
buffer contra el servicio fingerd que posibilitó su extensión a través de la Internet en
1988, el desbordamiento de buffer ha llegado a ser la vulnerabilidad de
seguridad de software más conocida.
Los lenguajes seguros deben proporcionar dos propiedades para asegurar que los
programas respetan límites de asignación:
Seguridad de tipo. En los lenguajes que cumplen esa propiedad, se asigna un tipo
a los objetos, como por ejemplo un tipo int, puntero a int, puntero a función, etc. Esta
propiedad asegura que las operaciones en el objeto son siempre compatibles con su
tipo. Sin esta característica cualquier valor arbitrario podría usarse como una
referencia en la memoria. C y C++ son lenguajes inseguros y ampliamente utilizados
y por tanto el programador es el responsable de prevenir que las operaciones que
manipulan la memoria puedan resultar en desbordamientos de buffer.
Un ejemplo de este tipo de error que se pude comenter en este lenguaje, es cuando se
declara un puntero a un número entero (int) y luego se utiliza como un puntero a una
función, tal y como se muestra en eejemplo siguiente:
int (*cmp)(char*,char*);
int *p = (int*)malloc(sizeof(int));
*p = 1;
cmp = (int (*)(char*,char*))p;
cmp(“hola”,”adios”); // El programa realiza realiza un crash
Figura 26. Error de seguridad de tipos
No obstante hay que tener en cuenta una serie de consideraciones sobre potenciales
vulnerabilidades de Java:
Seguridad de tipos. Los campos que son declarados privados o protegidos
o que tienen protección por defecto no deberían ser públicamente
accesibles. Sin embargo, hay un número de vulnerabilidades construidas para Java
que hacen que esta protección pueda ser violada. Esto no debería ser ninguna sorpresa
para expertos en Java, porque están bien documentados, pero pueden aprovecharse
del imprudente. Podría ser accesible el campo privado theCrownJewels como también
el público fredJunk. Más generalmente, «un ataque de confusión de tipo» podría
comprometer la seguridad de Java exponiendo los componentes del security
manager de la JVM.
Serialización. Esta facilidad posibilita que el estado del programa sea capturado y
convertido en una byte stream que puede ser restaurado por la operación inversa que
es la deserialización. También permite que un objeto sea transmitido por la red. La
serialización captura todos los campos de una clase incluidos los privados y protegidos
que pueden ser expuestos si se transmiten por la red. En este caso es necesario el uso
de criptografía (SSL) para preservar la confidencialidad de los datos. El
security manager no previene que un objeto con campos privados pueda ser
serializado.
JVM. La JVM en sí misma está escrita en C para una plataforma dada. Esto quiere
decir que la JVM en sí misma puede ser susceptible a problemas de
desbordamiento. La versión de la JVM de SUNS Microsystems está bastante bien
inspeccionada. Java Native Inteface (JNI), permite crear código nativo y crear,
acceder y modificar objetos de Java. Si el programa en Java usa JNI para llamar a
código escrito en un lenguaje no seguro como C, la garantía de seguridad de memoria
proporcionada por Java se pierde y vulnerabilidades de buffer overflow pueden llegar
a ser posibles. Se puede comprometer la integridad de la JVM porque el código nativo
se ejecuta en el mismo espacio de direcciones que la JVM.
class Echo {
public native void runEcho();
static {
System.loadLibrary("echo");
}
public static void main(String[] args) {
new Echo().runEcho();
}
}
Figura 28. Llamada a un método nativo JNI. Extraída [1]
#include <jni.h>
#include "Echo.h" // Echo class compiled with javah
#include <stdio.h>
JNIEXPORT void JNICALL
Java_Echo_runEcho(JNIEnv *env, jobject obj)
{
char buf[64];
gets(buf);
printf(buf);
}
Figura 29. Vulnerabilidad de buffer overflow en un método nativo. Extraída de [1]
En un ataque clásico por desbordamiento de buffer, el atacante envía los datos que
contienen un segmento de código malévolo a un programa que es vulnerable a este error,
basado en el stack. Además del código malévolo, el atacante incluye la dirección de
memoria del principio del código.
Esta shellcode, que ejecuta la shell /bin/sh, realiza una llamada al sistema execve para
realizar la ejecución de la shell contenida dentro del array scode.
Cuando el desbordamiento de buffer ocurre, el programa escribe los datos del atacante
en el buffer y sigue más allá de los límites del buffer hasta que superponga la
dirección de vuelta de la función con la dirección del principio del código
malicioso. Cuando la función devuelve el control, salta al valor almacenado en su
dirección de retorno.
El código del ejemplo de la figura siguiente, define el problema de función trouble(), que
asigna un buffer char y un int en el stack (pila)3 y lee una línea de texto de stdin con gets()
en el buffer. Como gets() sigue leyendo la entrada hasta que se encuentra un
carácter de final-de-línea, un atacante puede desbordar el buffer line con
datos maliciosos. En el ejemplo, esta función declara dos variables locales y usa gets()
para leer una línea de texto en el buffer line del stack de 128 octetos.
void trouble() {
int a = 32; /*integer*/
char line[128]; /*character array*/
gets(line); /*read a line from stdin*/
}
Figura 31. Empleo incorrecto de gets() que puede incurrir en desbordamiento de buffer.
Extraída de [1]
Este ejemplo se representa de forma gráfica en la siguiente figura, muestra lo que ocurre
en un clásico ataque de buffer overflow.
El primer marco de stack representa el contenido de la memoria después de llamar a
trouble() pero antes de que sea ejecutada. La variable local line es asignada sobre el
stack que comienza en la dirección 0xNN. La variable local a está justo encima de line
3 La pila es un lugar en la memoria de un equipo en el que todas las variables que se declaran e
inicializan antes de pasarlas a tiempo de ejecución se almacenan.
Dirección de
Línea a Retorno
32 0X<RETORNO>
EJECUCION NORMAL
Dirección de
Línea a Retorno
Dirección de
Línea a Retorno
Uno de los errores de concepto que se tienen sobre las vulnerabilidades de buffer
overflow, es que son solo explotables cuando el buffer está en el stack. Los ataques
basados en el heap4 pueden sobrescribir datos importantes almacenados en el mismo y
4Heap, es la sección de la memoria de un equipo donde todas las variables creadas o inicializadas
en tiempo de ejecución se almacenan.
Figura 33. Heap overflow. Extraída de presentación de Microsoft Basics of Secure Design,
Development, and Test
#include <stdio.h>
#include <unistd.h>
#define BUFSIZER1 512
#define BUFSIZER2 ((BUFSIZER1/2) - 8)
int main(int argc, char **argv) {
char *buf1R1;
char *buf2R1;
char *buf2R2;
char *buf3R2;
buf1R1 = (char *) malloc(BUFSIZER1);
buf2R1 = (char *) malloc(BUFSIZER1);
free(buf2R1); /* libero el puntero*/
buf2R2 = (char *) malloc(BUFSIZER2);
buf3R2 = (char *) malloc(BUFSIZER2);
strncpy(buf2R1, argv[1], BUFSIZER1-1); /* lo vuelvo a utilizar*/
free(buf1R1);
free(buf2R2);
free(buf3R2);
}
Figura 37. Ejemplo dereference free. Extraída de [14]
Liberar la memoria más de una vez (double free). Cuando un programa libera
un trozo de memoria más de una vez free(), las estructuras de datos para gestión de la
memoria pueden llegar a corromperse, lo que puede ocasionar que el programa falle
o causar que se llame dos veces a malloc(). Esto último puede dar el control a un
5Acción de acceder a la variable a la que apunta el puntero en cuestión. Ej: int * ptr= &b; *ptr=
15;
atacante de los datos que se escriben en esa memoria doblemente reservada. Entonces
se tiene un programa potencialmente expuesto a ataque de buffer overflow.
#include <stdlib.h>
int main(int argc, char *argv[]){
int k=0;
int *p=(int *)NULL;
switch(k) {
case 0:
if (*p) k = *p;
default:
break;
}
return 0;
}
Figura 39. Ejemplo null dereference
typedef struct{
char* ptr;
int bufsize;
} buffer;
int main(int argc, char **argv) {
buffer str;
int len;
if ((str.ptr = (char *)malloc(BUFSIZE)) == NULL) {
return FAILURE_MEMORY;
}
str.bufsize = BUFSIZE;
len = snprintf(str.ptr, str.bufsize, "%s(%d)", argv[0], argc);
if (len >= BUFSIZE) {
free(str.ptr);
if (len >= MAX_ALLOC) {
return FAILURE_TOOBIG;
}
if ((str.ptr = (char *)malloc(len + 1)) == NULL) {
return FAILURE_MEMORY;
}
str.bufsize = len + 1;
snprintf(str.ptr, str.bufsize, "%s(%d)", argv[0], argc);
}
printf("%s\n", str.ptr);
free(str.ptr);
str.ptr = NULL;
str.bufsize = 0;
return SUCCESS;
}
Figura 40. Ejemplo de código para controlar el tamaño de un buffer. Extraída de [1]
Manipulación de Strings
Introducción
Errores de truncado
o scanf(). Función que lee de la entrada estándar los caracteres que corresponden
al formato que se le especifica. Por ejemplo si se especifica %s, se leerán los
caracteres de la entrada en el buffer hasta que llegue un carácter no-ascii, pudiendo
ocasionar un desbordamiento del buffer. También se puede especificar % 255s
limitando a ese número de caracteres los que se pueden leer de la entrada y usarse
la función de un modo más seguro. Funciones de similar comportamiento son
fscanf(), wscanf().
s = strrchr(filename,'/');
if(s) {
strcpy(fn,s+1);
Figura 44. Código del programa php.cgi en PHP / FI 2.0beta10 vulnerable a un desbordamiento de
búfer remoto causada por una llamada insegura para strcpy (). Extraída de [1]
o sprintf(). Para usarla de forma segura hay que comprobar que el buffer de destino
pueda acomodarse a la combinación de todos los argumentos de la fuente de
entrada, controlando los tamaños fuente-destino y las conversiones de
formato que se especifican. Si la longitud del argumento fuente es mayor que
la del destino se producirá un buffer overflow. Funciones de comportamiento
similar son fprintf(), printf(), swprintf().
char speed[128];
...
sprintf(speed, "%s/%d", (cp = getenv("TERM")) ? cp : "",
(def_rspeed > 0) ? def_rspeed : 9600);
Figura 45. Código de la Versión 1.0 del daemon de Telnet Kerberos 5 que contiene un
desbordamiento de búfer debido a que la longitud de la variable de entorno TERM, que nunca se valida.
Extraída de [1]
La figura siguiente, muestra la equivalencia entre las funciones del primitivo estándar
de C y las nuevas funciones para controlar el tamaño del buffer para plataformas
Windows con Microsoft visual studio. La librería, Microsoft safe CRT library,
suministra funciones de manipulación de strings con límites con «_s» como sufijo.
Para determinar la seguridad de la llamada, una herramienta debe buscar los caminos
en que el string almacenado en src podría ser más largo que la cantidad de espacio
asignado para dest. Esto quiere decir que hay que analizar donde dest es asignado y
de dónde viene el valor de src (así como la presencia o no de terminadores nulos). Lo
anterior podría requerir la exploración de muchos caminos dispares en el programa.
Ahora analizar strncpy():
strncpy (dest, src, dest_size);
Para asegurarse que ningún desbordamiento de buffer ocurre con esta función, una
herramienta tiene que asegurarse solo que dest_size no es más grande que la cantidad
de espacio asignado para dest. El tamaño y el contenido de src no importan. En la
mayoría de los casos, es mucho más fácil comprobar que dest y dest_size están de
acuerdo el uno con el otro.
Función no Función limitada equivalente Función limitada
limitada librería C librería C estándar equivalente Windows Safe
estándar CRT
char * gets(char *dst) char * fgets(char *dst, int bound, FILE char * gets_s(char *s, size_t
*FP) bound)
Int scanf(const char Ninguna errno_t scanf_s(const char *FMT
*FMT [, arg, ...]) [, ARG, size_t bound,...])
Int sprintf(char *str, Int snprintf(char *str, size_t bound, errno_t sprintf_s(char *dst,
const char *FMT [, arg, const char *FMT, [, arg, ...]) size_t bound, const char *FMT [,
...]) arg, ...]) w
char * strcat(char *str, char * strncat(char *dst, const char errno_t strcat_s(char *dst,size_t
const char *SRC) *SRC, size_t bound) bound, const char *SRC)
char * strcpy(char *dst, char * strncpy(char *dst, const char errno_t strcpy_s(char *dst, size_t
const char *SRC) *SRC, size_t bound) bound, const char *SRC)
Tabla 2 Funciones ilimitadas con sus correspondientes limitadas. [1].
if (auth_sys == KRB5_RECVAUTH_V4) {
strcat(cmdbuf, "/v4rcp");
} else {
strcat(cmdbuf, "/rcp");
}
if (stat((char *)cmdbuf + offst, &s) >= 0)
strcat(cmdbuf, cp);
else
strcpy(cmdbuf, copy);
Figura 46. Función ilimitada, vulnerabilidad en Kerberos 5. Extraída de [1]
cmdbuf[sizeof(cmdbuf) - 1] = '\0';
if (auth_sys == KRB5_RECVAUTH_V4) {
strncat(cmdbuf, "/v4rcp", sizeof(cmdbuf) - 1 - strlen(cmdbuf));
} else {
strncat(cmdbuf, "/rcp", sizeof(cmdbuf) - 1 - strlen(cmdbuf));
}
if (stat((char *)cmdbuf + offst, &s) >= 0)
strncat(cmdbuf, cp, sizeof(cmdbuf) - 1 - strlen(cmdbuf));
else
strncpy(cmdbuf, copy, sizeof(cmdbuf) - 1 - strlen(cmdbuf));
Figura 47. Función limitada, vulnerabilidad corregida en Kerberos 5. Extraída de [1]
Las funciones de strings limitadas son más seguras que las funciones ilimitadas, pero
hay todavía mucho margen para el error. Los fallos de programación más comunes
que se pueden cometer con las funciones de string limitadas:
o El buffer de destino se desborda porque el límite depende del tamaño de los datos
de la fuente, más bien que del tamaño del buffer de destino.
o El buffer de destino se deja sin un terminador nulo.
Para evitar errores comunes con strncpy(), se pueden seguir dos directrices
simples:
o Usar límites seguros. Llamadas seguras a strncpy(), limitadas con un valor
obtenido del tamaño buffer de destino.
o Terminar con carácter nulo manualmente el buffer de destino
inmediatamente después de la llamada strncpy().
Para evitar casos de mal uso comunes y errores que usan strncat(), se pueden seguir
dos directrices:
o Usar límites seguros. Calcular el límite pasado a strncat() restando la longitud
actual del string de destino, reportado por strlen(), del tamaño total del buffer.
o Terminar manualmente con carácter nulo. Asegurarse que los buffers de la
fuente y el destino pasados a strncat(), contienen terminadores nulos.
Errores de truncado
Este error por un byte denominado «off by one» podría ser inconsecuente,
dependiendo de lo que se almacena en la memoria justo más allá del buffer, pues
permanecerá con eficacia terminado por el carácter uno hasta que otra posición de
memoria sea superpuesta. Es decir, strlen(buf) devolverá solo el tamaño real del
buffer más uno, PATH_MAX + 1, en este caso.
Sin embargo, cuando buf posteriormente sea copiado en otro buffer con el valor de
vuelta de readlink() → len como el límite pasado a strncpy(), los datos en buf están
truncados y el buffer de destino se deja sin terminar con el carácter nulo. Este «error
de por uno» probablemente causará un serio desbordamiento de buffer.
char path[PATH_MAX];
char buf[PATH_MAX];
if(S_ISLNK(st.st_mode)) {
len = readlink(link, buf, sizeof(path));
buf[len] = '\0';
}
strncpy(path, buf, len);
Figura 48. Llamada a strncpy() que podría causar error de truncado. Extraída de [1]
Sin embargo, esto puede causar una pobre utilidad si el sistema con frecuencia recibe
una entrada que no puede acomodar, o bien, si el programa trunca los datos y se sigue
ejecutando normalmente, pueden surgir una variedad de errores. Estos errores
típicamente se caen en dos campos: El string podría no tener el mismo significado
después de ser truncado o podría no estar terminado con carácter nulo. En la figura
siguiente, se muestra un resumen de diversas librerías seguras de manejo de cadenas.
Api Prohibidas Strsafe Safe C/C++
strcpy, wcscpy, _tcscpy, StringCchCopy, StringCbCopy, strcpy_s
_mbscpy, lstrcpy, lstrcpyA, StringCchCopyEx, StringCbCopyEx
lstrcpyW, strcpyA, strcpyW
StringCchCat, StringCbCat, strcat_s
strcat, wcscat
StringCchCatEx, StringCbCatEx
wnsprintf, wnsprintfA, StringCchPrintf, StringCbPrintf, sprintf_s
wnsprintfW StringCchPrintfEx, StirngCbPrintfEx
StringCchPrintf, StringCbPrintf, _snprintf_s
_snwprintf, _snprintf
StringCchPrintfEx, StirngCbPrintfEx _snwprintf_s
wvsprintf, wvsprintfA, StringCchVPrintf, StringCbVPrintf, _vstprintf_s
wvsprintfW, vsprintf StringCchVPrintfEx, StirngCbVPrintfEx
StringCchVPrintf, StringCbVPrintf, vsntprintf_s
_vsnprintf, _vsnwprintf
StringCchVPrintfEx, StirngCbVPrintfEx
StringCchCopyN, StringCbCopyN, strncpy_s
strncpy, wcsncpy
StringCchCopyNEx, StringCbCopyNEx
StringCchCatN, StringCbCatN, strncat_s
strncat, wcsncat
StringCchCatNEx, StringCbCatNEx
scanf, wcsanf Ninguna sscanf_s
strlen, wcslen, _mbslen, StringCchLength, StringCbLength strlen_s
_mbstrlen
Tabla 3 Librerías seguras equivalentes para manejo de strings. Extraídad de [1]
Conclusión:
Hay que evitar truncar los datos. Si la entrada proporcionada es demasiado grande
para una operación dada, hay que intentar manejar el problema redimensionando
Como una opción en el peor de los casos, truncar los datos e informar al
usuario que el truncado ha ocurrido. Las funciones de string de Microsoft
Strsafe y las librerías seguras CRT (figura 103) facilitan la identificación
y reporte de errores. Ambos conjuntos de funciones ponen en práctica las
comprobaciones en tiempo de ejecución que hacen que las funciones fallen y se
invoquen los correspondientes manejadores de error cuando ocurren errores de
truncado y otros.
Nunca hay asumir que los datos del mundo exterior están correctamente terminados
con carácter nulo. Si el string no se termina correctamente, la adición de un octeto
nulo en el último octeto del buffer impedirá a otras operaciones como
calcular mal la longitud del string o desbordar el buffer.
L A C A D E N A M A S L A R \0 Terminación
manual
Una función de formato es un tipo especial de función ANSI C, que toma un número
variable de argumentos, de los cuales uno es la llamada cadena de formato. Los
Format String son simples cadenas, caracterizadas por el formato que se les aplica.
Los tipos de formato a aplicar son los siguientes:
Vulnerable.c:
#include <stdio.h>
int main(void) {
char texto[30];
scanf(“%29s”, texto);
printf(texto);
return 0;
}
Figura 50. Ejemplo que contiene una vulnerabilidad de formato de string clásica. Extraída de [14].
Otro ejemplo real que contiene una vulnerabilidad de formato de string clásica del
demonio de FTP popular wuftpd (que ha sufrido de numerosas vulnerabilidades).
Formas de prevención:
o Pasar siempre un argumento estático a cualquier función que acepte un argumento
de formato de string.
El código de la figura siguiente, muestra la corrección del código de wuftpd 2.6.0 con
vulnerabilidad de formato de string de la figura anterior.
A causa del modo en que los frames del stack se construyen, el primer desafío
fácilmente puede conseguirse normalmente colocando la dirección objetivo para la
directiva % n en el formato de string que se procesa. Como los caracteres
probablemente serán copiados inalterados en el stack, hay que tener cuidado con los
datos específicos de conversión de números que pueden hacer que la función de
buffer[sizeof buf - 1] = 0;
printf("buffer (%d): %s\n", strlen(buffer), buffer);
printf("x is %d/%#x (@ %p)\n", x, x, &x);
return 0
Figura 53. Error de format string
La combinación de anchura de campo y la característica de que la directiva %n
escribirá el número de los caracteres que habrían sido procesados,
independientemente de cualquier truncamiento que podría haber ocurrido, da al
atacante la capacidad de construir valores arbitrariamente grandes. Una versión aún
más sofisticada de este ataque usa cuatro %n directivas para controlar el valor de un
puntero.
Linux Windows
Bstrlib: Vstr: http://www.and.org/vstr/
http://bstring.sourceforge.net/
FireString: http://firestuff.org/ Safe CRT:
http://msdn.microsoft.com/msdnmag/
issues/05/05/safecandc/default.aspx
GLib: http://developer.gnome.org StrSafe:
http://msdn2.microsoft.com/en-
us/library/ms995353.aspx
Libmib:
http://www.mibsoftware.com/libmib/
astring/
SafeStr: http://www.zork.org/safestr/
Tabla 5 Librerías alternativas de manejo de strings para C para Windows y Linux. [1]
Integer overflow
Todos los tipos de representación de números enteros tienen una limitada capacidad
debido a que representan con un limitado número de bits. Esto es un hecho que
normalmente se les olvida a los programadores y pueden ocasionar errores de tipo
integer overflow al intentar almacenar un valor demasiado grande en la
variable asociada que sobrepase esos límites inferior y superior o un
valor grande positivo se convierta en un valor grande negativo y viceversa,
generando un resultado inesperado (valores negativos, valores inferiores, etc.).
Este tipo de error puede tener consecuencias graves cuando el valor que genera
el integer overflow es resultado de alguna entrada de usuario (es decir, que
puede ser controlado por el mismo) y cuando, de este valor, se toman decisiones
de seguridad, se toma como base para hacer asignaciones de memoria, índice
de un array, concatenar datos, hacer bucles. etc. Este ha sido el caso de
vulnerabilidades como CVE-2001-014459 en SSH1 y que permitiría a un
atacante ejecutar código arbitrario con los privilegios del servicio ssh; o algunos
más recientes como CVE-2011-2371 en Mozilla Firefox.
Un integer overflow puede causar varios tipos de problemas pero en C, C++ puede
conducir a un ataque de buffer overflow que podría ocurrir cuando una variable
wrapped-around se usa para reservar memoria, se limita una operación sobre strings
o como índice de un buffer. Integers overflows también pueden ocurrir en Java, pero
debido a que Java fuerza la comprobación de seguridad de la memoria son más
difíciles de explotar.
Sin embargo dependiendo del valor de readamt, decrementar su valor podría causar
resultados erróneos, si se declara sin signo y un atacante ocasiona que getstringsize()
devuelva «0», malloc() será invocada con 4294267295 que es el valor más grande en
una representación de 32-bit y la operación fallará por insuficiencia de memoria.
return 0;
}
Figura 55. Error aritmético. Extraída presentación Microsoft «Basic of Secure Development Test»
La dos funciones mencpy intentan realizar una copia > de 255 bytes.
Errores de truncado
Suponiendo que se tienen cuatro bits para representar enteros con y sin signo se
tendría:
0 0 0 1 1 0 0 0 1 0 0 0
1 1 1 1 1 0 0 0
0 0 0 1 1 0 0 0
Extensión signo
Truncado
Figura 56. Error de truncado que ocurre cuando un entro de 8-bit es truncado a uno de 4 bits y una
de extensión de signo cuando un entero de 4 bit es truncado a un entero con signo de 8 bits. [1]
Esto significa que si un número negativo es convertido a un tipo de datos más grande,
su signo será el mismo, pero su valor aumentará considerablemente porque sus bits
más significativos se modificarán.
No pueden representar el mismo número de valores y solo algunos valores pueden ser
convertidos desde un dato de tipo sin signo a otro con signo y viceversa sin cambiar el
significado, mientras otros no. En el caso de valores positivos el problema es que el 50
% de valores sin signo requieren poner a uno el bit más significativo.
1 0 0 0 =-8 1 0 0 0 = 15
Con signo Sin signo
Figura 57. Error que ocurre cuando un entero de 4 bit es truncado de con signo a sin signo y
viceversa. [1]
6
The GNU Compiler Collection incluye front ends para C, C++, Objective-C, Fortran, Java, Ada, and Go,
así como librerias para esos lenguajes (libstdc++, libgcj,...). http://gcc.gnu.org/
Introducción
Los problemas de seguridad muchas veces comienzan cuando un atacante busca la forma
de violar las expectativas del programador, que muchas veces dan menos
importancia a las condiciones de error y manejo de excepciones. Por el contrario es uno
de los aspectos que tienen en cuenta los atacantes.
El lenguaje C usa el valor de retorno de una función para comunicar el éxito o el fracaso.
Esta aproximación tiene varios efectos secundarios no deseables:
Hace fácil no hacer caso de errores simplemente no haciendo caso del valor de retorno
de una función.
Conectar la información de error con el código para manejar el error hace los
programas más difíciles de leer. La lógica de manejo del error se mezcla con la lógica
para manejar casos esperados, que aumentan la tentación de ignorar errores.
No hay ninguna convención universal para comunicar la información de error, por
tanto los programadores deben investigar el mecanismo que maneja para cada
función.
Vale la pena notar que estos son algunos motivos para que los diseñadores de C++ y Java
incluyeran excepciones como un elemento del lenguaje. El efecto que tiene ignorar un
FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
FileInputStream fis = new FileInputStream(pFileName);
try {
fis.read(byteArray); // El fichero es siempre de 1k bytes
processPFile(userName, byteArray);
}
finally {
fis.close();
}
FileInputStream fis;
byte[] byteArray = new byte[1024];
for (Iterator i=users.iterator(); i.hasNext();) {
String userName = (String) i.next();
String pFileName = PFILE_ROOT + "/" + userName;
fis = new FileInputStream(pFileName);
try {
int bRead = 0;
while (bRead < 1024) {
int rd = fis.read(byteArray, bRead, 1024 - bRead);
if (rd == -1) {
throw new IOException("fichero es inusualmete pequeñ0");
}
bRead += rd;
}
}
finally {
fis.close();
}
// se podría añadir código para comprobar si el fichero es demasiado largo
processPFile(userName, byteArray) ;
}
Figura 62. Se comprueba el código de retorno del método read().[1]
Las excepciones solucionan muchos problemas de manejo de errores. Aunque sea fácil
ignorar el valor de retorno de una función simplemente omitiendo la comprobación del
código, no hacer caso de una excepción comprobada requiere justo lo
contrario, un programador tiene que escribir el código expresamente para ignorarlo.
Las excepciones también permiten la separación entre el código que sigue
un camino esperado y el código que maneja circunstancias anormales.
catch (RareException e) {
throw RuntimeException("Esto nunca debe ocurrir ", e);
}
Figura 63. Excepción con el bloque catch relleno.
Registro y depuración
Se recomienda:
Relacionar las entradas de log con marcas de tiempo (time-stamp).
Registrar cada acción importante.
Proteger los logs.
Diseñar ayudas de depuración de fallos y no tener puertas traseras de acceso en el
entorno de producción. Agregar con cuidado código de depuración de fallos dentro
del sistema de modo que se pueda asegurar que nunca aparece en un despliegue de
producción. El problema es que si este comportamiento ampliado abandona el
ambiente de depuración de fallos y va a producción, puede tener un efecto
potencialmente degradante sobre la seguridad del sistema. La depuración de fallos del
código no recibe el mismo nivel de revisión y prueba, como el resto del programa y
raras veces se escribe con idea de estabilidad, o de seguridad en mente.
El acceso al código por puertas traseras es un caso especial de eliminar fallos del
código. El código de acceso back-door está diseñado para permitir a desarrolladores e
ingenieros de prueba tener acceso a una aplicación de un modo que no se permite para
el usuario final. Normalmente es necesario probar los componentes de una aplicación
aislada o antes de que el sistema sea desplegado en su ambiente de producción. Sin
embargo, si el código back-door es enviado a producción, puede dar a atacantes
nuevos caminos de ataque a la aplicación que no fueron considerados durante el
diseño o las pruebas.
Seguir una adecuada política de backup que permita proteger los ficheros de backup
de ser accedidos.
Easter eggs, son elementos de aplicaciones ocultos que se añaden para diversión de
los programadores. Un Easter egg elaborado podría revelar un videojuego oculto
embebido en la aplicación. ¿Cómo determinar si una vulnerabilidad en un Easter egg
es intencionada por parte del programador o no? Lo mejor es no tolerarlos.
En este apartado se va a analizar cómo hay que manejar las contraseñas, tanto las
que utilizan los usuarios para autenticarse (intbound passwords) como las que utiliza un
programa que actúa como cliente para autenticarse frente a un servicio final (outbound
passwords), como una base de datos o un servidor LDAP. Intbound passwords pueden
ser hashed, sin embargo, outbound passwords deben ser accesibles al programa en texto
claro. Para profundizar sobre intbound passwords se recomienda la lectura de Security
Engineering [16].
Mantener las passwords fuera del código fuente. Cifrar las contraseñas con
algoritmo seguro y almacenarlas fuera del código. La figura siguiente, muestra un trozo
de código que muestra un mal uso de password codificada dentro de código Java.
import javax.crypto.*;
import java.security.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.IOException;
public class PasswordProtector {
final static String CIPHER_NAME = "AES";
final static Reader in = new BufferedReader(
new InputStreamReader(System.in));
/*
Esta utilidad tiene tres modos:
* 1) Generar una nueva clave:
* PasswordProtector nuevo
* Genera una nueva clave e imprime en stdout. Usted puede
* utilizar esta clave en operaciones posteriores.
* 2) Cifrar una contraseña:
* PasswordProtector cifrar <clave> <contraseña>
* Cifra la contraseña e imprime los resultados en stdout.
* 3) Descifrar una contraseña:
* PasswordProtector descifrar <clave> <contraseña cifrada>
}
private static String StringFromKey(Key k) {
return Base64.encodeObject(k, Base64.DONT_BREAK_LINES);
}
/* Cifrar la contraseña dada con la clave. Se codifica el texto cifrado en Base64 */
private static String encryptPassword(Key k, String passwd) throws Exception {
Cipher c = Cipher.getInstance(CIPHER_NAME);
c.init(Cipher.ENCRYPT_MODE, k);
byte[] bytes = c.doFinal(passwd.getBytes());
return Base64.encodeObject(bytes);
}
/*Descifrar una contraseña cifrada (asume que el texto cifrado es codificado en base64*/
private static String decryptPassword(Key k, String encrypted) throws Exception {
byte[] encryptedBytes;
encryptedBytes = (byte[]) Base64.decodeToObject(encrypted);
Cipher c = Cipher.getInstance(CIPHER_NAME);
c.init(Cipher.DECRYPT_MODE, k) ;
return new String(c.doFinal(encryptedBytes));
}
}
Figura 67. Utilidad de java para cifrar y descifrar passwords utilizando secret key
Usar passwords robustas. Usar generadores de números aleatorios seguros para las
passwords de conexiones outbounds. Hay un compromiso entre los conceptos de
robustez y de facilidad, a la hora de elegir un password. Las outbound passwords no
requieren ser recordadas deben ser largas y aleatoriamente generadas usando un método
seguro de generación de números aleatorios.
Los errores más comunes y más obvios relacionados con números aleatorios ocurren
cuando se usa PRNG estadístico en una situación que exige que los valores sean
sumamente imprevisibles y por tanto producidos por PRNG criptográfico. Otro error
ocurre cuando un PRNG no se crea con entropía insuficiente, causando que la corriente
de números sea predecible. En ambos casos, el PRNG producirá una corriente fiable
de valores que un atacante puede adivinar y potencialmente comprometer la seguridad
del programa. El resto de la sección se dirige a cómo evitar estos errores en java y en C.
Introducción
Escalada Privilegios
Alto Vertical
Privilegio
Atacante Usuario
Bajo UAC, los usuarios todavía tienen privilegios de administrador, pero los programas
se ejecutan con privilegios disminuidos por defecto. Los programas todavía pueden
ejecutarse con privilegios de administrador, pero el usuario explícitamente debe
permitirles hacerlo así. Esto fuerza a los desarrolladores de software a escribir
programas diseñados para correr con privilegios menos elevados, pero la transición
tardara muchos años.
son controlados por el usuario y el grupo IDs que heredan cuando se ejecutan o aquellos
que se cambian posteriormente. Los programas deben manejar su IDs activos para
controlar los privilegios que ellos tienen en cualquier tiempo dado. El modo privilegiado
más común da un acceso root al programa (el control administrativo total). Se
denominan root setuid a tales programas. Las operaciones que un programa realiza
limitan la capacidad de reducir al mínimo los privilegios.
Emac
s
Root
Usuario
Init
Root
Usuario
Apache
Root
Usuario
Ftp
Root
Usuario
Figura 71. Ejemplo de utilización de privilegios de root en la ejecución de varios programas. [1]
Como contraposición están programas que se ejecutan con privilegios elevados. Los
programas que no proporcionan la funcionalidad de sistema de bajo nivel raras veces
requieren el privilegio de root continuo, y un programa setuid que nunca reduce su
privilegio podría ser sintomático de un mal diseño. Si algunas operaciones de un
programa no requieren privilegios de root, la llave para la consecución de mínimos
privilegios correctamente, es reducir la cantidad de código que funciona con privilegios
de root al mínimo necesario. Incluso si un programa elimina sus privilegios de un modo
recuperable, un agente maliciosos podría encontrar un modo de recuperarlos como parte
de un ataque. Idealmente, los programas deberían ser diseñados de tal modo que
permanentemente puedan eliminar privilegios. Con un demonio de FTP por ejemplo,
esto no es siempre posible.
Como los IDs de grupo tienen impacto en los privilegios y se manejan del mismo modo
que el IDs de usuario, se enfocan como los IDs de usuario y se excluyen los IDs de grupo
de los ejemplos de este apartado en favor de la simplicidad.
Los procesos LINUX tienen un fsuid y un fsgid, que se usan para decisiones de control
de acceso sobre recursos del sistema de archivos y otras operaciones privilegiadas.
Permiten distinguir entre los privilegios usados para acceder a los sistemas de ficheros y
otras operaciones privilegiadas. Estos valores típicamente permanecen sincronizados
con el euid y egid, y por lo general juegan un papel importante en la gestión de privilegios.
Los privilegios sobre sistemas de UNIX comienzan con el login Shell de usuario. El
programa login comienza a ejecutarse como root. Después de autenticar al usuario, su
cometido es lanzar el login Shell del usuario con los IDs de usuario y de grupo
apropiados.
Como los procesos non-setuid heredan los IDs de usuario y de grupo del proceso que los
invoca, login debe cambiar sus propios IDs de usuario y de grupo antes de que este
invoque el login shell del mismo. Los sistemas UNIX proporcionan una familia de
funciones con sufijos uid y gid para cambiar el ID de usuario y de grupo que un programa
usa. El comportamiento complejo de estas funciones no está totalmente estandarizado a
través de todas las plataformas y ha conducido a una variedad de bugs relacionados
con la gestión de privilegios. El resto de este apartado trata del empleo correcto de
funciones de gestión de privilegios de sistemas UNIX.
La figura siguiente, lista cuatro funciones de gestión de privilegios comunes, con una
breve descripción de su semántica desde la correspondiente descripción de la ayuda man
de Linux. Aunque seteuid(), setuid(), y setreuid() están definidas en el estándar POSIX y
está disponible en todas las plataformas, la mayor parte de su funcionalidad se deja a
puestas en práctica individuales.
Función Prototipo Descripción
int setuid(uid_t uid) Establece el ID de usuario efectivo del proceso actual. Si el UID efectivo del
usuario que llama es root, también se establece el UID real y el ID de usuario
configurado.
int seteuid(uid_t euid) Establece el ID de usuario efectivo del proceso actual. Los procesos de usuario
no privilegiados sólo pueden establecer el ID de usuario efectivo para el ID del
usuario real, el ID de usuario efectivo o el ID de usuario de grupo.
int setreuid(uid_t Establece los ID de usuario reales y efectivos del proceso actual. Si se establece
ruid, uid_t euid) el ID de usuario real o el ID de usuario efectivo se establece en un valor que no
es igual al ID de usuario real anterior, el ID de usuario guardado se establecerá
en el nuevo ID de usuario efectivo. Proporciona un valor de -1 para el ID de
usuario real o efectivo y obliga al sistema a dejar ese ID sin cambios. Los
procesos no privilegiados sólo pueden establecer el ID de usuario efectivo para
el ID de usuario real, el ID de usuario efectivo o el ID de grupo de usuarios.
int setresuid(uid_t Establece el ID de usuario real, el ID de usuario efectivo y el ID de grupo del
ruid, uid_t euid, proceso actual. Proporcionar un valor de -1 para el ID de usuario real o efectivo
uid_t suid) y obliga al sistema a dejar ese ID sin cambios. Los procesos de usuario sin
privilegios pueden cambiar el UID real, el UID efectivo y el ID de grupo.
Tabla 6 Prototipos de funciones de gestión de privilegios en LINUX. [1]
Esto conduce a una variedad de errores comunes y malos usos de los cuales se habla
más adelante. La función de gestión de privilegios más interesante, setresuid(), ofrece
una semántica clara y está disponible sobre muchos Sistemas Operativos, como Linux
modernos y distribuciones de Unix, pero no sobre Solaris.
#include <string>
#include <iostream>
#include "task/tarea.h"
Muchos errores en programas privilegiados se derivan del mal uso de las funciones de
gestión de privilegios. Aunque setresuid() se comporta consistentemente en las
plataformas donde está soportado, muchas otras funciones de gestión de privilegios no
lo hacen. A pesar del hecho de que se incluyen en el estándar POSIX, setuid() y setreuid()
tienen matices en su implementación dependiendo de la plataforma. Hay que tener
cuidado con las inconsistencias en cambios de plataforma, en particular con
programas que cambian entre dos usuarios con IDs no raíz. En los sistemas operativos
Linux y Solaris, si el ID effective user no es 0, el nuevo ID usuario pasado a setuid() debe
igualar el ID real user o el ID saved user. Esto quiere decir que la tentativa de poner a los
tres IDs de usuario, al actual ID, podría fallar en algunas circunstancias. Sobre FreeBSD,
esta operación siempre tiene éxito. Incluso sobre una sola plataforma, los efectos
implícitos que estos métodos pueden tener sobre los IDs real user y saved user hacen
su semántica confusa y propensos a errores.
Los atacantes podrían ser capaces de hacer que las funciones de gestión de privilegios
fallen agotando recursos o cambiando el modo en que un programa se ejecuta [18].
Cuando una llamada a una función de gestión de privilegios falla de improviso, hay que
asegurarse de que el comportamiento del programa es seguro. Si la operación que falla
es un intento de eliminar privilegios, el programa debe parar su ejecución más bien que
seguir ejecutándose con mayores privilegios que los deseados.
Con estas tres operaciones en mente, los autores diseñaron un API para ejecutar estas
operaciones y no otras. Se da un prototipo de función y una breve descripción para cada
una de las funciones en la Figura 66, el ejemplo de la figura 67, muestra como un
programa simple privilegiado se puede escribir de dos formas distintas: con
setresuid() y con este API [19]. Aunque estos ejemplos pongan en práctica una gestión
de privilegios correcta en general, su seguridad podría mejorarse deshabilitando señales
antes de la elevación de sus privilegios, la figura siguiente, muestra un ejemplo con la
versión corregida totalmente con deshabilitación de señales de este código.
}
/* Privileges not necessary or desirable at this point */
processCommandLine(argc, argv);
/* Regain privileges */
if (restore_priv() != 0) {
exit(-1);
}
openSocket(88); /* requires root */
/* Drop privileges for good */
if (drop_priv_perm(getuid()) != 0) {
exit(-1);
}
doWork();
}
Figura 73. Ejemplos de gestión de privilegios 2: API. [1]
}
openSocket(88); /* requires root */
/* Drop privileges for good */
if (setresuid(caller_uid, caller_uid, caller_uid) != 0) {
sigprocmask(sigmask);
exit(-1);
}
/* re-enable signals */
if (sigprocmask(SIG_SETMASK, &saved, NULL) != 0) {
exit(-1);
}
doWork();
}
Figura 74. Ejemplo de gestión de privilegios deshabilitando señales antes de elevar privilegios. [1]
En sistemas FreeBSD, hay que pensar en usar la utilidad jaula, que pone en práctica
protecciones aún más fuertes que la jaula chroot [21].
Una llamada a chroot() puede fallar. Comprobar el valor de retorno de chroot() para
asegurarse del resultado de la llamada.
Para llamar chroot(), el programa debe ejecutarse con privilegios de root. En cuanto
la operación privilegiada se ha completado, el programa debería eliminar los
chroot("/var/ftproot");
}
}
Figura 76. Código de un simple FTP server reescrito para crear una jaula chroot correctamente. [1]
Hay que tener en cuenta una serie de directrices generales para manejar eventos
inesperados y excepciones que toman una importancia aún mayor en programas
privilegiados, donde los ataques son más probables y el coste de un exploit consumado
es mayor.
padre, por tanto si un proceso se ejecuta con privilegios de root cuando una señal se
activa o un subproceso es ejecutado, el manejador de la señal o el subproceso se
ejecutará con privilegios de root. El código que maneja las señales nunca debería
requerir privilegios elevados.
Desde luego, un manejador de señales bien escrito debería ser bastante pequeño y
bastante simple, de forma que no pueda causar errores cuando se ejecuta con privilegios:
manejadores de señales que tienen una complejidad mínima son siempre un objetivo que
vale la pena.
1º
Un programa comprueba alguna propiedad de un archivo,
refiriéndose al archivo por su nombre más bien que por el
objeto del sistema de fichas subyacente
2º
Un atacante cambia el significado del nombre del archivo
que el programa comprobó de modo que refiera un objeto
del sistema de archivos diferente.
3º
El programa más tarde realiza una operación sobre el
sistema de archivos que usa el mismo nombre del archivo y
asume que la propiedad previamente comprobada se
mantiene
Figura 77. Secuencia ataque TOCTOU
La figura siguiente, muestra el modo que las operaciones en lpr podrían intercalarse con
la ejecución del código de un agente malicioso de un ataque con éxito. Un atacante
invoca lpr con el argumento/tmp/attack y redirecciona el archivo para que apunte al
fichero de sistema/etc/shadow durante el tiempo que pasa entre que es comprobado y
utilizado.
Tiempo Lpr Atacante
Tiempo 1 access(“/tmp/attack”)
Tiempo 2 unlink(“/tmp/attack”)
Tiempo 3 symlink(“/etc/shadow”,“/tmp/attack”)
Tiempo 4 open(“/tmp/attack”)
Figura 78. Esquema de tiempos de una vulnerabilidad TOCTOU en el acceso a un fichero. [1]
La ventana de vulnerabilidad para tal ataque es el período de tiempo entre cuando una
propiedad es comprobada y cuando se usa el archivo. Los atacantes tienen una variedad
de técnicas para ampliar la longitud de esta ventana para hacer los exploits más fáciles,
como el enviar a una señal al proceso víctima que hace que ceda la CPU a otro proceso.
Incluso con una pequeña ventana, una tentativa de exploit puede repetirse hasta que se
consiga llevar a cabo.
Hay que tener en cuenta que un ataque de condición de carrera todavía puede darse
después de abrir el archivo, si alguna operación posterior depende de una propiedad
comprobada anterior a la apertura del archivo. Por ejemplo, si la estructura pasada a
stat() se obtiene antes de que un archivo sea abierto y una decisión posterior sobre si hay
que operar sobre el archivo está basada en un valor leído de la estructura de stat, entonces
una condición de carrera TOCTOU puede darse entre la llamada a stat() y la llamada
open().
Por suerte, la mayor parte de estas funciones hacen cumplir permisos de acceso al
sistema de archivos cuando se ejecutan, como son link(), mkdir(), mknod(), rename(),
rmdir(), symlink (), unlink(), y utime(). Si se disminuyen los privilegios del usuario actual
antes de la realización de cualquiera de estas operaciones, se asegura la comprobación
de control de acceso al sistema de archivos estándar. Si estas operaciones se
realizan con privilegios debido a los recursos que utilizan, el único modo de conseguir
seguridad es usando permisos de sistema de archivos restrictivos para impedir a los
usuarios cambiar el significado del nombre del archivo simbólico usado.
Las tablas 9 y 10, listan una serie de funciones de la biblioteca de C que intentan generar
un nombre de archivo único para un nuevo archivo temporal. Estas funciones sufren
inherentemente de vulnerabilidad de condición de carrera, TOCTOU, subyacente
en el nombre del archivo escogido. Aunque las funciones garanticen que el nombre del
archivo es único en el tiempo que es seleccionado, no hay ningún mecanismo que impida
a un atacante crear un archivo con el mismo nombre seleccionado antes de que la
aplicación intente abrir el archivo.
La probabilidad de ataques con éxito contra estas funciones aumenta por el hecho de que
usan fuentes muy pobres de aleatoriedad en los nombres que generan, lo cual hace
que un atacante pueda tener éxito. Si un atacante realmente logra crear el archivo
primero, dependiendo de cómo el archivo es abierto, el contenido o los permisos de
acceso del archivo podrían permanecer intactos. Si el contenido del archivo es malévolo
en naturaleza, un atacante podría inyectar datos peligrosos en la aplicación cuando lee
datos del archivo temporal. Si un atacante pre-crea el archivo con más permisos de
acceso de los necesarios, podría más tarde tener acceso, modificar, o corromper datos
que la aplicación almacena en el archivo temporal. Si el atacante pre-crea el archivo como
un enlace a otro archivo importante, la aplicación podría truncar o escribir datos al
archivo y sin ser consciente realizar operaciones perjudiciales.
Finalmente, en el mejor caso, el archivo puede ser abierto con open() usando O_CREAT
y O_EXCL flags, que fallará si el archivo ya existe y por tanto, prevenir estos tipos de
ataques. Sin embargo, si un atacante con exactitud puede predecir una secuencia de
nombres del archivo temporales, podría ser capaz de impedir al programa abrir el
almacenamiento temporal necesario, causando con eficacia un ataque de denegación de
servicio. Este tipo de ataque es trivial de montar, dada la pequeña cantidad de
aleatoriedad usada en la selección de los nombres del archivo que estas funciones
generan. En Windows, la función GetTempFileName() sufre de las mismas
vulnerabilidades.
Función Descripción
char* mktemp (char El mktemp () genera un nombre de archivo único por plantilla
*template) modificanda [...]. Si tiene éxito, devuelve la plantilla
modificada. Si mktemp () no puede encontrar un nombre de
archivo único, crea una plantilla vacía y la devuelve.
char* tmpnam (char *result) Esta función crea y devuelve un nombre de archivo válido que
no hace referencia a ningún archivo existente. Si el argumento
resultado es un puntero nulo, el valor de retorno es un
puntero a una cadena interna estática, que puede ser
modificado por llamadas posteriores. Si no, el resultado debe
ser un puntero a una matriz de al menos L_tmpnam
caracteres, y el resultado se escribe en la misma.
char* tempnam (const char Esta función genera un nombre de archivo temporal único. Si
*dir, prefijo no es un puntero nulo, hasta cinco caracteres de esta
const char *prefix) cadena usa como prefijo para el nombre de archivo. El valor
de retorno es una nueva cadena generada con malloc (), por
lo que debe liberar la memoria que ocups cuando ya no se
necesita.
Tabla 9 Funciones que intentan generar un único fichero temporal. [1]
Función Descripción
FILE* tmpfile (void) Esta función crea un archivo binario temporal para el modo
de actualización, como si al llamar fopen () con modo wb+. El
archivo se borra automáticamente cuando se cierra o cuando
el programa termina.
Int mkstemp (char *template) El mkstemp () genera un nombre de archivo único y abre el
archivo con open () [con el flag O_EXCL]. Si tiene éxito, se
modifica la plantilla y devuelve un descriptor para el archivo
abierto en modo lectura y escritura. Si mkstemp () no puede
crear un archivo con un nombre único, devuelve -1. El archivo
se abre en el modo 0600.
Tabla 10 Funciones que intentan generar un único fichero temporal y abrirlo además. [1]
Donde está disponible, mkstemp(), es la mejor opción para crear archivos temporales
entre las funciones ofrecidas por la biblioteca estándar. Pero debido al problema de
permisos de archivo con mkstemp() en viejos sistemas, se debería requerir que todos los
archivos recién creados sean accesibles solo al usuario actual llamando a umask(077)
antes de la creación de cualquier archivo temporal, esto fuerza a todos los archivos recién
creados sean accesibles solo por el usuario que los crea. Como los permisos que asigna
umask() se heredan de un proceso a otro, no confiar en la umask puesta por defecto por
la shell. Un atacante explícitamente puede invocar su programa con una umask
inadecuada y conseguir su objetivo.
if(cur == -1) {
return –1;
}
if(lstat(dir, &linf) == –1) {
close(cur);
return –2;
}
do {
chdir(dir);
if((fd = open(".", O_RDONLY)) == –1) {
fchdir(cur); close(cur); return –3;
}
if(fstat(fd, &sinf) == –1) {
fchdir(cur); close(cur);
close(fd); return –4;
}
close(fd);
Figura 81. Código para creación de un directorio seguro. [1]
Inyección de comandos
CGI-Web que permite a los usuarios cambiar sus contraseñas en el sistema [22]. El
proceso de actualización de contraseña incluye ejecutar make en el directorio /var/yp.
Notar que debido a que el programa pone al día registros de contraseña, debe ser
instalado con setuid root.
Como el programa no especifica una ruta absoluta para sus variables de entorno antes de
la invocación del comando, un atacante puede aprovecharse del programa ejecutándolo
localmente modificando la variable de entorno $PATH para apuntar su código malicioso
llamado make y luego ejecutando un script CGI desde un intérprete de comandos shell,
el atacante puede ejecutar código arbitrario con privilegios de root. En programas
seguros privilegiados, $PATH debería contener únicamente directorios de root, ya que
el programa probablemente tendrá que ejecutar utilidades de sistema con acceso a
directorios controlados por el propietario del progarama. No incluir el directorio actual
(.) rutas relativas que un atacante podría ser capaz de manipular. En la mayor parte de
entornos, $IFS debería ser puesto a su valor por defecto, \t\n.
Descriptores de ficheros
Los descriptores de archivo estándar stdin (FD 0), stdout (FD 1), y stderr (FD 2) son
típicamente abiertos por el terminal y son usados tanto explícitamente como
implícitamente por funciones como printf(). Algunos programas remiten uno o varios de
estos descriptores a streams de datos diferentes, como un archivo de log, para reutilizar
su comportamiento implícito de forma que sea más adecuado al diseño del programa,
pero la mayor parte los mismos nunca cambia sus valores. Como con umask y con
variables de entorno, un proceso hijo hereda descriptores de archivo estándar de su
padre. Los atacantes pueden aprovechar este hecho para hacer que un
programa vulnerable lea o escriba en archivos del sistema cuando en
realidad se espera interactuar con el terminal.
fd = open("/etc/passwd", O_RDWR);
...
if (!process_ok(argv[1])) {
perror(argv[1]);
}
Figura 82. Código vulnerable a ataques a su descriptor de ficheros estándar. [1]
abrir archivos streams (si el programa tiene la intención de usar funciones que
implícitamente dependen de estos valores), o abrir /dev/null tres veces para asegurar
que los tres primeros descriptores de archivo se usan. El ejemplo de la figura 84, ilustra
la segunda opción.
2.8. Referencias
[1]. Chess, B., and West, J. Secure Programming with Static Analysis. Addison-
Wesley Software Security Series.
[3]. Graff, M. G., and van Wyk, K. R. (2003). Secure Coding: Principles & Practices.
O'Reilly.
[4]. Howard, M., and LeBlanc, D. (2003). Writing Secure Code. Microsoft Press.
[5]. Allen, J. H., Barnum, S., Ellison, R. J., McGraw, G., and Mead, N. R. (2008).
Software Security Engineering: A Guide for Project Managers. Addison Wesley
Professional.
[6]. Goertzel, K. M., and Winograd, T. (2008). Enhancing the Development Life Cycle
to Produce Secure Software, Version 2.0. United States Department of Defense
Data and Analysis Center for Software.
[7]. Howard, M., and Lipner, S. (2006).The Security Development Lifecycle: SDL: A
Process for Developing Demonstrably Secure Software. Microsoft Press.
[8]. Goertzel, K. M., Winograd, T. (2008). Enhancing the Development Life Cycle to
Produce Secure Software, Version 2.0. United States Department of Defense Data
and Analysis Center for Software.
[10]. Hoglund, G., and McGraw, G. Exploiting Software: How to Break Code. Addison-
Wesley Software Security Series.
[11]. The SANS Technology Institute. (2006). The Twenty Most Critical Internet
Security Vulnerabilities. Recuperado el 19 de abril de 2013 en
http://www.sans.org/top20/
[12]. Wagner, D., Foster, J. S., Brewer, E. A., and Aiken, A. (2002). A First Step Towards
Automated Detection of Buffer Overrun Vulnerabilities. Proceedings of the 7th
Network and Distributed System Security Symposium. San Diego, CA.
[15]. CERT (2000). Malicious HTML Tags Embedded in Client Web Requests.
Recuperado el 19 de abril de 2013 en http://www.cert.org/advisories/CA-2000-
02.html
[17]. Brown, K. (2004). Windows Vista Technical Articles. Security in Longhorn: Focus
on Least Privilege, DevelopMentor.
[19]. Chen, H., et al. Setuid Demystified. 11th USENIX Security Symposium (San
Francisco, CA, 2002), 171–190. Recuperado el 19 de abril de 2013 en
http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf
[20]. Provos, N., Friedl, M., and Honeyman, P. (2003). Preventing Privilege Escalation.
http://niels.xtdnet.nl/papers/privsep.pdf