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.
© 2002 Antonio Villalón Huerta