Hogar Mapa Indice Busqueda Noticias Arca Enlaces Sobre LF
[Top bar]
[Bottom bar]
Este documento está disponible en los siguientes idiomas: English  Castellano  Deutsch  Francais  Nederlands  Indonesian  Russian  

convert to palmConvert to GutenPalm
or to PalmDoc

[image of the authors]
por Frédéric Raynal, Christophe Blaess, Christophe Grenier

Sobre el autor:

Christophe Blaess es un ingeniero en aeronáutica independiente. Él es un entusiasta de Linux y realiza la mayoría de sus trabajos en este sistema. Él está a cargo de la coordinación en la traducción de las 'man pages' publicada por el Proyecto de la documentación Linux(LDP).

Christophe Grenier es estudiante del quinto año en el ESIEA, donde él también se desempeña como Administrador de sistemas. Es un apasionado por la seguridad computacional.

Frédéric Raynal usa Linux, certificado sin (software u otro) patentes. Aparte de eso, usted debe ver Dancer in the Dark: además de que Björk es grande, esta película no puede dejarlo indiferente (yo no puedo decir más sin quitar el velo del final, trágico y espléndido).


Contenidos:

Evitando los agujeros de seguridad durante el desarrollo de aplicaciones - Parte 1

[article illustartion]

Resumen:

Este artículo es el primero de una serie sobre los principales agujeros de seguridad que, normalmente pueden aparecer dentro de un aplicación. A lo largo de estos artículos, nosotros mostraremos las maneras de evitarlos cambiando un poco el desarrollo de los hábitos.



 

Introducción

No toma más de dos semanas antes que una aplicación mayor, parte de la mayoría de las distribuciones de Linux, nos presente un agujero de seguridad, permitiendo, por ejemplo, a un usuario local para alcanzar privilegios de root. A pesar de la gran calidad de la mayoría de estos programas, es un trabajo duro asegurar la fiabilidad del mismo: no se le debe permitir a un tipo con malas intenciones, beneficiarse ilegalmente de los recursos del sistema. La disponibilidad en la aplicación del código fuente es bueno, muy apreciado por los programadores, pero un pequeño defecto en el programa se hace visible a todos. Además, el descubrimiento de los tales defectos son aleatorios y las personas que hacen esta clase de cosas no siempre actúan con buenas intenciones.

Del lado del administrador de sistemas, el trabajo diario consiste en la lectura de las listas relacionadas con los problemas de seguridad e inmediatamente poner al día los paquetes afectados. Para un programador puede ser una buena lección, poner a prueba los problemas de seguridad. Es preferible evitar desde un principio los agujeros de seguridad. Intentaremos definir algunas conductas peligrosas "clásicas"

y proporcionar soluciones para reducir los riesgos. Nosotros no hablaremos sobre los problemas de seguridad en redes, ya que ellos se presentan a menudo por errores de la configuración ( los peligrosos scripts cgi-bin,...) o de los errores del sistema que permiten los ataques del tipo DoS (Denegación de Servicio) para impedir a una máquina escuchar a sus propios clientes.

Estos problemas involucran a los Administradores de sistemas o desarrolladores del kernel, pero también al programador de la aplicación, en tanto tenga en cuenta los datos externos. Por ejemplo, pine, acroread, netscape, access,... en algunas versiones y bajo ciertas condiciones, permiten el acceso o fugas de información. De hecho la programación segura nos concierne a todos.

Este grupo de artículos muestran los métodos que pueden usarse para dañar un sistema Unix. Nosotros sólo hemos mencionado algunos o dicho algunas palabras sobre ellos, pero preferimos explicaciones abiertas para hacer entender a las personas de tales riesgos. Así, cuando ponemos a punto un programa o desarrollamos uno propio, usted podrá evitar o corregir éstos errores. Para cada uno de los agujeros que se traten, efectuaremos el mismo análisis. Empezaremos detallando la manera de funcionamiento. Luego, mostraremos cómo evitarlo. Para cada ejemplo, usaremos los agujeros de seguridad que se presentan frecuentemente en un amplio expectro de programas.

Este primer artículo habla sobre el fundamentos necesarios para la comprensión de los agujeros de seguridad, que son la noción de privilegio y el bit de Set-UID o Set-GID. Luego, analizaremos los agujeros basados en la función system (), ya que son más fáciles de entender.

A menudo usaremos pequeños programas en C, para ilustrar sobre lo que nosotros hablaremos. Sin embargo, los acercamientos mencionados en estos artículos son aplicables a otros lenguajes de programación: perl, java, shell scripts... Algunos agujeros de seguridad dependen de un lenguaje, pero esto no es completamente cierto para todos ellos, cuando nosotros lo veamos con system ().

 

Privilegios

En un sistema Unix, los usuarios no son iguales y las aplicaciones tampoco. El acceso a los nodos del sistema de archivos y -de acuerdo con los periféricos de la máquina - confíamos en un control de identidad estricto. Algunos usuarios se permiten realizar operaciones sensibles para mantener el sistema en buenas condiciones. Un número llamado UID (User Identifier) permite la identificación. Para hacer las cosas más fácil, un nombre del usuario corresponde a este número, la asociación se hace en el archivo de /etc/passwd.

El usuario root, con UID predefinido de 0, puede acceder a todo el sistema. Él puede crear, modificar, quitar cada nodo del sistema, pero también puede manejar la configuración física de la máquina y puede montar particiones, activar interfaces de red y cambiar su configuración (dirección IP ), o usando llamadas del sistema como es mlock () para actuar en la memoria física, o sched_setscheduler () para cambiar el mecanismo del ordenación. En un artículo futuro, estudiaremos los Posix.1e ,características que permiten limitar un bit de los privilegios de una aplicación ejecutados como root, pero por ahora, asumamos que el superusuario puede hacer de todo en una máquina.

Los ataques que nosotros mencionaremos son internos, es decir, que es un usuario autorizado en una máquina que intenta conseguir privilegios que no tiene. Por otro lado, los ataques de la red son externos y vienen de las personas que intentan conectarse a una máquina donde no les está permitido. Para conseguir los privilegios de otros usuarios, lo piensan hacer bajo el nombre, el UID de ese usuario, y no bajo el nombre de usuario propio. Por supuesto, un cracker intenta conseguir el ID del superusuario, pero también hay muchas otras cuentas de usuarios que son de interés, porque cualquiera de ellas dan acceso a la información del sistema (news, mail, lp...) o porque ellas permiten leer datos privados (correo, archivos personales, etc) o ellas pueden usarse para ocultar actividades ilegales como ataques hacia otros sitios.

Para usar privilegios reservados de otro usuario, sin poder notar su verdadera identidad, uno debe por lo menos tener la oportunidad de comunicarse con una aplicación que corre bajo el UID de la víctima. Cuando una aplicación - un proceso - corre bajo Linux, tiene una identidad bien definida. Primero, el programa tiene un atributo llamado RUID (Real UID) correspondiendo al usuario ID que lo lanzó. Este dato es manejado por el kernel y normalmente no puede cambiarse. Un segundo atributo completa esta información: el campo EUID (Effective UID) correspondiendo a la identidad del kernel, que tiene en cuenta cuando maneja los derechos de acceso (abriendo archivos, llamados al sistema reservados).

Para ejecutar una aplicación con un EUID (sus privilegios) diferente del RUID (el usuario que lo lanzó), el archivo ejecutable debe tener un bit específico llamado Set-UID. Este bit se encuentra en el atributo de permisos del archivo (como usuario puede ejecutar, leer, escribir bits, miembros de grupo u otros) y tiene el valor octal de 4000. El bit del Set-UID se representa con un s al desplegarse los derechos con el comando ls:

>> ls -l /bin/su
-rwsr-xr-x 1 root root 14124 Aug 18 1999 /bin/su
>>

El comando "find / -tipo f -perm +4000" despliega una lista de las aplicaciones del sistema que tienen su bit de Set-UID fijandolo en 1. Cuando el kernel ejecuta una aplicación con el bit Set-UID puesto en 1, usa la identidad de propietario como EUID de los procesos. Por otro lado, el RUID no cambia y corresponde al usuario que lanzó el programa. Hablando por ejemplo sobre /bin/su, cada usuario puede tener acceso a este comando, pero corre bajo su identidad de propietario (root), de acuerdo a cada uno de los privilegios que tiene en el sistema. No basta decir, que se debe ser muy cuidadoso al escribir un programa con este atributo.

Cada proceso también tiene un ID de grupo Efectivo, EGID, y un identificador real RGID. El bit del Set-GID (2000 en octal) en los derechos de acceso de un archivo ejecutable, le pide al kernel tomar el grupo de propietarios del archivo como EGID y no de uno del de grupo que haya lanzado el programa. A veces aparece una combinación curiosa, con el Set-GID fijado en 1, pero sin el bit de ejecución de grupo. De hecho, es una convención que no tiene nada que hacer con privilegios relacionados con las aplicaciones, pero indicando el archivo que puede bloquearse con la función fcntl(fd, F_SETLK, lock). Normalmente una aplicación no usa el bit Set-GID, pero a veces pasa, en algunos juegos, por ejemplo, lo usan para guardar los mejores resultados en un directorio del sistema.

 

Tipo de ataques y los blancos potenciales

Hay varios tipos de ataques contra un sistema. Hoy nosotros estudiamos los mecanismos para ejecutar un comando externo desde dentro y la aplicación. Éste normalmente es la shell que corre bajo la identidad del dueño de la aplicación. Un segundo tipo de ataque confía en la inundación de la memoria temporal(buffer overflow), dandole al atacante la posibilidad de acuerdo a instrucciones del código personales. Por último, el tercer tipo principal de ataque es basado en la condición de competencia(race condition), lapso de tiempo entre dos instrucciones en las que un componente del sistema se cambia (normalmente un archivo) mientras la aplicación lo considera inmutable.

Los dos primeros tipos de ataques, intentan a menudo ejecutar un shell con los privilegios del propietario de la aplicación, mientras el tercero tiene como objetivo conseguir acceso de escritura a los archivos del sistema protegido. A veces el acceso de lectura es considerado como una debilidad de la seguridad del sistema (archivos personales, emails, el archivo de la contraseña /etc/shadow, y los archivos de configuración del pseudo-kernel en /proc.

Los blancos de ataques de seguridad son principalmente los programas que tienen el bit de Set-UID (o Set-GID) habilitado. Sin embargo, esto también concierne a cada aplicación que corre bajo un ID diferente a uno de los del usuario. Los demonios del sistema representan una parte importante de estos programas. Un demonio normalmente es una aplicación que empieza al momento de la inicialización(boot) y corre en segundo plano sin ningún terminal de control, y efectuando tareas con privilegios para cualquier usuario. Por ejemplo, el demonio lpd permite a cualquier usuario enviar documentos a la impresora, el sendmail recibe y envía correo electrónico, o el apmd le pide el estado de la batería a la BIOS de un portátil. Algunos demonios están a cargo de la comunicación con usuarios externos a través de la red (los servicios Ftp, Http, Telnet...). Un servidor llama al inetd para manejar la conexión.

Entonces nosotros podemos concluir que un programa puede atacarse en cuanto se comunique - muy brevemente - a un usuario diferente del que lo empezó. Si el diseño de una aplicación suya posee semejante rasgo, usted debe tener cuidado mientras la desarrolla y tener presente los riesgos que se presentan con las funciones que hemos estudiado.

 

Cambiando los niveles de privilegios

Cuando una aplicación corre con un EUID diferente de su RUID, es proporcionarle privilegios a ese usuario que no debería tener (acceso al archivo, llamados al sistema reservado...). Sin embargo, esto sólo se necesita puntualmente, por ejemplo cuando abrimos un archivo; por otra parte la aplicación puede cubrirse con los privilegios de su usuario. Es posible temporalmente cambiar una aplicación EUID con la llamada al sistema:

seteuid del int (uid del uid_t);

Un proceso siempre puede cambiar sus valores EUID dándole uno de su RUID. En ese caso, el UID viejo se retiene en un campo de guardado llamado SUID (Saved UID) diferente del SID (Session ID) usado por el administrador del terminal de control. Siempre es posible volver de los SUID para usarlos como EUID. Por supuesto, un programa que tiene un null EUID (root) puede cambiar su EUID y RUID a voluntad (es la manera como trabaja /bin/su).

Para reducir los riesgos de ataques, se sugiere también cambiar el EUID y usar el RUID de los usuarios. Cuando una porción de código necesitan privilegios que corresponden a aquéllos propietarios del archivo, es posible poner el Saved UID en EUID. Aquí hay un ejemplo:

uid_t e_uid_initial;
uid_t r_uid;

int main (int argc, char * argv [])
{
  /* Se guardan las diferentes UIDs */
  e_uid_initial = geteuid ();

r_uid = getuid ();

/* limita los derechos de acceso a uno de los * programas usados en el lanzamiento */ seteuid (r_uid); ... privileged_function (); ... } void privileged_function (void) { /* Le devuelve los privilegios iniciales */ seteuid (e_uid_initial); ... /* Porción que necesita los privilegios */ ... /* Devuelve los derechos del programa ejecutante */ seteuid (r_uid); }

Esta manera de trabajar es mucho más segura que el opuesto, demasiado a menudo vista y consiste en utilizar la EUID inicial y entonces reducir temporalmente los privilegios sólo antes de hacer una operación "arriesgada". Sin embargo esta reducción del privilegio es inútil contra los ataques de desbordamiento de memoria temporal. Cuando nosotros veamos en un próximo artículo, estos ataques intentan interrogar a la aplicación para la ejecución de instrucciones personales y pueda contener las llamadas al sistema (system-calls) necesarias para obtener el nivel de privilegios más alto. No obstante, este acercamiento nos protege de las comandos externos y de la mayoría de las condiciones de competencia.

 

Ejecutando comandos externos

Una aplicación necesita a menudo llamar a un servicio del sistema externo. Un ejemplo bien conocido, involucra a mail que ordena como manejar un correo electrónico (informe corriente, alarmas, estadísticas, etc.) sin requerir un complejo diálogo con el sistema de correo. La solución más fácil es usar la función de la biblioteca:

int system (const char * command)

 

Peligros de la función system ()

Esta función es bastante peligrosa: llama a la shell para ejecutar el comando enviándolo como un argumento. La conducta de la shell depende de la opción del usuario. Un ejemplo típico viene de la variable de ambiente PATH. Supongamos una aplicación que llama a la función del mail. Por ejemplo, el programa siguiente envía su código fuente al usuario que lo lanzó:

/ * system1.c * /
#include < stdio.h >
#include < stdlib.h >
int
main (void)
{
  if (sistema ("el correo $USER < system1.c") != 0)
    perror ("sistema");
  return (0);
}

Digamos que este programa es el Set-UID del superusuario:

>> cc system1.c -o system1
>> su
Password:
[root] el chown root.root system1
[root] el chmod +s system1
[root] exit
>> ls -l system1
-rwsrwsr-x 1  root root  11831  Oct 16  17:25 system1
>>

Para ejecutar este programa, el sistema ejecuta un shell (con /bin/sh) y con la opción -c, le dice la instrucción para invocar. Entonces la shell pasa por la jerarquía del directorio, según la variable de ambiente del PATH que encuentra un ejecutable llamado mail. Entonces, el usuario sólo tiene que cambiar esta variable contenida antes de correr la aplicación principal. Por ejemplo:

>> export PATH=.
>>. /system1

intenta encontrar el comando mail dentro del directorio actual. Bastan entonces, para crear allí un archivo ejecutable (para este caso, un script que ejecute una nueva shell) y llamar al mail y el programa se ejecuta entonces con el EUID de dueño de la aplicación principal. Aquí, nuestro script ejecuta /bin/sh. Sin embargo, desde que se ejecuta con una entrada estándar redireccionada (como la del mail inicial), nosotros debemos volver al terminal. Entonces nosotros creamos el script:

#! /bin/sh
# "mail" script que corre bajo el shell
# lo devuelve a su entrada normal.
/bin/sh < /dev/tty
Aquí está el resultado:
>> export PATH="."
>> . /system1
bash# /usr/bin/whoami
root
bash#

Por supuesto, la primera solución consiste en dar la ruta completa del programa, por ejemplo /bin/mail. Entonces aparece un nuevo problema: la aplicación confía en la instalación del sistema. Si /bin/mail está normalmente disponible en cada sistema, ¿dónde está por ejemplo, GhostScript? (está en /usr/bin, /usr/share/bin, /usr/local/bin). Por otro lado, otro tipo de ataque es posible con shell antiguas: el uso de la variable de ambiente IFS. La shell lo usa para analizar sintácticamente las palabras en la línea de comandos. Esta variable contiene los separadores. Los valores por defecto son el espacio, el tabulador y el retorno. Si el usuario agrega la barra inclinada /, el comando " /bin/mail" se entiende por la shell como "bin mail" Un archivo ejecutable llamado bin en el directorio actual, puede ser ejecutado simplemente poniendo el PATH, como hemos visto antes, y permitirnos ejecutar este programa con la aplicación EUID.

Bajo Linux, la variable de ambiente IFS no es ya un problema desde que el bash lo completa con los carácteres por defecto en la partida (también hecho con pdksh). Pero, con la portabilidad de la aplicación en mente, usted debe estar consciente de que algunos sistemas pueden quedar inseguros viéndolos con esta variable.

Algunas otras variables de ambiente pueden causar problemas inesperados. Por ejemplo, la aplicación de mail le permite al usuario ejecutar un comando mientras compone un mensaje usando una sucesión de escape" ~! ". Si el usuario escribe el string" ~ ! command" al principio de la línea, el comando se ejecuta. El programa /usr/bin/suidperl usado para hacer los scripts en perl Set-UID, al descubrir un problema, llama a /bin/mail para enviar un mensaje al superusuario La aplicación que es del Set-UID superusuario , invoca a /bin/mail que lo hace bajo esta identidad. En el mensaje enviado al superusuario , el nombre del archivo defectuoso está presente. Un usuario puede crear un archivo entonces donde el nombre del archivo contiene un retorno del carro seguido por un secuencia ~!command y otro retorno de carro. Si el script en perl llamado suidperl falla en un problema de bajo nivel relacionado a este archivo, un mensaje se envía bajo la identidad del superusuario conteniendo la secuencia de escape desde la aplicación del mail.

Este problema no debería existir si es que el programa mail, suponemos que no acepta secuencias de escape cuando corre automáticamente (no de un terminal). Desgraciadamente, un característica indocumentada de esta aplicación (probablemente dejada desde la depuración), permite que las secuencias de escape interactúe como también cuando se fijó la variable de ambiente . ¿El resultado? Un agujero de seguridad fácilmente explotable (y ampliamente utilizado) en una aplicación que se supone mejora la seguridad del sistema. El error es compartido. Primero, /bin/mail tiene una opción indocumentada muy peligrosa, ya que permite la ejecución del código que sólo verifica los datos enviados, lo que debe ser a priori sospechoso para una utilidad de mail. Segundo, aún cuando el desarrollo de /usr/bin/suidperl no ponen cuidado de la variable interactive, ellos no deben dejar pasar por alto el ambiente de la ejecución cuando se hace una llamada con un comando externo, sobre todo cuando escribimos este programa con el Set-UID de superusuario.

De hecho, Linux ignora el bit del Set-UID y del Set-GID al ejecutar los scripts (léase /usr/src/linux/fs/binfmt_script.c y /usr/src/linux/fs/exec.c). Algunos trucos permiten saltarse esta regla, como Perl que hay que tener en cuenta, lo hace con sus propios scripts que usan este bit en /usr/bin/suidperl .

 

Soluciones

No es tan fácil encontrar siempre un reemplazo para la función system () . La primera variante es usar las llamadas al sistema como execl () o execle (). Sin embargo, será bastante diferente desde que el programa externo ya no se llama como un subrutina, pero el comando invocado reemplaza el proceso actual. Usted debe agregar una duplicación del proceso y analizar sintácticamente los argumentos de la línea de comandos. Así el programa:

if (system ("/bin/lpr -Plisting stats.txt") != 0) {
  perror ("Imprimiendo");
  retorno (-1);
}
se vuelve:
pid_t pid; int   status;
 if ((pid = fork ()) < 0) {   perror ("fork");   return (-1);
}
if (pid == 0) {
  /* el proceso hijo */
  execl (" /bin/lpr", "lpr"," -Plisting", "stats.txt", NULL);
  perror ("execl");
  exit (-1);
}
/* el proceso del padre */
waitpid (pid, & status, 0);
if ((! WIFEXITED (status)) || (WEXITSTATUS (status) != 0)) {
  perror ("Imprimiendo");
  retorno (-1);
}

¡Obviamente, el código se pone más pesado! En algunas situaciones, se pone bastante complejo, por ejemplo, cuando usted debe redirigir la aplicación a la entrada estándar como en:

system ("mail root < stat.txt"); 

Es decir, el redireccionamiento definido por < se hace desde la shell. Usted puede hacer el mismo, usando un trabajo complejo con sucesiones como fork (), open (), dup2 (), execl (), etc. En ese caso, una solución aceptable sería usando la función system (), pero configurando completamente el ambiente.

Bajo Linux, las variables de ambiente se guardan en la forma de un puntero en la tabla de carácteres: char ** environ. Esta tabla termina con NULL. Los strings son de la forma "NAME=value"

Nosotros empezamos quitando el ambiente que usa en la extensión Gnu:

clearenv del int (void);
o forzando al puntero
extern char ** environ;

para tomar el valor NULL. Luego las variables de ambiente importantes se inicializan usando valores controlados, con las funciones:

int setenv (const char * name, const char * value int remove)
int putenv(const char *string)

antes de llamar a la función system () . Por ejemplo:r

clearenv ();
setenv ("PATH"," /bin:/usr/bin:/usr/local/bin", 1);
setenv ("IFS"," \t\n", 1);
system  ("mail root < /tmp/msg.txt");

Si es necesario, usted puede devolver el contenido de algunas variables útiles antes de quitar el ambiente (HOME, LANG, TERM, TZ,etc.). El contenido, la forma, el tamaño de estas variables debe verificarse concienzudamente. Es importante que usted quite de todo el ambiente, antes de redefinir las variables que necesitará. El agujero de seguridad de suidperl no habría aparecido si el ambiente hubiese sido previamente removido.

En forma similar, protegiendo primero una máquina en una red implica denegar cada conexión. Luego, se activan los servicios requiridos o útiles. De la misma manera, al programar la aplicación de un Set-UID , el ambiente debe aclararse y entonces debe llenarse con las variables requeridas.

Verificando si el formato del parámetro es aceptable comparándolo con el valor esperado de los formatos permitidos. Si la comparación tiene éxito, el parámetro se valida. De otra manera, se rechaza. Si usted ejecuta la prueba usando una lista de expresiones inválidas del formato, aumenta el riesgo de dejar valores erróneos y puede ser un desastre para el sistema.

Nosotros debemos entender lo peligroso que es con system () , como también, es más peligroso para algunos funciones derivadas como popen (), o con llamadas al sistema como execlp () o execvp () teniendo en cuenta la variable PATH.

 

Comandos de ejecución indirecta

Para mejorar el diseño de los programas, es fácil de dejarle conducir al usuario la habilidad de poder configurar la mayoría del software , usando macros por ejemplo. Manejar variables o los modelos genéricos como lo hace la shell; hay una poderosa función llamada wordexp ().

Usted debe tener mucho cuidado con ella, desde enviar una cadena como $(commande) , que permite ejecutar el mencionado comando externo.

Basta con darle la cadena " $(/bin/sh)" para conseguir la shell del Set-UID. Para evitar semejante cosa, wordexp () tiene un atributo llamado WRDE_NOCMD dejando fuera de funcionamiento la interpretación de las secuencias $().

Cuando invocamos comandos externos usted debe ser cuidadoso con no llamar una utilidad que proporcione un mecanismo de escape hacia la shell (como por ejemplo, la secuencia vi :!command ). Es difícil de listarlos todos, algunas aplicaciones son obvias (editores del texto, administradores de archivos...), otros son más difíciles de descubrir (como hemos visto con /bin/mail) o tienen modos de depuración peligrosos.

 

Conclusión

Este artículo ilustra varios aspectos:

 

Formulario de "talkback" para este artículo

Cada artículo tiene su propia página de "talkback". A través de esa página puedes enviar un comentario o consultar los comentarios de otros lectores
 Ir a la página de "talkback" 

Contactar con el equipo de LinuFocus
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Pinchar aquí para informar de algún problema o enviar comentarios a LinuxFocus
Información sobre la traducción:
fr -> -- Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr -> en Georges Tarbouriech
en -> es Iván Rojas

2001-04-16, generated by lfparser version 2.9