Upload
others
View
4
Download
0
Embed Size (px)
Citation preview
ESTUDIOS DE INGENIERÍA
DE TELECOMUNICACIÓN
PROYECTO FIN DE CARRERA
Desarrollo de un entorno para la inclusión de
medidas de defensa frente a ataques de
denegación de servicio basadas en políticas de
gestión de colas.
REALIZADO POR:
Rafael Alejandro Rodríguez Gómez
DIRIGIDO POR:
Prof. Gabriel Maciá Fernández
DEPARTAMENTO:
Teoría de la Señal, Telemática y Comunicaciones
Granada
Septiembre, 2008
Desarrollo de un entorno para la inclusión de medidas
de defensa frente a ataques de denegación de servicio
basadas en políticas de gestión de colas.
Rodríguez Gómez, Rafael Alejandro
PALABRAS CLAVE: seguridad, denegación de servicio, gestión de colas,
conexión TCP.
Resumen
En este proyecto se ha estudiado la problemática de los ataques de
denegación de servicio y se ha propuesto como solución el uso de políticas de
gestión de colas en los buffers del establecimiento de la conexión para el
protocolo TCP. Con el fin de posibilitar la labor de implementación de estas
políticas de gestión de colas en un sistema real, se ha modificado la estructura
del sistema operativo Linux proporcionando un marco de trabajo que permite
de forma flexible y simplificada, la introducción del código que implementa
dichas políticas.
Se ha realizado un profundo análisis del código fuente de la sección del
núcleo de Linux que implementa el establecimiento de la conexión para el
protocolo TCP y para determinar los lugares en los que se aplican las
modificaciones en el núcleo.
Se ha propuesto una política de colas simple y se ha mostrado que su
implementación es factible en este núcleo modificado.
Por último se han realizado una serie de pruebas con dicha política de
gestión de colas. A la luz de los resultados obtenidos se ha comprobado que la
política implementada dificulta los efectos de los ataques DoS.
Development of a framework for the inclusion of denial of service
defense measures based on queue management policies
Rodríguez Gómez, Rafael Alejandro
KEYWORDS: security, denial of service, queue management, TCP
connections.
Abstract
This thesis is focused on the problem of denial of service (DoS) attacks. For
this problem, a solution based on the use of TCP establishment queue
management policies has been proposed. To ease the implementation of
these management policies in a real system, the structure of a Linux kernel
has been modified with the purpose of providing a framework aimed at
allowing the insertion, in a flexible and simplified way, of the code that
implements the policies.
A deep analysis of the Linux kernel source code that implements the
connection establishment for the TCP protocol has been carried out. This
analysis has allowed to choose the right positions in the kernel code to locate
the modifications proposed in this thesis.
Additionally, a simplified queue management policy has also been proposed
and implemented, and it has been shown that this implementation is feasible
and easy in the modified kernel.
Finally, a set of tests using the implemented policy has been carried out.
The results obtained show that the policy improves the behavior of the server
that is under DoS condition.
D. Gabriel Maciá Fernández
Profesor del departamento de Teoría de la Señal, Telemática y
Comunicaciones de la universidad de Granada, como director del Proyecto Fin
de Carrera de D. Rafael Alejandro Rodríguez Gómez.
Informa:
que el presente trabajo titulado:
Desarrollo de un entorno para la inclusión de medidas de defensa frente
a ataques de denegación de servicio basadas en políticas de gestión de
colas.
Ha sido realizado por el mencionado alumno bajo nuestra supervisión y con
esta firma autorizo su presentación.
Granada, 14 de Septiembre de 2008
Firma:
Gabriel Maciá Fernández
Los abajo firmantes autorizan a que la presente copia de Proyecto Fin de
Carrera se ubique en la Biblioteca del Centro y/o departamento para ser
libremente consultada por las personas que lo deseen.
Nombre: Rafael Al. Rodríguez Gómez Nombre: Gabriel Maciá Fernández
DNI: DNI:
Firma: Firma:
Granada, 14 de Septiembre de 2008
Agradecimientos
La sonrisa es más barata que la luz e ilumina más
(Roberto Pettinato)
Este trabajo representa la finalización de una fase muy importante en mi
vida y aunque “gracias” a él he tenido más de un quebradero de cabeza ahora
observando el resultado me recorre un escalofrío desde los pies a la cabeza.
No sé si se debe a la alegría que me da el haberlo terminado o tal vez a que
en realidad pienso que he realizado un trabajo digno.
Si me siento orgulloso de este proyecto se lo debo en primer lugar a un
grandioso docente e investigador Gabriel Maciá, mi tutor, aunque ninguna de
estas virtudes es la que hace que lo admire realmente. Esta admiración se
deriva de la atención extrema que ha tenido y sigue teniendo conmigo, de
corazón gracias.
También se lo debo por supuesto a mi familia, papá, mamá y Patri, si
alguien ha tenido que sufrir el lado oscuro que tiene la realización de un
proyecto fin de carrera, esos habéis sido vosotros. Habéis aguantado todas
mis frustraciones: tras no comprender cómo en una tarde de trabajo no había
sido capaz de escribir más de una página de introducción, cómo podía pasar
días buscando una solución que estaba realmente tan cerca. Gracias por
apoyarme, soportarme y sobretodo por alentarme con vuestro amor.
Gracias a todos los que, en este fase de mi vida, me habéis acompañado y
os deseo a todos que
Seáis felices.
Índice de contenido
►Índice de contenido xv
►Índice de figuras XIX
►Capítulo 1: Presentación del problema y de la solución propuesta 1
►1.1 Introducción......................................................................................1
►1.2 Ataques de denegación de servicio (DoS)........................................2
►1.3 Ataques de denegación de servicio contra servidores.....................4
►1.3.1 Modelo del servidor........................................................................5►1.3.2 Ejecución del ataque de denegación de servicio contra servidores
.......................................................................................................7
►1.4 Solución propuesta...........................................................................8
►1.4.1 Políticas de gestión de colas..........................................................9►1.4.2 Ejemplo de una política de gestión de colas................................10
►1.5 Objetivo y alcance del proyecto.....................................................14
►Capítulo 2: Análisis de la implementación de TCP/IP en Linux 17
►2.1 Establecimiento de la conexión de TCP en el núcleo......................19
►2.1.1 Acceso a la cola de conexiones completadas desde la aplicación......................................................................................................24
►2.1.2 Acceso a la cola de conexiones completadas desde la red..........29
►2.2 Estudio de las estructuras software implicadas en la conexión TCP.
.......................................................................................................33
►2.2.1 Estructuras socket para la conexión............................................34►2.2.2 Estructura de la cola de peticiones completadas.........................40
►2.3 Conclusiones del capítulo...............................................................43
►Capítulo 3: Implementación del entorno para la inclusión de medidas
de defensa en el núcleo de Linux 45
►3.1 Núcleo y módulos del núcleo..........................................................46
►3.1.1 Compilación del núcleo................................................................47►3.1.2 Módulos del núcleo......................................................................48►3.1.3 Módulos vs implementación directa en el núcleo........................52
►3.2 Módulos de seguridad de Linux (LSM) y garfios..............................53
►3.2.1 Dificultad hallada en la implementación del módulo...................53►3.2.2 Solución encontrada: Garfio.........................................................54►3.2.3 Proceso de implementación de un garfio.....................................56
►3.3 Implementación de una política de gestión de colas......................62
► Anexo 3.A: Códigos desarrollado para la generación del entorno.. 64
► Código del archivo hook.c............................................................64► Código del archivo rodgom.h.......................................................67► Código del archivo net/ipv4/tcp_ipv4.c........................................67► Código del archivo Makefile.........................................................69
►Capítulo 4: Evaluación de políticas de gestión de colas para la
defensa. 71
►4.1 Entorno de pruebas........................................................................72
►4.2 Modificaciones realizadas en el sistema.........................................75
►4.3 Resultados de las pruebas realizadas.............................................76
► Anexo 4.A Presentación del código del modelo cliente-servidor.. . .80
► Archivo servidor.cpp....................................................................80► Archivo cliente_bueno.cpp...........................................................82► Archivo cliente_malo.cpp.............................................................86
►Capítulo 5: Planificación del proyecto. 91
►5.1 Objetivos y tareas en las que se han dividido................................91
►5.2 Recursos utilizados.........................................................................93
►5.2.1 Recursos humanos.......................................................................93►5.2.2 Recursos materiales.....................................................................94
►5.3 Planificación temporal y diagrama de Gantt...................................95
►Capítulo 6: Conclusiones 97
►6.1 Conclusiones acerca del trabajo realizado.....................................97
►6.2 Líneas futuras.................................................................................98
►Bibliografía xcix
Índice de figuras
Índice de figurasFig. 1.1: Modelo del servidor tomada de [2].......................................................5
Fig. 1.2: Modelo de servidor con módulo de gestión..........................................9
Fig. 1.3: Diagrama de flujo de la política de gestión de colas..........................11
Fig. 2.1: Negociación en tres pasos..................................................................20
Fig. 2.2: Máquina de estados finitos para el establecimiento de conexión del
protocolo TCP...................................................................................................21
Fig. 2.3: Movimiento de las peticiones en las colas [4]....................................24
Fig. 2.4: Capas de la arquitectura de red TCP/IP..............................................25
Fig. 2.5: Extracción de una petición de la cola de conexiones completadas....25
Fig. 2.6: Camino seguido por una función que deba acceder al núcleo...........27
Fig. 2.7: Estructuras socket..............................................................................35
Fig. 2.8: Estructura de la cola de conexiones establecidas..............................40
Fig. 3.1: Esquema simplificado de la recompilación del núcleo.......................48
Fig. 3.2: Estructura básica de un módulo del núcleo........................................50
Fig. 3.3: Esquema de funcionamiento de un garfio..........................................55
Fig. 3.4: Funcionamiento de un garfio..............................................................57
Fig. 3.5: Estructura de rodgom.h......................................................................58
Fig. 3.6: Estructura de modificación del archivo del núcleo tcp_ipv4.c............59
Fig. 3.7: Estructura de creación del módulo del núcleo....................................60
Fig. 3.8: Diagrama de flujo de la política de gestión de colas ejemplo.............62
Fig. 4.1: Diagrama de flujo del cliente_bueno..................................................73
Fig. 4.2: Diagrama de flujo de cliente_malo.cpp..............................................74
Fig. 4.3: Representación de los resultados con backlog a 2 y sin aplicar la
política..............................................................................................................78
Fig. 4.4: Representación de los resultados con backlog a 2 y aplicando la
política..............................................................................................................78
XIX
Índice de figuras
Fig. 4.5: Representación de los resultados con backlog a 50 y sin política......79
Fig. 4.6: Representación de los resultados con backlog a 50 y aplicando la
política..............................................................................................................79
Fig. 5.1: Tabla resumen de la planificación temporal del proyecto..................95
Fig. 5.2: Diagrama de Gantt del proyecto........................................................96
XX
Capítulo 1: Presentación del problema y de la solución propuesta
1.1 Introducción
Desde el inicio de la humanidad el hombre siempre ha necesitado mantener
a buen recaudo sus bienes personales. Para esto, en primera instancia, se
utilizaron los soldados que vigilaban las arcas reales. Actualmente, son
algunos complejos sistemas de seguridad los tienen la labor que antaño
llevaron a cabo las espadas. En la actualidad es innegable la creciente
importancia que está adquiriendo el tema de la seguridad en redes de
comunicación y esto se debe a este mismo motivo.
Hoy por hoy se realizan multitud de operaciones a través de Internet. Entre
ellas se puede destacar el comercio electrónico que implica el tránsito de
dinero y esto conlleva la necesidad de garantizar ciertos niveles de seguridad
para que los clientes quieran realizar dichas operaciones provechosas para el
sistema. Como es sabido por todos, no todo el mundo es bienintencionado y
en ésta, una nueva era, aparecieron nuevos ataques de seguridad.
De entre estos ataques hay un tipo concreto, en el que se centra el
presente proyecto, que es el de los llamados ataques de denegación de
servicio o también DoS (de las siglas en inglés: Denial of Service).
En la primera parte del presente capítulo se describe el funcionamiento a
grandes rasgos de los ataques DoS para posteriormente centrarnos en un tipo
de ataque DoS: el ataque DoS contra servidores. Este tipo de ataque es de
especial relevancia en este proyecto debido a que la solución que se presenta
en él está especialmente diseñada para este tipo de ataques. En la segunda
1
Capítulo 1: Presentación del problema y de la solución propuesta
sección de este capítulo se da forma a la solución que, frente a la
problemática presentada en el primer punto, se ha estimado más oportuna. Se
ve necesario en esta sección exponer las bases teóricas sobre las que se
razonará en lo sucesivo.
Y por último, se resumen los objetivos de este proyecto con la intención de
determinar con claridad qué se pretende conseguir a lo largo del resto de este
documento.
1.2 Ataques de denegación de servicio (DoS)
El ataque DoS, en una red de comunicación, tiene como objetivo eliminar o
reducir la disponibilidad de un determinado activo mediante la ejecución de
determinadas acciones maliciosas dirigidas a la fuente de información, al
canal de comunicación o a ambos. Este tipo de ataques impide o inhibe el uso
normal o la gestión de recursos de comunicaciones.
La mayoría de los ataques a la seguridad de redes de comunicación tratan
de penetrar en un sistema y de este modo poder acceder a información
privada como pueden ser contraseñas de correo electrónico, información de
una investigación aún no patentada, número de cuenta bancaria, etc. En el
caso de los ataques DoS el objetivo es diferente; en este caso el ataque
consiste en evitar la ejecución de una actividad legítima, como puede ser
navegar por Internet, realizar un pedido a una tienda online o incluso transferir
dinero de una cuenta bancaria. Este tipo de ataques se consigue mediante el
envío de ciertos mensajes hacia uno de los destinatarios o el propio canal de
comunicación de modo que se impida de forma total o parcial el acceso al
servicio ofertado.
Existen dos métodos fundamentales para la realización de un ataque DoS:
la explotación de una vulnerabilidad (ataques de vulnerabilidad) y la
inundación con mensajes de apariencia legítima (ataques de inundación).
✗ Los ataques de vulnerabilidad consisten en enviar, a un equipo
remoto, una serie de mensajes construidos de manera que sean capaces
de aprovecharse de una vulnerabilidad existente en el equipo,
previamente conocida y estudiada. Estos mensajes suelen provocar el
paso por un estado del programa que su creador no había contemplado,
2
1.2 Ataques de denegación de servicio (DoS)
de modo que generen un fallo como puede ser un bucle infinito en la
ejecución del programa, su parada o incluso el reinicio de la máquina
completa, etc.
✗ Los ataques de inundación consisten en el envío de un elevado número
de mensajes a un sistema, de modo que este proceso acaba por consumir
determinados recursos críticos del mismo, como pueden ser el tiempo de
CPU, el ancho de banda de la red del sistema atacado, etc. Como
consecuencia, la disponibilidad del sistema se reduce o desaparece por
completo.
Si bien los ataques de vulnerabilidad han tenido mucha importancia en un
cercano pasado, la mejora de los procesos de elaboración de software ha
reducido considerablemente su aparición. Sin embargo, han ido tomando más
importancia los ataques de inundación. Los ataques de inundación se basan en
la hipótesis de que el atacante dispone de unos recursos muy abundantes
para ejecutar el ataque. Ahora bien, puede suceder que el sistema atacado
tenga unos recursos abundantes y de este modo el ataque, desde una sola
máquina, sea ineficaz. El atacante utiliza entonces una estrategia diferente.
Debe conseguir el control de un conjunto de máquinas, llamadas agentes o
zombies, y sincronizarlas para conseguir un ataque masivo que consiga agotar
estos abundantes recursos. Los ataques que siguen esta estrategia se
denominan ataques distribuidos de denegación de servicio o DDoS [9], [10],
[11] (de las siglas en inglés: Distributed Denial of Service). Aunque para
ejecutar un DDoS se precisa un gran número de agentes el problema es la
facilidad con que los atacantes obtienen el control de un buen número de
ellos. Esto se debe a que un gran número de los usuarios de Internet no
poseen las nociones básicas de seguridad necesarias para evitar que su
máquina sea controlada por el atacante.
Otro dato que hace a los ataques DoS especialmente complejos de tratar es
que pueden utilizar tráfico similar o idéntico al legítimo, pero en cantidades
inaceptables para el sistema. Esto hace que sea muy complejo tomar medidas
contra este tipo de ataques sin que los usuarios legítimos se vean afectados.
Para mitigar este y otros problemas de seguridad en las redes de
comunicación se diseñaron unos sistemas destinados a proteger las redes de
los efectos nocivos de estos ataques. Estos dispositivos se denominan
3
Capítulo 1: Presentación del problema y de la solución propuesta
sistemas de detección de intrusiones o IDS [12], [13] (del inglés: Intrusion
Detection System). Un IDS detecta tráfico malicioso o proveniente de un
atacante mediante la detección de anomalías probabilísticas en el tráfico de
una red de comunicaciones. Estos sistemas no tienen demasiado problema en
detectar un ataque DoS de alta tasa debido a que el tráfico generado por el
atacante es extremadamente superior al de un usuario común.
Para evitar la detección por parte de los IDS, además de para poder atacar
máquinas sin necesidad de un ordenador con unos recursos abundantes, los
atacantes diseñaron el ataque DoS a baja tasa. Este tipo de ataques DoS
utiliza la técnica de inundación pero con la gran diferencia de que no
necesitan una alta tasa de tráfico para saturar a la víctima del ataque.
Precisamente es esta tasa baja de tráfico la que convierte a este tipo de
ataques prácticamente en indetectables para los IDS.
El ataque de baja tasa es aquel que consigue saturar a la víctima del ataque
con una tasa de tráfico suficientemente baja para que no pueda ser detectado
por los sistemas de detección de intrusos que se basan en la observación de
anomalías estadísticas debidas a tasas de tráfico más elevadas de lo normal.
Esto tipo de ataques se basa en la explotación de una vulnerabilidad que
permite denegar el servicio a los clientes aún con baja tasa. Esto implica que
estos ataques pueden ser clasificados como de intrusión o de vulnerabilidad.
En un corto período de tiempo han sido desarrollados una serie de ataques de
este tipo, como son: el ataque Shrew [5], los ataques RoQ [6-8] (de reducción
de la calidad o en inglés Reduction of Quality), el ataque a baja tasa contra
servidores [1] [2], etc. En este proyecto se pretende abordar el problema de
seguridad generado por los ataques de DoS pero principalmente los generados
contra aplicaciones.
1.3 Ataques de denegación de servicio contra servidores
En este apartado se descubrirán los mecanismos para los ataques DoS contra
servidores. Para ello se propone en primer lugar un modelo de servidor para,
a continuación, ilustrar el proceso seguido por el atacante para llevar a cabo
este tipo de ataques.
4
1.3 Ataques de denegación de servicio contra servidores
1.3.1 Modelo del servidor
En el ataque contra servidores, el modelo del servidor consiste en un
módulo que recibe mensajes o peticiones, los procesa y emite las respuestas
que sean necesarias. Los mensajes recibidos en este servidor pueden
proceder bien de los usuarios legítimos, bien de los usuarios
malintencionados. El servidor tiene dos tipos de respuestas: los eventos OK y
MO.
✗ Los eventos OK representan la respuesta que da el servidor a una
petición determinada de un cliente en un estado del sistema normal.
✗ Los eventos MO (de las siglas en inglés: Message Overflow) son la
respuesta que da el servidor a los clientes en un estado de sobrecarga de
las colas internas o de la memoria de la aplicación.
El modelo genérico viene descrito en la Fig. 1.1 y consta de las siguientes
secciones: un balanceador de carga1 y M máquinas servidoras, cada una de
ellas con una cola de servicio y un módulo de servicio.
1Balanceador de carga es un dispositivo encargado de distribuir el tráfico entrante entre sus salidas según una política de distribución.
5
Fig. 1.1: Modelo del servidor tomada de [2]
Cola de servicio 1
Módulo de servicio 1
MO
OK
Cola de servicio
MMódulo de servicio M
MO
OK
∙∙∙ Máquina M
Máquina 1
Balanceador de carga
Capítulo 1: Presentación del problema y de la solución propuesta
En la Fig. 1.1 las flechas indican en el camino que siguen las peticiones en
el modelo. La cola de servicio es el primer elemento que una petición
encuentra tras salir del balanceador de carga. Esta cola de servicio no es más
que una cola de longitud finita en la que las peticiones se almacenan hasta ser
requeridas por el modulo de servicio. En caso de estar completa la cola el
evento devuelto será MO y la petición se descartará. En los sistemas UNIX esta
cola esta implementada a través de las estructuras del tipo socket2, que
almacenan las peticiones entrantes para una determinada aplicación en un
puerto dado.
El siguiente elemento al que se dirige una petición tras haber sido
almacenada en la cola de servicio es el módulo de servicio. En éste es
procesada y se envía al cliente la consiguiente respuesta (evento OK).
El paso para la cola de servicio al módulo de servicio se realiza cuando el
módulo de servicio queda libre para aceptar una nueva petición. En el sistema
UNIX, este proceso es llevado a cabo mediante la implementación de la
llamada al sistema3 generada por la función accept, cuyo efecto es tomar una
conexión encolada y pasarla a la aplicación para su posterior manipulación.
En resumen, el camino que sigue una conexión entrante al servidor
presentado en la Fig. 1.1 es el siguiente:
1 La petición llega al balanceador de carga, que elige según su política
interna la máquina de entre las M posibles a la que la enviará,
exceptuando aquellas máquinas cuya cola se encuentre ocupada
completamente. En el caso de que todas las colas se hallen en esta
situación el balanceador decidirá aleatoriamente entre una ya que el
resultado será igualmente la devolución de un evento MO (de
sobrecarga).
2 La petición es encolada en la cola de servicio de la máquina elegida por el
balanceador de carga siempre y cuando quede alguna posición libre. En
caso contrario la petición se descarta y se envía un evento MO.
2Socket: es el elemento que permite el flujo de información entre dos puntos.3Llamada al sistema: mecanismo usado por una aplicación para solicitar un servicio al sistema operativo.
6
1.3 Ataques de denegación de servicio contra servidores
3 Transcurrido el tiempo necesario en la cola, la petición pasa al módulo de
servicio en el que será procesada y tras lo cual se responderá al cliente
que inició la petición con un evento OK.
Se define ahora la estrategia seguida por los ataques de denegación a baja
tasa para, tras su conocimiento y estudio, poder así aportar una solución.
1.3.2 Ejecución del ataque de denegación de servicio contra servidores
Ya que el problema principal al que se pretende dar solución en este
documento es el de los ataques DoS, pero de una manera especial los
realizados a aplicaciones, es necesario estudiar este tipo de ataques para
conocer el problema profundamente. El objetivo básico de este tipo de
ataques consiste en evitar la disponibilidad del servidor, haciendo que el
tiempo en el que queda un espacio libre en alguna cola de servicio tienda a
cero. De este modo, la probabilidad de que los clientes realicen una conexión
en ese breve espacio de tiempo es mínima. El atacante estará consiguiendo
así que el servidor únicamente sirva sus peticiones y conseguirá, como
consecuencia de esto, denegar el servicio a todos los demás clientes.
En esta situación el servidor continúa trabajando con total normalidad
sirviendo los mensajes que tiene encolados, con la única salvedad de que
todos los mensajes que sirve son del mismo cliente: el atacante. Este
funcionamiento del servidor, en régimen de normalidad, hace aún más
compleja la detección de este tipo de ataques.
El proceso que sigue el atacante para realizar un ataque DoS a baja tasa
puede resumirse en los siguientes dos pasos:
1 Saturación de las colas de servicio. Este paso consiste en llenar todas
las colas de servicio de peticiones del atacante. La manera mediante la
que se realiza este proceso no será tratada en este proyecto y se dará por
supuesto que el atacante consigue este estado en las colas.
2 Captura del máximo número de posiciones posibles. La captura es el
acto de introducir una petición en el momento en el que se genera una
respuesta y por tanto, se libera una posición de alguna cola de servicio.
7
Capítulo 1: Presentación del problema y de la solución propuesta
La captura es la que permite al atacante mantener el estado de
saturación de las colas de servicio a lo largo de un período de tiempo.
En las colas de servicio se libera una posición siempre y cuando el modulo
de servicio termine de procesar una petición y envíe el mensaje de respuesta.
Justo tras esta acción, el módulo de servicio que ha respondido extraerá la
siguiente conexión de su cola de servicio y generará una posición libre en ella.
A la hora de capturar el máximo número de posiciones posibles, en la cola
de servicio, el atacante puede escoger dos estrategias:
✗ Enviar una tasa de tráfico suficientemente alta de modo que la
probabilidad de capturar todas las posiciones tienda a uno. Esta estrategia
es la utilizada por los ataques DoS de tasa alta pero tiene el inconveniente
de que son relativamente sencillos de detectar por los sistemas de
seguridad.
✗ Explotar una vulnerabilidad que le permita averiguar, de algún modo, el
momento en el que alguno de los módulos de servicio llega al término del
procesamiento de una conexión y envía su respuesta. En estos instantes el
atacante envía la siguiente conexión y será también alta la probabilidad
de que ocupe el espacio libre que aparecerá en la cola de servicio. Los
detalles acerca del modo en que el atacante averigua dichos instantes
pueden consultarse en [1] [2]. En este caso el ataque DoS se hace a baja
tasa.
1.4 Solución propuesta
Conocida la problemática de los ataques DoS contra servidores y el modelo
del servidor, se plantea en este proyecto abordar una solución posible que
ayude a reducir los efectos de este tipo de ataques. La solución elegida se
basa en aplicar políticas de gestión de colas para mitigar los efectos del
ataque de denegación de servicio a baja tasa.
En este apartado se definirá primeramente qué es una política de gestión
de colas para poder plantear con base teórica la solución que se presenta.
También se explicará el funcionamiento general de estas políticas y se
concretará el funcionamiento general de las mismas en unos ejemplos que
pretenden clarificar su funcionamiento.
8
1.4 Solución propuesta
Es importante aclarar en este punto que el objeto de este proyecto no es
desarrollar una solución completa al problema de los ataques DoS mediante el
desarrollo de políticas de gestión de colas. El objeto del proyecto es el
desarrollo de un entorno que permita la inclusión de cualquier política de
gestión de colas desarrollada como solución al problema. Por esta misma
razón en este apartado no se presentará la mejor política de gestión de colas
posible sino una que nos permita demostrar el funcionamiento de nuestro
entorno.
1.4.1 Políticas de gestión de colas
Se denomina política de gestión de colas a aquel mecanismo que realiza
un manejo de las colas de servicio con el fin de mitigar los efectos de los
ataques DoS. La política de gestión de colas consiste en descartar peticiones
de la cola de servicio ante la aparición de determinados estados en la misma y
eligiendo las peticiones según una estrategia determinada. Esta actuación
pretende conseguir justicia entre los usuarios evitando por tanto, que los
clientes maliciosos capturen más.
En el modelo de la Fig. 1.2 tan sólo se ilustra una de las M máquinas que
contiene el módulo servidor inicial. En este caso las M-1 máquinas restantes y
el balanceador de carga han sido obviadas por simplicidad.
9
Fig. 1.2: Modelo de servidor con módulo de gestión
Cola de servicio 1
Módulo deservicio 1
MO
OK
Máquina 1
Módulo degestión 1
Capítulo 1: Presentación del problema y de la solución propuesta
En esta ocasión se ha incluido en el modelo del servidor un módulo
denominado módulo de gestión. Este módulo es el encargado de aplicar la
política de gestión de colas. Por tanto, se añade también un evento más a las
respuestas del servidor. Anteriormente se han descrito MO y OK y el evento
que se añade ahora es RCD (de las siglas en inglés Request Connection Drop).
Este evento se produce en el servidor cuando el módulo de gestión determina
que una conexión de las encoladas debe ser extraída de la cola y desechada.
El evento RCD podría dar como resultado el envío, al cliente correspondiente,
de un mensaje RCD. El envío de este mensaje es opcional y dependerá de la
política de gestión de colas el solicitar o no su envío. Es por esta razón por la
que la flecha que, en la Fig. 1.2, indica la expulsión de una petición encolada
con un envío de un RCD es punteada y no continua.
En resumen el funcionamiento del módulo de gestión consiste en
monitorizar la cola de servicio, de ahí la unión de éste con la cola. Tras la
recogida de cierta información de interés sobre el estado de la cola, el módulo
de gestión la procesa y actúa en consecuencia a su política interna.
La política interna que rige el módulo de gestión es la clave del éxito de
esta solución. Una buena política de gestión de colas será aquella que extraiga
de la cola el mayor número de peticiones malintencionadas del atacante,
eliminando en el camino el menor número de peticiones de los clientes
legítimos. Si se es capaz de conseguir que los clientes puedan seguir
realizando conexiones con normalidad se habrán logrado mitigar los efectos
del ataque DoS.
1.4.2 Ejemplo de una política de gestión de colas
El desarrollo de una política de gestión de colas eficiente para este tipo de
entornos es una tarea compleja y actualmente en investigación, lo que implica
que no se ha llegado a obtener una que solucione completamente la
problemática de los ataques DoS.
El motivo por el que se presenta un ejemplo de una política de gestión de
colas es el de aclarar el concepto definido en el apartado anterior, de modo
que se ilustren los detalles acerca del funcionamiento del módulo de gestión
en el servidor.
10
1.4 Solución propuesta
Se podrían elegir muchas alternativas para las políticas. A modo de ejemplo,
se podría descartar una petición al azar de la cola al superar la ocupación de
ésta un umbral determinado, por ejemplo un porcentaje del tamaño máximo.
También se podrían descartar todas las peticiones existentes en la cola al
sobrepasar dicho umbral. Otra alternativa más compleja sería realizar un
análisis de la información de las peticiones encoladas determinando cuáles
son malintencionadas y descartándolas.
Se ha visto necesario implementar una política de gestión de colas
mediante la que poder probar el funcionamiento del entorno para la inclusión
de medidas de defensa frente a ataques DoS basadas en políticas de gestión
de colas. Por esta razón se ha elegido una política ejemplo sencilla en cuanto a
implementación a la par que eficaz en su funcionamiento.
Descripción del ejemplo:
Para realizar de una manera más sencilla la ilustración de este ejemplo, se
presenta seguidamente la notación que se va a utililzar. El número de
peticiones encoladas en la cola de servicio será representado por n_act, el
número de posiciones totales de la cola por n_max, el umbral de la cola a partir
del cual se aplicará una determinada estrategia de descarte por n_umb y N
representará el número de posiciones que se descartarán de la cola.
11
Fig. 1.3: Diagrama de flujo de la política de
gestión de colas
n_act>n_umb
Descartar Nal azar
Sí No hacer nada
Recogida del tamañode la cola (Monitorizar)
NoSi
Capítulo 1: Presentación del problema y de la solución propuesta
La política elegida se basa en monitorizar la ocupación de la cola y cuando
ésta supere el umbral establecido, es decir, cuando n_act sea mayor que
n_umb, elegir N de las peticiones encoladas al azar y descartarlas. El umbral se
determina como una proporción del tamaño máximo de la cola. El proceso
seguido cuando se aplica la política se puede ver gráficamente en la Fig. 1.3,
en la que se presenta el diagrama de flujo de la misma.
Nótese que, en esta estrategia, la monitorización del estado de la cola se
basa exclusivamente en la observación del número de posiciones ocupadas en
cada instante. Sin embargo, podrían existir otras alternativas, como por
ejemplo, monitorizar el número de posiciones de un mismo usuario, el número
de posiciones de diferentes usuarios, etc.
Se realiza la explicación anterior ilustradándola en pseudo-código para ver
este proceso de una manera secuencial y simplificada.
repetir siempre{si n_act es mayor que n_umb{
elegir al azar N peticiones de entre las n_act;descartar la petición elegida al azar;
}}
Mitigación del efecto del ataque DoS
Hasta ahora se ha descrito un ejemplo de una política de gestión de colas
seguidamente, se pretende demostrar teóricamente cómo esta política influye
de una manera clara en la mitigación del efecto nocivo del ataque de DoS, es
decir, si tras la aplicación de la política de gestión de colas propuesta un
cliente legítimo puede acceder a los recursos del servidor con mayor
disponibilidad. Este análisis se llevará a cabo con mayor profundidad en el
Capítulo 4. Aún así ahora se explicarán las nociones básicas sin introducir
excesivos detalles.
Como se ha visto anteriormente el ataque DoS se basa en dos procesos
principalmente:
✗ Mantener las colas siempre saturadas, sin posibilidad de que nuevos
clientes puedan hacer conexiones.
✗ Capturar todas las posiciones que se habiliten en la cola mediante alta
tasa o baja tasa prediciendo los instantes en que éstas se generan.
12
1.4 Solución propuesta
La política descrita con anterioridad ataca a ambos pilares: comenzando con
la saturación de las colas, esta tarea se hace mucho más compleja para el
atacante ya que la política determina un tamaño umbral a partir del cual
cuando el número de peticiones en cola lo supera un cierto número de
peticiones de las encoladas son expulsadas. De esta manera se dificulta la
tarea de mantener las colas siempre saturadas, ya que el servidor libera
posiciones siempre que se supere el umbral.
Esta política también dificulta la captura de posiciones en la cola por parte
del atacante, ya que aunque éste pueda predecir los instantes en que el
servidor envía respuestas a los clientes y, por tanto, las posiciones de la cola
liberadas de una manera normal por el servidor, no sucede lo mismo para las
peticiones que son expulsadas por la política de gestión de colas ya que esta
expulsión genera una posición libre en la cola, pero no del modo que el
atacante espera. Esto implica que no afectaría la vulnerabilidad que está
atacando el cliente malicioso gracias a la cual puede conocer la liberación de
una posición de la cola y adelantarse a los clientes legítimos en la captura de
posiciones.
En resumen, el que haya un umbral permite a los clientes legítimos ocupar
una posición de la cola de servicio ya que ésta no se encuentra saturada.
Posteriormente la política de gestión de colas liberará una petición si se ha
superado el umbral pero al elegirse aleatoriamente podrá ser tanto del
atacante como del cliente legítimo. El otro camino para que el cliente legítimo
consiga realizar una conexión es que la política de gestión de colas libere una
posición de la cola y ésta sea ocupada por el cliente. Como se ha dicho la
liberación de esta posición de la cola es invisible al atacante de modo que
genera una posición que un cliente no tendrá dificultad en ocupar.
Intuitivamente se prevé que esta sencilla política de gestión de colas es
capaz de hacerle frente a complejos mecanismos utilizados por los atacantes
para predecir el momento en el que una posición será liberada de la cola. En
los resultados mostrados en el Capítulo 4 se mostrará que, efectivamente, el
efecto del ataque DoS se reduce considerablemente.
En nuestros días existen líneas de investigación trabajando en el desarrollo
de políticas de gestión de colas para mitigar los efectos de ataques DoS. Cada
una de estas políticas pretende aportar una solución a los ataques contra los
13
Capítulo 1: Presentación del problema y de la solución propuesta
que esté diseñada. El problema será entonces determinar si funcionan como
se planteó en teoría. También será importante determinar cual de ellas es más
eficaz. En conclusión todas ellas necesitarán evaluar su buen funcionamiento y
ser implementadas en un entorno real. Esta necesidad existente y creciente es
la que este proyecto pretende cubrir, desarrollando un marco que permita de
forma flexible implementar cualquier política de gestión de colas desarrolladas
con el objetivo de mitigar los efectos de los ataques DoS. Este entorno podrá
ser utilizado en beneficio de toda aquella investigación que lo requiera.
1.5 Objetivo y alcance del proyecto
El objetivo principal de este proyecto es desarrollar un entorno para la
inclusión de medidas de defensa frente a ataques DoS basadas en políticas de
gestión de colas. Se ha intentado que el entorno sea:
✗ Flexible de modo que quepan en él cualquier tipo de implementaciones
de políticas de gestión de colas.
✗ Proporciona una interfaz clara y cerrada para introducir nuevo código y
modificaciones.
Para presentar este entorno de la manera más sencilla posible se ha
dividido la información en los siguientes capítulos:
✗ En el segundo capítulo se realiza un análisis del núcleo de Linux para
determinar y estudiar las estructuras que intervendrán en la gestión de la
cola de servicio (del modelo del servidor). Todo este análisis está
encaminado a determinar las posiciones en el núcleo en las que hay que
insertar el nuevo código para la implementación del entorno objetivo. Para
de este modo llegar a determinar la ubicación en el núcleo del entorno a
desarrollar.
✗ El capítulo tercero muestra el proceso seguido para realizar la
modificación en el núcleo de Linux encaminada al desarrollo del entorno
para la implementación de políticas. Se indicarán alternativas de diseño
para de realizar dicha modificación razonándose el porqué de la elección
realizada. Se mostrará, en este mismo capítulo, una descripción detallada
de la modificación realizada para posibilitar la inclusión de las políticas de
gestión de colas.
14
1.5 Objetivo y alcance del proyecto
✗ El cuarto capítulo está destinado a probar el funcionamiento del entorno
desarrollado mediante la implementación de una política de gestión de
colas de prueba.
✗ En el quinto capítulo se indica la planificación temporal llevada a cabo
durante la ejecución del proyecto ilustrándola con un diagrama de Gantt.
También se describen en éste los recursos tanto humanos como
materiales que han sido necesarios para la consecución de los objetivos
planteados.
✗ El sexto capítulo es en el que se recogen las conclusiones finales que se
extraen de la realización de este proyecto. Se detallan, además, unas
líneas de posibles trabajos futuros tomando como base el entorno
desarrollado en este proyecto.
15
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
En este capítulo se estudia el modo en el que el núcleo del sistema
operativo Linux lleva a cabo el establecimiento de conexión para el protocolo
TCP. El estudio de esta sección del núcleo de Linux se ha visto necesario para
poder determinar el lugar en el que se implementará el entorno que posibilite
la inclusión de políticas de gestión de colas en este sistema. Los
conocimientos aquí presentados pueden ser útiles en el proceso de generación
de políticas de gestión de colas.
Se elige el estudio de la arquitectura de red TCP/IP por ser la más utilizada
en la actualidad y concretamente el de la conexión porque es en este proceso
en el que se añaden y extraen peticiones a las colas de servicio, como se las
llamaba en el capítulo anterior.
Otra elección realizada es la de trabajar con el núcleo del Sistema Operativo
Linux. El núcleo es el software encargado de posibilitar el acceso seguro al
hardware del ordenador o también es el que se encarga de gestionar los
recursos a través de llamadas al sistema. El núcleo también es el que decide
el tiempo que cada proceso tendrá el recurso deseado.
La razón por la que se propone la modificación del núcleo del Sistema
Operativo Linux se debe a que Linux es un sistema operativo de software libre.
El software libre es aquel que se distribuye sin coste alguno aparte de permitir
su modificación al usuario. El hecho de que se permita su modificación es el
que ha posibilitado que se disponga del código fuente del núcleo necesario
para su estudio en profundidad.
17
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Ahora que ya se conoce la problemática a la que se enfrenta este proyecto,
los ataques DoS y más concretamente los DoS a baja tasa, se está en
disposición de profundizar en el estudio del marco de pruebas experimentales
a desarrollar. Para comprender el funcionamiento de este marco de pruebas,
antes de especificar los detalles relativos a su ejecución, es necesario aportar
alguna información adicional.
En primer lugar, se ilustran una serie de conceptos básicos relativos al
proceso seguido para el establecimiento de conexiones en el protocolo TCP.
Posteriormente se concreta cómo se realiza este establecimiento de la
conexión en el núcleo del sistema operativo Linux, más específicamente en su
versión 2.6.24 por ser ésta la más actual en el momento de inicio del presente
proyecto. Por último, se detallan las estructuras de datos más importantes
cuyo conocimiento permitirá entender con mayor profundidad el
funcionamiento del núcleo de Linux en el establecimiento de conexión.
El capítulo está estructurado en dos partes, de la siguiente forma: En la
primera parte se describe de manera general cómo realiza la conexión el
protocolo TCP/IP incluyendo la descripción de las colas que se utilizan para
almacenar las conexiones. Estas colas son las equivalentes a la cola de
servicio en el módulo del servidor mostrado en la Fig. 1.1. Posteriormente se
describen detalladamente los dos procesos seguidos para acceder a la cola de
servicio, uno desde la red, momento en el que se añade la conexión a la cola y
otro desde el espacio de usuario, desde donde la aplicación servidora extrae
conexiones de la cola. Para ilustrar más claramente este hecho se muestra el
código de las funciones del núcleo que intervienen en este proceso.
En la segunda parte del capítulo se pasa de una descripción a alto nivel a
mostrar más detalladamente qué estructuras de datos intervienen en los
procesos antes mencionados. Se divide también en dos partes. En la primera
se describenlas estructuras socket que se utilizan en la conexión del protocolo
TCP, siendo un socket una abstracción software que permite el flujo de
información entre dos puntos. Un socket de Internet contiene básicamente las
direcciones de los participantes en la comunicación, el protocolo utilizado y el
número de puerto. En la segunda parte se muestra en detalle la estructura de
la implementación en Linux de la cola de conexiones establecidas, que es en
la que se centra este trabajo.
18
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Este capítulo pretende también servir de guía para el estudio del núcleo del
sistema operativo Linux. Como se ha dicho, se estudia en él el establecimiento
de la conexión del protocolo TCP. Pero el proceso que se ha seguido para su
estudio y la estructura son comunes a otras secciones del núcleo con lo que
puede ser de utilidad para aquél que pretenda enfrentarse al estudio de otra
sección.
2.1 Establecimiento de la conexión de TCP en el núcleo.
Para comprender cómo realiza el núcleo las conexiones del protocolo TCP es
necesario primero ilustrar cómo se realiza la conexión en general en dicho
protocolo.
El protocolo TCP es un protocolo orientado a conexión, lo que quiere decir
que requiere de la realización de una conexión para poder crear un flujo de
información entre los dos puntos finales de una comunicación. Este protocolo
se compone de tres fases: establecimiento de la conexión, transmisión de
datos y finalización de la conexión. La fase interesante para este proyecto es
la de establecimiento de la conexión que se detalla a continuación.
Esta primera fase se realiza mediante la llamada negociación en tres pasos
(o en inglés: three-way handshake). Consiste en una apertura activa del
cliente, que envía un segmento SYN al servidor dirigido a un determinado
puerto. Si el servidor está escuchando en este puerto y todo es correcto
responde enviando un segmento SYN+ACK al cliente y éste termina la fase de
establecimiento de la conexión respondiendo con un ACK.
En la Fig. 2.1 se puede observar gráficamente el proceso anteriormente
descrito. Además se ve que en los mensajes viaja información referente al
número de secuencia (identificada como seq). El número de secuencia es un
valor que cliente y servidor intercambian para que el protocolo pueda
asegurar que ningún mensaje se ha perdido y que todos han llegado en el
orden correcto. Por ejemplo, el cliente inicializa con un número de secuencia
al azar (x) y el servidor debe responder incrementando en uno este número de
secuencia (x+1) y enviando un nuevo número de secuencia (y) también
inicializado al azar. Por último el cliente responde incrementando el número de
secuencia que el servidor le envió (y+1). Este proceso de números de
19
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
secuencia pretende también dificultar una posible suplantación o spoofing,
esto es, que un usuario responda al servidor haciéndose pasar por otro cliente.
Si el atacante no conoce el número de secuencia que el servidor envió al
cliente legítimo no puede incrementarlo en uno estableciendo así la conexión
y suplantando al legítimo.
Es interesante indicar cuales son los estados por los que se pasa en el
proceso de establecimiento de la conexión para, de esta forma, poder
comprender más fácilmente las explicaciones futuras. La Fig. 2.2 representa la
máquina de estados finitos del protocolo TCP para el establecimiento de la
conexión. Esta máquina de estados finitos nos es más que una ilustración en la
que se resume gráficamente el comportamiento del protocolo en el
establecimiento.
El estado indica en qué situación se encuentra en ese momento la conexión.
Hay dos puntos de vista desde los que se puede observar una conexión. Uno
es el del cliente, que inicia activamente la comunicación, y otro es el del
servidor, que se mantiene a la escucha. De este modo, el cliente no tiene
porqué ver la conexión en el mismo estado en el que la ve el servidor.
20
Fig. 2.1: Negociación en tres pasos
2.1 Establecimiento de la conexión de TCP en el núcleo.
Se comienza estudiando el proceso desde el punto de vista del cliente:
1 Comienza en el estado CLOSED (cerrado). Realiza una apertura activa
enviando un SYN y pasa al estado SYN_SENT (syn_enviado).
2 Tras recibir el SYN+ACK del servidor envía un ACK pasando a estado
ESTABLISHED (establecida).
Se ve ahora este mismo proceso desde el punto de vista del servidor:
1 Comienza con una apertura pasiva pasando del estado CLOSED (cerrado)
al estado LISTEN (escucha).
2 Tras recibir SYN y enviar SYN+ACK pasa al estado SYN_RCVD
(syn_recibido).
3 Si recibe el ACK correcto proveniente del cliente el estado de la conexión
será ESTABLISHED (establecida).
21
Fig. 2.2: Máquina de estados finitos para el establecimiento de conexión del protocolo TCP
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Una vez que ambos extremos están en el estado ESTABLECIDA pueden
transferir información por la conexión establecida entre ellos.
Concepto de socket
Es obvio que para que dos aplicaciones puedan comunicarse entre sí es
necesario que se cumplan ciertos requisitos:
✗ Que una aplicación sea capaz de localizar a la otra.
✗ Que ambos sean capaces de intercambiarse cualquier secuencia de
octetos, es decir, datos relevantes a su finalidad.
Para ello son necesarios tres recursos que originan el concepto de socket:
• Un protocolo de comunicaciones, que permite el intercambio de octetos.
• Una dirección del protocolo de red (dirección IP, cuando consideramos el
protocolo IP), que identifica un ordenador.
• Un número de puerto, que identifica a una aplicación dentro de un
ordenador.
Los sockets permiten implementar una arquitectura cliente-servidor. La
comunicación ha de ser iniciada por uno de los programas, que se denomina
programa cliente. El segundo programa espera a que el otro inicie la
comunicación, por este motivo se denomina programa servidor.
Un socket es una abstracción software en la máquina cliente y en la
máquina servidora, que sirve, en última instancia, para que el programa
servidor y el cliente lean y escriban la información. Esta información será la
transmitida por las diferentes capas de red.
Colas de servicio existentes en Linux
Hasta ahora se ha visto el proceso de establecimiento de la conexión en el
protocolo TCP. Un servidor recibirá peticiones de múltiples clientes que
pasarán por estados diferentes. El servidor necesita un tiempo para procesar
estas peticiones y, por tanto, no podrá atender a todos los clientes a la vez.
Para solventar esta situación, el módulo servidor (Fig. 1.1) contiene colas de
servicio en las que se almacenarán estas peticiones. La implementación de la
cola de servicio y su modo de funcionamiento dependen del sistema operativo
22
2.1 Establecimiento de la conexión de TCP en el núcleo.
considerado e incluso del núcleo concreto. Como se dijo en la introducción, en
este proyecto se considera el núcleo del sistema operativo Linux en su versión
más actual, en el inicio de este proyecto, la v. 2.6.24.
En el caso del núcleo 2.6.24 de Linux la implementación de la cola de
servicio para conexiones TCP se divide en dos colas diferentes [3]: una para
las peticiones SYN recibidas del cliente y otra para las conexiones
completadas:
1 Cola de conexiones incompletas. Tiene una entrada por cada cliente
que ha enviado un SYN. Para estas conexiones el servidor se encuentra en
el estado SYN_RCVD.
2 Cola de conexiones completadas. En ésta se encuentra una entrada
por cada cliente que ha completado la negociación en tres pasos, con lo
que se encuentran en el estado ESTABLECIDA.
Cuando el servidor recibe un SYN correcto almacena esta petición en la cola
de conexiones incompletas, enviando posteriormente un SYN+ACK. Tras la
recepción del correcto ACK de respuesta, pasa esta petición de la cola de
peticiones incompletas a la última posición de la cola de peticiones
completadas, modificando su estado a ESTABLECIDA. Es decir, al completarse
correctamente el establecimiento de conexión se introduce la petición en la
cola de completadas.
La manera en la que se extraen conexiones de la cola de peticiones
completadas es recibiendo la llamada desde la aplicación de la función accept
y pasando al espacio de usuario la primera petición en la cola de peticiones
completadas. De esto se deriva que la cola de peticiones completadas sigue
una distribución FIFO (First Input First Output), lo que quiere decir que la
primera conexión en entrar a la cola será la primera conexión en ser servida.
En la Fig. 2.4 podemos ver este proceso gráficamente.
Estas colas, como en el caso del modelo del servidor descrito en el Capítulo
1, tienen un tamaño finito. El tamaño de la cola de conexiones completadas,
en el caso del núcleo de Linux, viene determinada por la variable backlog. Es
siempre una unidad superior a ésta.
23
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Si bien la cola de conexiones incompletas afecta al comportamiento de la
entrada de las conexiones a la aplicación, la implementación que realmente
corresponde a la cola de servicio del modelo del servidor es la de conexiones
completadas. Por esta razón, interesa el comportamiento de esta cola, a la
que se accede de dos formas:
✗ Se añaden conexiones a la cola de conexiones completadas al pasar del
estado SYN_RCVD al estado ESTABLISHED y esto se produce al recibir el
ACK como respuesta final de la negociación en tres pasos y siempre y
cuando quedase espacio en esa cola (acceso desde la red).
✗ Se extraen peticiones cuando la aplicación llama a la función accept. En
este caso la primera conexión de la cola pasa al espacio de usuario
(acceso desde la aplicación).
Existen, por tanto, dos posibilidades para modificar la cola de conexiones
completadas que se pueden describir, también, desde la visión de capas de la
arquitectura de red TCP/IP:
24
Fig. 2.3: Movimiento de las peticiones en las colas [4]
Servidor
Negociación en tres pasos completada
Cola de conexionescompletadas.Estado: ESTABLISHED
Cola de conexionesincompletas.Estado: SYN_RCVD
Llegada de un SYN
2.1 Establecimiento de la conexión de TCP en el núcleo.
✗ 1º Desde la capa de aplicación:
La aplicación le pide a la capa de Transporte
que acepte la conexión de la cola de
conexiones completadas.
✗ 2º Desde la capa de red:
A través de la capa Física llega el ACK que
finaliza la negociación en tres pasos y consigue
pasar la petición a la cola de conexiones
completadas.
A continuación se ilustran los detalles de estos dos caminos.
2.1.1 Acceso a la cola de conexiones completadas desde la aplicación.
En este punto se ilustra la sección de código en el núcleo destinada a
extraer una petición de la cola de peticiones completadas. Como se ha
explicado anteriormente el acceso a la cola de peticiones completadas desde
la aplicación se realiza para extraer de ésta la primera petición encolada y
pasarla al espacio de usuario (Fig. 2.5).
Para explicar el modo en el que esta operación es realizada en el núcleo, se
continúa el proceso de búsqueda que se realizó en su momento. Se adopta
esta estrategia por considerarse la más clara para el lector, de este modo
podrá seguir la cadena de razonamientos que en su momento fue necesario
desarrollar. Para aclarar esto del mejor modo posible se incluyen secciones de
código que confirman las suposiciones presentadas. Estas secciones vendrán
siempre delimitadas con dos líneas, una al inicio y otra al final. En el extremo
25
Fig. 2.5: Extracción de una petición de la cola de
conexiones completadas
Fig. 2.4: Capas de la
arquitectura de red TCP/IP
APLICACIÓN
TRANSPORTE
RED
ENLACE
Cola de conexionesestablecidas
socketaccept()
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
derecho de esta línea se indicará la ruta del archivo del que se está
extrayendo el código. El código contendrá el número de línea que ocupa en el
archivo origen al inicio de la misma. Si se pretende consultar el archivo
completo se puede descargar el código fuente del núcleo de Linux de la
página oficial http://www.kernel.org/ y recordar que la versión utilizada es la
2.6.24.
En este momento es importante determinar los diferentes modos de
ejecución que provee el sistema operativo Linux, entendiéndose por ello: la
manera en la que al proceso concreto que ocupa la CPU le está permitido
ejecutarse. En el sistema operativo Linux existen dos modos de ejecución:
1 Modo usuario: permite la ejecución de instrucciones que no afectan a
otros procesos. Se ejecuta en el espacio de usuario.
2 Modo privilegiado: permite la ejecución de todas las instrucciones. Se
ejecuta en el espacio de núcleo.
Para localizar el código en el que se extrae una petición de la cola de
peticiones completadas se sigue el camino que realiza la función accept. La
función accept está en la librería libc, y desde ella se solicita al núcleo al
núcleo que ejecute las instrucciones que en el espacio de usuario están
restringidas. La petición al núcleo se realiza mediante una llamada al sistema.
Las llamadas al sistema son las encargadas de comunicar los espacios de
núcleo y de usuario.
El camino teórico que sigue la función accept puede verse en la Fig. 2.6. La
aplicación llama a la función accept y ésta necesita ejecutar operaciones en
modo privilegiado. Al no poder hacerlo, ya que se encuentra en el espacio de
usuario recurre a una llamada al sistema. La llamada al sistema accede al
núcleo y le pide que ejecute la tarea que en este caso necesitara realizar
accept. El núcleo entonces realiza la operación solicitada devolviendo el
resultado.
El siguiente paso es descubrir como se implementa una llamada al sistema
de red en Linux. Concretamente nos interesa ver la implementación de accept
para el protocolo TCP. El prototipo de accept (en el espacio de usuario) es:
#include <sys/types.h> #include <sys/socket.h> int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
26
2.1 Establecimiento de la conexión de TCP en el núcleo.
La llamada al sistema que se genera con esta función se llama:
sys_socketcall.
arch/m32r/kernel/syscall_table.S
1ENTRY(sys_call_table)2 .long sys_restart_syscall/* 0 old "setup()" system call*/...104 .long sys_socketcall
arch/m32r/kernel/syscall_table.S
Ésta es una llamada al sistema que engloba a todas las llamadas a
funciones de la librería de sockets. En función de un parámetro de dicha
llamada al sistema se ejecutará una función u otra. A continuación se presenta
la implementación:
27
Fig. 2.6: Camino seguido por una función que deba acceder al núcleo
aplicación
hardware
núcleo
librería
Interacción mediante llamadas al sistema
Interacción mediante llamadas a funciones
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
net/socket.c
2007asmlinkage long sys_socketcall(int call, unsigned long __user *args)2008{2009 unsigned long a[6];2010 unsigned long a0, a1;2011 int err;...2040 case SYS_ACCEPT:2041 err =2042 sys_accept(a0, (struct sockaddr __user *)a1,2043 (int __user *)a[2]);2044 break;
net/socket.c
En dicha implementación, según el parámetro de la llamada al sistema
llamado call, se llama a una función u otra. En nuestro caso de estudio,
cuando call = SYS_ACCEPT, se llama a la función sys_accept, definida también
en socket.c.
La función sys_accept realiza varias funciones y finalmente llama a la
función del listening socket (el socket que recibe las conexiones y las
establece hasta que se llama a accept), denominado sock, mediante la
invocación:
net/socket.c1421 err = sock>ops>accept(sock, newsock, sock>file>f_flags);
net/socket.c
Hay que notar que ops es una estructura del tipo proto_ops, la cual tiene un
puntero a función que se llama accept.
include/linux/net.h 135struct proto_ops { 136 int family;... 147 int (*accept) (struct socket *sock, 148 struct socket *newsock, int flags);
include/linux/net.h
Este puntero se inicializa a la función que implementa la llamada accept
cuando se crea el socket, de modo que, dependiendo del tipo de socket que se
crea, la implementación de la función es diferente. En nuestro caso de estudio,
el protocolo es TCP y se supone IPv4, por lo que la asignación de la función se
realiza en el siguiente código:
net/tcp_ipv4.c 2414struct proto tcp_prot = { ... 2420 .accept = inet_csk_accept,
net/tcp_ipv4.c
28
2.1 Establecimiento de la conexión de TCP en el núcleo.
Por tanto, la implementación de la llamada accept es la función
inet_csk_accept, cuya implementación se presenta a continuación:
net/ipv4/inet_connection_sock.c/* 210 * This will accept the next outstanding connection. 211 */ 212struct sock *inet_csk_accept(struct sock *sk, int flags, int *err) 213{ 214 struct inet_connection_sock *icsk = inet_csk(sk); 215 struct sock *newsk; 216 int error; 217 218 lock_sock(sk); 219 220 /* We need to make sure that this socket is listening, 221 * and that it has something pending. 222 */ 223 error = EINVAL; 224 if (sk>sk_state != TCP_LISTEN) 225 goto out_err; 226 227 /* Find already established connection */ 228 if (reqsk_queue_empty(&icsk>icsk_accept_queue)) {... 241 newsk = reqsk_queue_get_child(&icsk>icsk_accept_queue, sk); 242 BUG_TRAP(newsk>sk_state != TCP_SYN_RECV);... 250}
net/ipv4/inet_connection_sock.c
Primero se bloquea el socket para posteriormente asegurarnos de que se
encuentra en el estado LISTEN. Es interesante recordar que el socket de
escucha, el que se encuentra en el servidor, debe estar en el estado LISTEN
para poder recibir peticiones. Tras esto en la línea 228 se revisa si la cola está
vacía con la función reqsk_queue_empty. Esta cola es la cola de peticiones
establecidas, ya que es la que hay que comprobar en accept si tiene alguna
posición ocupada para poder extraerla. Nótese también que en la línea 241, la
función reqsk_queue_get_child utiliza de nuevo esta cola; veamos qué se hace
en esta función:
net/ipv4/inet_connection_sock.c
183static inline struct sock *reqsk_queue_get_child(struct 184 request_sock_queue *queue,struct sock *parent) 185{ 186 struct request_sock *req = reqsk_queue_remove(queue); 187 struct sock *child = req>sk; 188 189 BUG_TRAP(child != NULL); 190 191 sk_acceptq_removed(parent); 192 __reqsk_free(req); 193 return child; 194} net/ipv4/inet_connection_sock.c
29
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Como vemos la cola pasada como argumento, queue, es una estructura del
tipo request_sock_queue, cuya estructura se explicará en la Apartado 2.2.2.
Dentro de esta función se llama (línea 186) a la función reqsk_queue_remove,
que es la encargada de extraer el socket que ocupa la primera posición de la
cola de peticiones establecidas (llamado child) para más tarde devolverlo
(línea 193).
Así pues del análisis de este código podemos deducir por tanto que la cola
de peticiones establecidas es la icsk>icsk_accept_queue, la cual se estudiará
más adelante en profundidad.
2.1.2 Acceso a la cola de conexiones completadas desde la red.
Ahora veremos el otro camino para acceder a la cola de peticiones
completadas. Buscamos la implementación de este proceso en el núcleo
sabiendo que la sección que nos interesa especialmente se encuentra en el
término de la negociación en tres pasos, momento en el que el núcleo extrae
la petición de la cola de conexiones incompletas y la añade a la cola de
conexiones completadas.
En la Apartado 2.2.1, describiremos la estructura
inet_connection_sock_af_ops en la que se encontrarán diferentes punteros a
funciones de TCP y en el siguiente punto se presentarán sus correspondencias
a las funciones para el caso de IPv4.
Buscando en el archivo en el que se definen dichas funciones
(net/ipv4/tcp_ipv4.c) se puede encontrar la función tcp_v4_syn_recv_sock (el
puntero a función del que viene es syn_recv_sock). A continuación se describe
en mayor profundidad esta función ya que será de gran interés más adelante.
net/ipv4/tcp_ipv4.c1411/*1412 * The three way handshake has completed we got a valid synack 1413 * now create the new socket.1414 */1415struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,1416 struct request_sock *req,1417 struct dst_entry *dst)1418{
net/ipv4/tcp_ipv4.c
30
2.1 Establecimiento de la conexión de TCP en el núcleo.
El comentario que encontramos al inicio de esta función ya nos indica que
estamos ante la función buscada. Nos comenta que la negociación en tres
pasos ha sido terminada y tenemos un synack válido lo que implica que
debemos crear el nuevo socket. Con el nuevo socket se refiere al que será
añadido a la cola de peticiones completadas.
net/ipv4/tcp_ipv4.c1419 struct inet_request_sock *ireq;1420 struct inet_sock *newinet;1421 struct tcp_sock *newtp;1422 struct sock *newsk;1423#ifdef CONFIG_TCP_MD5SIG1424 struct tcp_md5sig_key *key;1425#endif14261427 if (sk_acceptq_is_full(sk))1428 goto exit_overflow;14291430 if (!dst && (dst = inet_csk_route_req(sk, req)) == NULL)1431 goto exit;14321433 newsk = tcp_create_openreq_child(sk, req, skb);1434 if (!newsk)1435 goto exit;1436
net/ipv4/tcp_ipv4.c
En esta primera parte de la función se definen las variables a utilizar y se
comprueban varias condiciones de error para, en caso de cumplirse alguna,
salir de la función. Además se crea el socket newsk con la función
tcp_create_openreq_child. Ésta crea un socket con todos los parámetros por
defecto. net/ipv4/tcp_ipv4.c1437 newsk>sk_gso_type = SKB_GSO_TCPV4;1438 sk_setup_caps(newsk, dst);14391440 newtp = tcp_sk(newsk);1441 newinet = inet_sk(newsk);1442 ireq = inet_rsk(req);1443 newinet>daddr = ireq>rmt_addr;1444 newinet>rcv_saddr = ireq>loc_addr;...1449 newinet>mc_ttl = ip_hdr(skb)>ttl;1450 inet_csk(newsk)>icsk_ext_hdr_len = 0; net/ipv4/tcp_ipv4.c
En esta sección del código se rellenan los campos específicos del socket con
la información pasada como argumento a la función. net/ipv4/tcp_ipv4.c1451 if (newinet>opt)1452 inet_csk(newsk)>icsk_ext_hdr_len = newinet>opt>optlen;1453 newinet>id = newtp>write_seq ^ jiffies;14541455 tcp_mtup_init(newsk);1456 tcp_sync_mss(newsk, dst_mtu(dst));
31
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
1457 newtp>advmss = dst_metric(dst, RTAX_ADVMSS);1458 tcp_initialize_rcv_mss(newsk);...1479 return newsk;14801481exit_overflow:1482 NET_INC_STATS_BH(LINUX_MIB_LISTENOVERFLOWS);1483exit:1484 NET_INC_STATS_BH(LINUX_MIB_LISTENDROPS);1485 dst_release(dst);1486 return NULL;1487} net/ipv4/tcp_ipv4.c
Por último, se realizan unas comprobaciones y se devuelve el socket
creado o NULL si el código ha devuelto alguna condición de error. Si esta
función devuelve NULL la función en la que se utiliza y a la que le llegará este
valor nulo entenderá esto como un listen_overflow. En el siguiente extracto
de código de la función tcp_check_req se ve un claro ejemplo de esto:
net/ipv4/tcp_minisocks.c 488struct sock *tcp_check_req(struct sock *sk,struct sk_buff *skb, 489 struct request_sock *req, 490 struct request_sock **prev) 491{... 651 child = inet_csk(sk)>icsk_af_ops>syn_recv_sock(sk, skb, 652 req, NULL); 653 if (child == NULL) 654 goto listen_overflow;... 681 inet_csk_reqsk_queue_removed(sk, req); 682 683 inet_csk_reqsk_queue_add(sk, req, child); 684 return child; 685 686 listen_overflow: 687 if (!sysctl_tcp_abort_on_overflow) {...
net/ipv4/tcp_minisocks.c
Como se puede ver (línea 653 se) comprueba si child, el socket devuelto
por la función syn_recv_sock, analizada anteriormente, es igual a NULL. En caso
de serlo la línea 654 se ejecutaría y por tanto se envía a la línea 686 que como
ya sabíamos indica listen_overflow.
Como ya se ha explicado la función syn_recv_sock es la que devuelve el
socket que debe ser incluido en la cola de peticiones completadas. Pero la
función que la añade es la resaltada en gris en la sección de código anterior
inet_csk_reqsk_queue_add. Es importante comprender el funcionamiento de
esta función para así comprender en última instancia el funcionamiento de la
cola de peticiones aceptadas.
32
2.1 Establecimiento de la conexión de TCP en el núcleo.
✗ La función inet_csk_reqsk_queue_add llama a su vez a la función
reqsk_queue_add que se desarrolla tras ésta. include/net/inet_connection_sock.h 255static inline void inet_csk_reqsk_queue_add(struct sock *sk, 256 struct request_sock *req, 257 struct sock *child) 258{ 259 reqsk_queue_add(&inet_csk(sk)>icsk_accept_queue, req, sk, child); 260} include/net/inet_connection_sock.h
✗ La función reqsk_queue_add añade el socket child, pasado como
argumento, a la cola request_sock_queue (llamada queue).
include/net/request_sock.h 169static inline void reqsk_queue_add(struct request_sock_queue *queue, 170 struct request_sock *req, 171 struct sock *parent, 172 struct sock *child) 173{ 174 req>sk = child; 175 sk_acceptq_added(parent); 176 177 if (queue>rskq_accept_head == NULL) 178 queue>rskq_accept_head = req; 179 else 180 queue>rskq_accept_tail>dl_next = req; 181 182 queue>rskq_accept_tail = req; 183 req>dl_next = NULL; 184}
include/net/request_sock.h
Si nos fijamos en la función inet_csk_reqsk_queue_add la cola que en esta
función llamamos queue es la cola asociada a sk. Habiendo sido sk pasado
como argumento a esta función en la que adopta el nombre de parent. Este
socket (sk en inet_csk_reqsk_queue_add o parent en reqsk_queue_add) es el
que tiene la información de la conexión como es: la cola de peticiones
establecidas, objeto de nuestro estudio.
La manera de añadir el socket child es incluirlo en el request_sock (llamado
req). El siguiente paso es añadirlo a la lista enlazada4 de modo que se debe
apuntar la última posición de queue a éste, ocupando por tanto la última
posición de la cola. Por último, se ha de apuntar el siguiente de req (es decir
dl_next) a NULL debido a que req es ahora el último de la cola y no tiene
ninguna petición tras él. Como vemos las nuevas peticiones se incluyen en la
última posición de la cola.
4Lista enlazada: es una cola cuyos elementos apuntan todos ellos al siguiente exceptuando al último ya que éste no tiene sucesor.
33
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Se elige la función tcp_v4_syn_recv_sock para añadir en ella el código
necesario que permita desarrollar el marco para la inclusión de políticas de
gestión de colas como acción para mitigar los efectos de los ataques DoS, que
es el objeto de este proyecto. La razón por la cual es ésta la función elegida se
debe a que es por esta función por la que se pasa tras el término correcto de
la negociación en tres pasos. Esta función devuelve el nuevo socket que será
añadido a la cola de peticiones completadas. Es esta función un lugar
estratégico desde el que se puede monitorizar perfectamente la cola de
conexiones establecidas y tomar, dependiendo de la política de gestión de
colas que se haya implementado, una decisión u otra.
2.2 Estudio de las estructuras software implicadas en la conexión TCP.
Una vez descrito a alto nivel el proceso del establecimiento de conexión y
concretado en los dos caminos que se siguen en el núcleo para acceder a la
cola de conexiones establecidas estamos en disposición de ahondar en el
conocimiento de las estructuras que intervienen en este proceso.
En la primera parte de esta sección se explicarán las estructuras socket que
intervienen en el establecimiento de conexión de TCP. En la segunda y última
parte de esta sección se profundizará en el conocimiento de la estructura de la
cola de conexiones establecidas y de las estructuras de las que ésta hace uso.
2.2.1 Estructuras socket para la conexión.
Para la definición completa de todas las estructuras que intervienen en el
establecimiento de conexión para el núcleo 2.6 de Linux partiremos de la más
general para, paso a paso, llegar hasta la más particular. El siguiente esquema
nos muestra gráficamente la organización de estas estructuras:
En la Fig. 2.7 podemos observar cómo cada estructura aparte de contener a
la general del nivel superior añade unas particularidades no especificadas en
la imagen. Por ejemplo: el nivel superior (más general) lo forma la estructura
sock_common y la estructura sock contiene a la sock_common y añade unas
particularidades que detallaremos a continuación.
34
2.2 Estudio de las estructuras software implicadas en la conexión TCP.
Para explicar la función de cada estructura y la información que añaden
unas con respecto a otras se ha visto conveniente mostrar el código de cada
una de ellas para desde éste poder fundamentar las explicaciones realizadas.
En el código presentado generalmente no se muestra la estructura completa
sino las partes importantes o que serán de utilidad en explicaciones futuras.
Se resaltará en cada estructura la información interesante para la
comprensión completa del trabajo realizado en este proyecto.
✗ Estructura sock_common:
include/net/sock.h 112struct sock_common { 113 unsigned short skc_family; 114 volatile unsigned char skc_state; 115 unsigned char skc_reuse; 116 int skc_bound_dev_if; 117 struct hlist_node skc_node; 118 struct hlist_node skc_bind_node; 119 atomic_t skc_refcnt; 120 unsigned int skc_hash; 121 struct proto *skc_prot; 122};
include/net/sock.h
35
Fig. 2.7: Estructuras socket
sock_common
sock
inet_sock
inet_connection_sock
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Esta estructura es la representación mínima de los socket de la capa de red.
Con esto queremos decir que esta mínima representación se utiliza para
conectar dos puntos de una red. Esta comunicación puede ser entre dos
procesos dentro de una misma máquina, o entre dos procesos en máquinas
distintas. Si se produce en la misma máquina el dominio de comunicación será
AF_UNIX y si la comunicación se realiza entre máquinas distintas el dominio de
comunicación será AF_INET. En esta estructura encontramos básicamente:
• Las características generales de los socket de red (familia, estado,
nodo, etc.).
✗ Estructura sock:
include/net/sock.h 183struct sock { 184 /* 185 * Now struct inet_timewait_sock also uses sock_common, so please just 186 * don't add nothing before this first member (__sk_common) acme 187 */ 188 struct sock_common __sk_common; 189#define sk_family __sk_common.skc_family... 201 unsigned char sk_protocol; 202 unsigned short sk_type;... 210 struct { 211 struct sk_buff *head; 212 struct sk_buff *tail; 213 } sk_backlog;... 259 int (*sk_backlog_rcv)(struct sock *sk, 260 struct sk_buff *skb); 261 void (*sk_destruct)(struct sock *sk); 262};
include/net/sock.h
Esta estructura representa los socket de red y en ella encontramos
básicamente:
• La estructura sock_common (llamada sk_common) definida en el punto
anterior.
36
sock_common
sock
inet_sock
2.2 Estudio de las estructuras software implicadas en la conexión TCP.
• Las características generales de los socket de red como son familia,
estado, etc. (extraídas de sk_common)
• Una estructura sk_backlog que es la encargada de almacenar los datos
enviados por el enlace TCP.
• Otras características más concretas de los socket de red como puede
ser sk_type que indica el tipo de socket STREAM, DATAGRAM o RAW.
Los socket STREAM son utilizados para el protocolo TCP que es orientado a
conexión y que implica, como hemos visto, un establecimiento de la
conexión.
Los socket DATAGRAM para el protocolo UDP. Lo que implica que son no
orientados a conexión así que no requieren establecimiento de conexión.
Los socket RAW se utilizan para el desarrollo de nuevos protocolos o para
hacer uso de funcionalidades ocultas en los protocolos existentes.
• Punteros a funciones como son las mostradas en el código
(sk_backlog_rcv, sk_destruct) y otras que no mostramos.
✗ Estructura inet_sock:
include/net/inet_sock.h 107struct inet_sock { 108/* sk and pinet6 has to be the first two members of inet_sock */ 109 struct sock sk; 110#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE) 111 struct ipv6_pinfo *pinet6; 112#endif 113 /* Socket demultiplex comparisons on incoming packets. */ 114 __be32 daddr; 115 __be32 rcv_saddr; 116 __be16 dport;
include/net/inet_sock.h
37
sock_common
sock
inet_sock
inet_connection_sock
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Esta estructura representa los socket de red del tipo INET que son aquellos
cuyo dominio de comunicación es del tipo AF_INET. Esto quiere decir que los
procesos que están siendo comunicados están en diferentes sistemas unidos
por una red TCP/IP. Estos son los utilizados para comunicarse en Internet. En
esta estructura encontramos básicamente:
• La estructura sock (llamado sk) descrita en el punto anterior.
• Los datos de la conexión: puerto de destino (dport), dirección de destino
(daddr), etc. Útiles en para obtener información de los clientes al
monitorizar la cola.
• Una estructura ip_options con las opciones del protocolo IP. Desde esta
estructura podremos consultar las opciones del protocolo IP para una
conexión concreta.
✗ Estructura inet_connection_sock:
include/net/inet_connection_sock.h
84struct inet_connection_sock { 85 /* inet_sock has to be the first member! */ 86 struct inet_sock icsk_inet; 87 struct request_sock_queue icsk_accept_queue;... 94 const struct tcp_congestion_ops *icsk_ca_ops; 95 const struct inet_connection_sock_af_ops *icsk_af_ops;... 97 __u8 icsk_ca_state; include/net/inet_connection_sock.h
Esta estructura define los socket de red INET más concretamente los
orientados a conexión que son los implicados en este proyecto ya que nos
basamos en el protocolo TCP que es, a su vez, orientado a conexión. En esta
estructura encontramos básicamente:
38
sock_common
sock
inet_sock
inet_connection_sock
2.2 Estudio de las estructuras software implicadas en la conexión TCP.
• La estructura inet_sock que se describe en el punto anterior.
• La cola de conexiones establecidas (icsk_accept_queue) cuya estructura
definiremos más adelante.
• Una serie de datos de la conexión como son el estado de control de
congestión (icsk_ca_state).
• Una estructura tcp_congestion_ops con punteros a función para
gestionar la congestión.
• La estructura inet_connection_sock_af_ops que estudiamos en el
siguiente punto.
Hasta aquí el estudio de las estructuras socket que intervienen en el
establecimiento de conexión. Es interesante estudiar antes de pasar al
siguiente punto las estructuras ops para mostrar el lugar en el que
encontramos la función tcp_v4_syn_recv_sock. Que como ya se ha comentado ha
sido trascendental en la elaboración de este proyecto.
✗ Estructura inet_connection_sock_af_ops:
include/net/inet_connection_sock.h
39struct inet_connection_sock_af_ops {... 45 struct sock *(*syn_recv_sock)(struct sock *sk, struct sk_buff *skb, 46 struct request_sock *req, 47 struct dst_entry *dst); 48 int (*remember_stamp)(struct sock *sk);
include/net/inet_connection_sock.h
Esta estructura contiene:
• Punteros a función que gobiernan el proceso de la conexión TCP como la
función que se utiliza al finalizarse la negociación en tres pasos
(syn_recv_sock). Estas funciones fueron estudiadas hasta dar con la
comentada en el apartado 2.1.2 Desde la red por ser ésta la más apta
para la consecución de nuestro objetivo como se comentó al término de
este apartado. Como dijimos el puntero a función no es más que una
dirección donde empieza a definirse la función. Dependiendo del
protocolo IP que se esté utilizando estos punteros apuntarán a una u
otra definición. Para el caso de IP versión 4 (la más utilizada ahora
mismo en Internet) el siguiente archivo indica las funciones a las que
estos punteros a función deben apuntar.
39
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
✗ Funciones de inet_connection_sock_af_ops en el caso de Ipv4: net/ipv4/tcp_ipv4.c1812struct inet_connection_sock_af_ops ipv4_specific = {1813 .queue_xmit = ip_queue_xmit,1814 .send_check = tcp_v4_send_check,1815 .rebuild_header = inet_sk_rebuild_header,1816 .conn_request = tcp_v4_conn_request,1817 .syn_recv_sock = tcp_v4_syn_recv_sock, net/ipv4/tcp_ipv4.c
Para ver esta compleja operación de un modo más sencillo
particularizaremos la cuestión en un único puntero a función syn_recv_sock.
Éste para el caso de Ipv4, como vemos en la sección de código superior en la
línea 1817, apunta a la función tcp_v4_syn_recv_sock. La definición de esta
función será la que se utilizará en caso de usar el puntero a función indicado y
en el caso de estar en el protocolo Ipv4.
2.2.2 Estructura de la cola de peticiones completadas.
Este proyecto se centra en la intervención sobre la cola de peticiones
completadas que se describe en las primeras páginas de este capítulo. Esta
cola es icsk_accept_queue y por esta razón se ha visto necesario crear un
apartado exclusivo para describirla en profundidad.
Dentro de la estructura inet_connection_sock encontramos la cola
icsk_accept_queue es una estructura del tipo request_sock_queue y como se ha
comentado el objeto de nuestro estudio. Se presenta un esquema útil para su
comprensión en la Fig. 2.8. Para estudiarla en profundidad, por tanto, hemos
de conocer el funcionamiento de la estructura request_sock_queue. Aquí
presentamos la sección del núcleo en la que esta estructura queda definida:
include/net/request_sock.h 115struct request_sock_queue { 116 struct request_sock *rskq_accept_head; 117 struct request_sock *rskq_accept_tail; 118 rwlock_t syn_wait_lock; 119 u8 rskq_defer_accept; 120 /* 3 bytes hole, try to pack */ 121 struct listen_sock *listen_opt; 122};
include/net/request_sock.h
40
2.2 Estudio de las estructuras software implicadas en la conexión TCP.
Sus características principales son:
• Contiene una cabeza y una cola que son, a su vez, estructuras del tipo
request_sock (esta estructura es la que hay en cada posición de la cola).
• Y una estructura listen_sock (llamada listen_opt). Que tratamos a
continuación.
✗ Estructura listen_opt:
include/net/request_sock.h
86 * @max_qlen_log log_2 of maximal queued SYNs/REQUESTs 87 */ 88struct listen_sock { 89 u8 max_qlen_log; 90 /* 3 bytes hole, try to use */ 91 int qlen; 92 int qlen_young; 93 int clock_hand; 94 u32 hash_rnd; 95 u32 nr_table_entries; 96 struct request_sock *syn_table[0]; 97};
include/net/request_sock.h
41
Fig. 2.8: Estructura de la cola de conexiones establecidas
request_sock_queue
request_sock head
request_sock tail
. . .
request_sock request_sock request_sock
socket sk1
dl_next dl_next
socket sk2 socket sk3
dl_next
NULL
. . .. . . . . .
Capítulo 2: Análisis de la implementación de TCP/IP en Linux
Define características del estado de la escucha, entre las que destacamos,
por su utilidad en los siguientes capítulos, max_qlen_log que representa el
logaritmo en base 2 del máximo de solicitudes/syn que pueden ser encoladas
y qlen que son el número de solicitudes /syn encoladas.
✗ Estructura request_sock:
include/net/request_sock.h
45struct request_sock { 46 struct request_sock *dl_next; /* Must be first member! */ 47 u16 mss; 48 u8 retrans; 49 u8 __pad; 50 /* The following two fields can be easily recomputed I think AK */ 51 u32 window_clamp; 52 u32 rcv_wnd; 53 u32 ts_recent; 54 unsigned long expires; 55 const struct request_sock_ops *rsk_ops; 56 struct sock *sk; 57 u32 secid; 58 u32 peer_secid; 59}; include/net/request_sock.h
Esta estructura es un mini sock que representa una solicitud de conexión,
es un miembro de la lista enlazada (request_sock_queue) y básicamente
contiene:
• Un puntero al socket siguiente.
• Una serie de características de la solicitud de conexión.
• Un puntero a una estructura constante del tipo request_sock_ops que se
definirá a continuación.
• Un puntero a la estructura sock que es la que lleva la información de la
conexión, como ya se ha visto.
✗ Estructura request_sock_ops: include/net/request_sock.h 29struct request_sock_ops { 30 int family; 31 int obj_size; 32 struct kmem_cache *slab; 33 int (*rtx_syn_ack)(struct sock *sk, 34 struct request_sock *req, 35 struct dst_entry *dst); 36 void (*send_ack)(struct sk_buff *skb, 37 struct request_sock *req); 38 void (*send_reset)(struct sock *sk, 39 struct sk_buff *skb); 40 void (*destructor)(struct request_sock *req); 41}; include/net/request_sock.h
42
2.2 Estudio de las estructuras software implicadas en la conexión TCP.
Esta estructura contiene:
– Punteros a función cuya implementación hemos de encontrar para el caso
de Ipv4.
✗ De la misma forma que encontramos la implementación de accept
encontramos también la de los punteros a función de request_sock_ops: net/ipv4/tcp_ipv4.c1235struct request_sock_ops tcp_request_sock_ops __read_mostly = {1236 .family = PF_INET,1237 .obj_size = sizeof(struct tcp_request_sock),1238 .rtx_syn_ack = tcp_v4_send_synack,1239 .send_ack = tcp_v4_reqsk_send_ack,1240 .destructor = tcp_v4_reqsk_destructor,1241 .send_reset = tcp_v4_send_reset,1242}; net/ipv4/tcp_ipv4.c
Estas funciones son las que rigen el envío de los posibles mensajes del
establecimiento de conexión de TCP.
2.3 Conclusiones del capítulo.
Como resumen del presente capítulo y con objeto de hacer especial
hincapié en las cuestiones más importantes, se presentan a continuación las
principales conclusiones obtenidas en este capítulo:
✗ Se han analizado las estructuras que se han considerado interesantes para
ilustrar el funcionamiento del establecimiento de conexión del protocolo
TCP, y el de la cola de conexiones establecidas, en el núcleo de Linux.
✗ Se ha identificado la función tcp_ipv4_syn_recv como la idónea para
introducir en ella las modificaciones que permitirán el desarrollo del marco
para la inclusión de medidas de defensa frente a ataques DoS basados en
políticas de gestión de colas.
43
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleo de Linux
En el Capítulo 1 en el se ha determinado cómo actúan los ataques DoS y,
más concretamente, los ataques DoS contra servidores. Para este tipo de
ataques se propuso una solución consistente en aplicar una política de gestión
de colas que imposibilitara o dificultara en cierta medida la efectividad de este
tipo de ataques. Posteriormente, en el segundo capítulo, se ha ilustrado el
mecanismo de establecimiento de la conexión del protocolo TCP. Se han
identificado también las estructuras que intervienen en este proceso en el
núcleo del sistema operativo Linux y se ha mostrado que la cola de peticiones
establecidas será la que nuestro módulo de gestión monitorice. Se ha
localizado además la sección del código de Linux en que se debe insertar el
código encargado de la gestión de las colas.
Partiendo del desarrollo de políticas de gestión de colas para mitigar el
problema de los ataques DoS, en este proyecto se plantea la necesidad de
implementarlas en un entorno real. La manera en la que se ha propuesto
solventar esta necesidad es generando un entorno flexible para la inclusión de
medidas de defensa contra ataques DoS basadas en gestión de colas en el
núcleo del Sistema Operativo Linux. Este marco pretende ser una herramienta
para todo aquel que pretenda el desarrollo de una política de gestión de colas
para mitigar el problema derivado de los ataques DoS. Esta herramienta debe
permitir, de la manera más sencilla posible, incluir cualquier política de
gestión de colas en el núcleo de Linux y poder comprobar cómo se comporta
en un entorno real.
45
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
Es éste, por tanto, el capítulo destinado a mostrar cómo ha sido el desarrollo
del ya mencionado entorno para la inclusión de medidas de defensa para
ataques DoS. El capítulo se encuentra dividido en tres partes bien
diferenciadas. La primera es la encargada de profundizar en el concepto de
núcleo de un sistema operativo y en el desarrollo de módulos del núcleo o
simplemente módulo. Se explicará la manera en la que el núcleo de Linux
puede ser modificado, ya sea mediante modificación directa, incluyendo
código en él o mediante el uso de módulos del núcleo. Por último se explicarán
los motivos que nos guiaron a realizar la modificación del núcleo con el apoyo
de módulos del núcleo. En la segunda parte del capítulo se identifican las
dificultades encontradas al intentar modificar el núcleo mediante módulos, y
se ilustra la medida adoptada finalmente: la utilización de garfios (en inglés
hook [2]). La tercera y última parte de este tema está destinada a ilustrar el
mecanismo de creación de módulos utilizando hooks. Este proceso se ilustrará
mediante la explicación del módulo generado para implementar el entorno
comentado. También se descubrirá la implementación de una política de
gestión de colas que permite probar el funcionamiento del marco de pruebas
generado.
3.1 Núcleo y módulos del núcleo
El sistema operativo Linux es un sistema operativo de código libre. Esto
quiere decir no solamente que su distribución es por completo legal y gratuita,
sino mucho más. Lo que implica también es que el código fuente que lo
compone esta a disposición de todo el que quiera descargarlo. Este hecho nos
entrega una herramienta extremadamente poderosa gracias a la cual ha sido
posible realizar este proyecto. Nos brinda la posibilidad de introducir cambios,
a nuestro antojo, en el funcionamiento de todo el sistema. Precisamente ese
tema es el trata de aclarar esta sección, el de introducir cambios en el sistema
operativo.
El núcleo del sistema operativo Linux está organizado siguiendo una
arquitectura monolítica, en la cual, todas sus partes (sistemas de ficheros,
manejadores de dispositivos, protocolos de red, etc.) están enlazadas como
una sola imagen que es la que se carga y ejecuta en el arranque del sistema.
Esta estructura lo hace bastante eficiente, ya que disminuye los cambios de
46
3.1 Núcleo y módulos del núcleo
contexto, es decir, los cambios de espacio de direcciones de usuario a núcleo,
para poder ejecutar instrucciones en modo privilegiado. Por contra, el ser un
sistema monolítico implica también que existe una menor fiabilidad. A modo
de ejemplo la gestión de ficheros del sistema puede tener acceso a la gestión
de la tarjeta de red y esto podría ser nefasto e incluso constituir una
vulnerabilidad a explotar desde el punto de vista de un posible atacante.
También podría dar lugar a un sistema poco flexible, ya que cualquier
funcionalidad que se le quisiera añadir al núcleo del sistema requeriría una
recompilación completa del mismo. Como veremos en el Apartado 3.1.2 esta
flexibilidad ha sido mejorada en sucesivas versiones de Linux.
3.1.1 Compilación del núcleo
El primer procedimiento para modificar el núcleo de Linux es muy directo.
Consiste en modificar el código directamente sobre el código fuente de Linux.
Tras esta modificación debe compilarse el núcleo para obtener el objeto
imagen que deberá ser cargado para que se pueda ejecutar el nuevo núcleo.
El proceso de compilación del núcleo es costoso tanto en tiempo como en
recursos del sistema. Esto implica que la compilación completa del sistema
podría llevar alrededor de 3 horas dependiendo del ordenador utilizado en el
proceso. Para entender este mecanismo de la manera más sencilla posible
supongamos que el núcleo no es más que un programa, de dimensiones
desproporcionadas y complejidad extrema, pero un programa al fin y al cabo.
Suponiendo esto el proceso para obtener la imagen de este programa pasaría
por compilarlo y enlazarlo.
En resumen estas son las operaciones realizadas en el proceso de
compilación (Fig. 3.1). Generalmente a la imagen obtenida se le asigna el
nombre de vmlinuz y a continuación la versión del núcleo compilado.
Una vez se ha obtenido la imagen del nuevo núcleo lo único que ha de
conseguirse es que el sistema cargue esta imagen y no la antigua. La
consecución de este objetivo pasa por modificar el archivo menu.lst que se
encuentra en el directorio /boot/grub/ si se usa el gestor de arranque Grub. Se
podría decir que en este archivo se le indica al gestor de arranque Grub el
conjunto de núcleos de los que dispone y entre los que el usuario, al iniciar el
sistema, elegirá.
47
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
Una vez realizada por primera vez la compilación del núcleo las sucesivas
ocasiones no requerirán que se compile de nuevo todo el código fuente sino
solamente aquél al cual la modificación realizada le afecte. A este proceso de
posteriores compilaciones se le denomina recompilación y aunque no requiere
tanto tiempo sí que implica un necesario reinicio del sistema para que los
nuevos cambios entren en vigor.
3.1.2 Módulos del núcleo
La limitación que impone la engorrosa tarea de reiniciar cada vez que
necesitamos aplicar un nuevo cambio a nuestra modificación del núcleo
desapareció con la incorporación, en la versión 1.2 de Linux, del soporte para
la carga dinámica de módulos en el núcleo (en inglés Loadable Kernel Module
LKM). Este nuevo avance permitió la incorporación de una modificación al
núcleo sin la necesidad de reiniciar el sistema completo. Lo que implica que si
necesitamos realizar cualquier cambio en un módulo esto sólo tomará el
tiempo necesario para recompilar el código y recargar el módulo, proceso que
consiste en la simple ejecución de dos instrucciones, tal y como se explicará
más adelante.
48
Fig. 3.1: Esquema simplificado de la recompilación del núcleo
3.1 Núcleo y módulos del núcleo
¿Qué es un módulo del núcleo?
Los módulos son programas escritos en el lenguaje de programación del
núcleo, ANSI C. La característica peculiar que tienen estos programas es la
capacidad de, una vez obtenida su imagen, ser cargados en tiempo de
ejecución, es decir, sin necesidad de reiniciar el sistema y pasar a formar
parte del núcleo con toda su capacidad. Existe la desventaja de no poder
utilizar ninguna función de las que se implementan en las librerías
convencionales como puede ser printf(), o rand(), etc. Deberemos utilizar las
funciones propias del núcleo como son printk(), get_random_byte(), etc. Esta
desventaja implica un tiempo importante de análisis del código del núcleo
para así hallar la función requerida. Sin embargo, se podrá ejecutar el código
en modo privilegiado, lo que da control sobre todas las funciones del sistema
y, más aún, sobre el hardware del mismo.
Por tanto, un módulo es un fichero o conjunto de ficheros con extensión .c
que, al ser compilados con las opciones correspondientes, producen una
imagen con extensión .ko que puede ser cargada en tiempo de ejecución del
sistema.
Estructura de un módulo del núcleo
Un módulo del núcleo debe cumplir ciertos requisitos que le permitirán ser
insertado en el núcleo. El módulo tendrá una estructura determinada para que
su compilación sea exitosa y se pueda cargar con éxito tras ella.
La estructura puede resumirse en las siguientes secciones (Fig. 3.2):
✗ Cabeceras necesarias. Necesitaremos, al menos, las cabeceras de
kernel.h que nos indica que trabajamos en relación al núcleo y module.h
que implica que el código que realizamos no es sólo relativo al núcleo sino
que es un módulo del núcleo.
✗ Funciones init_module y cleanup_module. Estas funciones son las
encargadas de cargar el módulo en el sistema y de descargarlo ambas a
petición del usuario con las sentencias que se indicarán más adelante en
esta sección.
49
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
✗ Funciones auxiliares. Funciones que realicen las operaciones deseadas
y que serán utilizadas en el interior de init_module o de cleanup_module.
Estas funciones no es obligatorio que aparezcan en el código pero es
común utilizar en programación la política de “Divide y vencerás” que
consiste en dividir un problema en secciones a las se les da solución con
una función. Posteriormente todas ellas son encuadradas en una misma.
Una vez cargado el módulo la única diferencia entre su código y el código
del núcleo es la posibilidad de poder ser extraído a petición del usuario. Al
realizar esta extracción se liberarán todos los recursos que el módulo hubiera
utilizado, según se indique en la función cleanup_module.
La manera en la que el módulo puede ejecutar funciones del núcleo sin
haber sido enlazado en tiempo de compilación con él, consiste en que las
referencias a las funciones del núcleo utilizadas no están resueltas. Para que
estas referencias queden configuradas de una manera correcta, el proceso de
inserción de un módulo debe seguir una rutina muy específica:
1 Obtener las referencias a las funciones del módulo.
2 Incluir dichas referencias al núcleo. Siendo éstas temporales para que al
ser liberado el módulo desaparezcan.
50
Fig. 3.2: Estructura básica de un
módulo del núcleo
3.1 Núcleo y módulos del núcleo
3 Hallar las referencias a las funciones no resueltas en el módulo. Que son:
las funciones propias del núcleo o las funciones exportadas por otros
módulos.
4 Incluir el módulo en el espacio de memoria reservado al núcleo para que
pueda ejecutarse en modo privilegiado.
5 Por último, invocar a la función init_module del nuevo módulo.
Para extraer el módulo también es necesario realizar una rutina concreta.
Ésta realiza los mismos primeros cuatro pasos, en orden inverso, y el quinto
consiste en invocar a la función cleanup_module.
Proceso de carga y descarga de un módulo del núcleo
Para la carga y la descarga de módulos del núcleo se utilizan las funciones
insmod y rmmod. Para realizar cualquiera de estas operaciones debemos ser
superusuarios así que se ejecutarán precedidas por sudo5. Un módulo es
cargado ejecutando la sentencia sudo insmod seguida del nombre de la imagen
del núcleo que como ya se ha mencionado tiene extensión .ko.
sudo insmod hook.ko
El módulo es descargado ejecutando sudo rmmod y añadiendo el nombre del
módulo cargado que, a menos de haber sido cambiado voluntariamente, será
igual que la imagen exceptuando su extensión.
sudo rnmod hook
Para ver si un módulo ha sido correctamente cargado se puede consultar la
lista de módulos cargados en el sistema esto se lleva a cabo consultando el
archivo modules en el directorio /proc/.
cat /proc/modules
Ejecutando esta orden en una terminal Linux se podrá visualizar el
contenido del archivo modules. También se puede realizar esta consulta con el
comando lsmod que lista la información contenida en el archivo /proc/modules
de una manera más ordenada. Escribiendo lsmod en la termina se obtiene una
salida de cuatro columnas.
5Sudo es una orden de la shell de Linux que da permisos de superusuario para ejecutar la orden que le siga.
51
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
Module Size Used by af_packet 23812 2 l2cap 25728 13 rfcomm
✗ La primera muestra el nombre del módulo. Generalmente es el mismo
nombre que el archivo que se carga pero sin la extensión .ko.
✗ La segunda indica el número de memoria en bytes que ocupa el módulo.
✗ La tercera el número de instancias de este módulo que actualmente están
lanzadas. Un valor de 0 indica que puedes descargar el módulo.
✗ La cuarta columna representa los módulos en los que éste es utilizado.
3.1.3 Módulos vs implementación directa en el núcleo
En el presente proyecto se ha optado por la implementación mediante el
uso de módulos. Las razones por las que se elige realizar la modificación del
núcleo mediante un módulo y no mediante la modificación directa sobre el
núcleo son varias y se detallan a continuación:
✗ El proceso de implementación de cualquier programa pasa por una fase de
constantes modificaciones en las que se va depurando el código hasta
obtener el resultado deseado. Si este proceso lo realizáramos modificando
directamente sobre el núcleo tras cada variación de la implementación
necesitaríamos un reinicio del sistema y esto conllevaría grandes tiempos
de espera.
✗ La fase de pruebas del entorno para una política concreta dada necesita
someter el sistema a las mismas condiciones aplicando la política y sin
aplicarla, y esto de nuevo conllevaría un gran número de reinicios del
sistema.
✗ El realizar esta modificación mediante un módulo del núcleo permite
presentar una interfaz gracias a la cual el usuario no debe introducir el
código en un archivo del núcleo y dentro de él en una función concreta.
Este interfaz ofrece un nivel de abstracción más por el que no es necesario
modificar los archivos con el código fuente del núcleo para poder incluir
una política de gestión de colas.
52
3.2 Módulos de seguridad de Linux (LSM) y garfios.
3.2 Módulos de seguridad de Linux (LSM) y garfios.
En esta sección se describen las dificultades encontradas al tratar de
implementar, en un módulo del núcleo, el entorno que posibilita la inclusión de
políticas de gestión de colas en el núcleo de Linux, descrito en la Apartado
3.2.1. Posteriormente se muestra la solución encontrada a dichas dificultades:
los garfios, descrita en la Apartado 3.2.2. Por último se muestra cómo se ha
realizado una modificación en el núcleo de Linux utilizando garfios, descrito en
la Apartado 3.2.3.
3.2.1 Dificultad hallada en la implementación del módulo.
Ya se ha comentado que los módulos del núcleo son realmente útiles y esto se
debe, en gran parte, a su peculiar característica de poder ser cargados en
tiempo de ejecución, siendo innecesario el reinicio del sistema. Son utilizados
típicamente en tres escenarios:
✗ Driver para dispositivos. Un driver para dispositivos es diseñado para
una sección específica del hardware. El núcleo los utiliza para comunicarse
con la sección de hardware sin tener que conocer ningún detalle acerca de
cómo funciona.
✗ Driver para sistemas de ficheros. Este tipo de drivers interpreta el
contenido de un sistema de almacenamiento de información como
ficheros y directorios. Hay muchos caminos diferentes de almacenar
ficheros. Por ejemplo, el sistema de ficheros utilizado universalmente por
Linux es ext2, y Windows utiliza NTFS o FAT32. Para manejarlos
necesitamos los driver para esos sistemas de ficheros.
✗ Llamadas al sistema. Los programas del espacio de usuario utilizan las
llamadas al sistema para solicitar servicios al núcleo. Por ejemplo, la
llamada al sistema mencionada en el Apartado 2.1.1 sys_socketcall que
solicita servicios relacionados con la tarjeta de red.
El núcleo está preparado para insertar módulos en los escenarios descritos
pero realizar modificaciones en otro sentido, como por ejemplo, en un lugar
concreto del núcleo requiere de un análisis exhaustivo del código. En el caso
53
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
del presente proyecto es necesario modificar el núcleo en un punto concreto,
como se dijo en el Apartado 2.1.2, en la función tcp_v4_syn_recv_sock. Por
esta razón el hecho de aplicar los módulos del núcleo como hasta ahora se
han explicado se hace complejo. La solución escogida es una adaptación de un
modo concreto de modificación mediante módulos descrito en [2]. Esta
modificación es posible gracias a los garfios (hooks) utilizados en los módulos
de seguridad de Linux o LSM (del inglés: Linux Security Modules) que se
describen en el próximo apartado. La utilización de los garfios en el trabajo
citado se hace de una manera excesivamente compleja y hace uso de una
llamada al sistema creada para este fin y de unas librerías que no son
necesarias para la modificación que se persigue en este proyecto. Por este
motivo ha sido necesario adaptar el método de modificación utilizado en LSM
para satisfacer las necesidades de este proyecto.
3.2.2 Solución encontrada: Garfio.
El objetivo de los LSM es dar solución al problema que se encontró al
descubrir que los mecanismos de control de acceso de los principales sistemas
operativos eran inadecuados. Debido a que los sistemas operativos deben
satisfacer un amplio rango de requerimientos de los usuarios, es necesario,
del mismo modo, que cualquier mecanismo de control de acceso instalado en
el sistema sea capaz de soportar una multitud de modelos de control de
acceso diferentes. Para este fin se desarrolló un marco para el núcleo de Linux
que permite una gran variedad de modelos de control de acceso para ser
implementados en el sistema como módulos del núcleo.
La creación de LSM perseguía un objetivo similar al que se propone en este
proyecto, que ciertas políticas de seguridad fueran aplicadas en puntos
concretos del núcleo en los que se comprobarían, en caso de estar cargado el
núcleo, una serie de condiciones para permitir el acceso o no de esta petición.
Es por este motivo por el que se presenta, someramente, la idea con la que el
proyecto LSM consiguió aplicar dichas modificaciones en lugares específicos
del código fuente de Linux.
54
3.2 Módulos de seguridad de Linux (LSM) y garfios.
LSM permite el acceso a los objetos del núcleo incluyendo trozos de código
denominados garfios. Justo antes de que el núcleo tenga acceso a un objeto
interno, un garfio realiza la operación de llamar a la función que el módulo
LSM debe proveer. Se podría decir que el garfio pregunta a la función que
implementa la política de control de acceso (en el caso de LSM) si puede darle
el control del objeto requerido (Fig. 3.3). La política implementada en el
módulo LSM analiza la información que tiene y determina si permite o no el
acceso respondiendo en consecuencia. Una adaptación de este mecanismo se
puede aplicar perfectamente para la creación de la modificación concreta en
la función tcp_v4_syn_recv_sock.
En resumen el objetivo de un garfio es el de, en el punto concreto del
código en el que se incluya, llamar a la función del módulo que analiza el
estado de la información del núcleo y decide si pasa o no la política de control
de acceso. La ventaja encontrada para la modificación del núcleo mediante
garfios es precisamente la que hizo que se la adoptara como método para la
generación del entorno objeto de este proyecto. Los garfios permiten
modificar el núcleo en puntos concretos de su código y como se ha comentado
en el inicio de este punto ésta era precisamente la necesidad que se había
detectado.
55
Fig. 3.3: Esquema de funcionamiento de un garfio
Espacio de usuario
Espacio de núcleo
Nivel de proceso de usuario
Llamada al sistema
Garfio
Acceso al objeto
Examina si pasala política
Módulo LSM
Sí o No
¿Puedo?
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
3.2.3 Proceso de implementación de un garfio.
Para que el garfio desarrollado funcione correctamente es necesario que
éste actúe, como es lógico, sobre el núcleo y sobre el módulo, por esto mismo
se comentó que esta solución era intermedia entre los módulos del núcleo y la
modificación directa sobre él. De esta forma este punto se divide en dos
partes: laparte describe la implementación necesaria en el núcleo y la
siguiente en el módulo.
Para ilustrar de un modo más concreto cómo se realiza una modificación en
el núcleo mediante un módulo del núcleo ayudado de un garfio, en los
siguientes puntos se describen, sin profundizar en detalles minuciosos, las
secciones más interesantes del código desarrollado para la creación del
entorno para la inclusión de medidas de defensa frente a ataques DoS.
Implementación en el núcleo
El elemento de programación que realizará la función de garfio será una
estructura denominada access_control_rodgom. Cuando se hable de una
instanciación concreta de dicha estructura se la llamará access_cont.
El contenido de esta estructura es de un único elemento: un puntero a
función. Este puntero a función es la clave del éxito de la modificación
mediante garfios, es el encargado de apuntar a la función_inútil, que no
aplica ninguna política, hasta que el módulo es cargado momento en el que
pasa a apuntar a la función descrita en el módulo que implementa la política
de gestión de colas (Fig. 3.4).
Para la correcta utilización del mecanismo de los garfios debe crearse un
archivo que es denominado de definición y cuya extensión es .h que
concretamente ha sido almacenado como rodgom.h. La estructura del archivo
de definición debe ser la siguiente:
✗ La primera parte incluye las cabeceras que son necesarias para la
ejecución del código posterior.
//Cabecera que nos permite utilizar la estructura sock entre otras.#include <net/sock.h>
56
3.2 Módulos de seguridad de Linux (LSM) y garfios.
✗ Debe contener la definición de la estructura access_control_rodgom que
debe formar parte del núcleo. En cuyo interior se encuentra el puntero a
función.
//Definición de la estructura struct access_control_rodgom {
int (*modificacion) (struct sock *sk,struct sock *newsk);
};
✗ Una función con el mismo formato que el puntero a función de la
estructura access_control_rodgom, es decir, con el mismo tipo de dato en
su devolución y los mismos argumentos. Esta función será la llamada
función inútil y será a la que llame el garfio mientras el módulo esté
descargado. Para que el núcleo continúe con su ejecución normal, sin
ningún tipo de modificación, mientras el módulo está descargado, es
necesario que esta función no realice ninguna operación.
57
Fig. 3.4: Funcionamiento de un garfio
Puntero a función
Estructura access_control_rodgom
Función inútil { No hacer nada}
Función inútil
Modificación del núcleo
Modificación del módulo
Función útil { Política de gestión de colas}
Función útil
access_cont
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
inline int modificacion_tonta(struct sock *sk,struct sock *newsk){
printk(KERN_ALERT "Estoy pasando por tontolandia\n"); return 0;
}
Otra cuestión a tener en cuenta es que este archivo de definición debe
ubicarse en un lugar lógico dentro de la estructura compleja del código fuente
del núcleo Linux y al tratarse un de un archivo .h y tratar del tema de red el
lugar propuesto es include/net/rodgom.h. La estructura esquematizada de la
cabecera rodgom.h se encuentra en la Fig. 3.5.
La otra modificación que es necesaria para el funcionamiento del módulo se
realiza en el archivo .c del núcleo en el que se pretende incluir la modificación.
En nuestro caso comentamos que la función en la que incluiríamos la
modificación era tcp_v4_syn_recv_sock y ésta se encuentra definida en el
archivo net/ipv4/tcp_ipv4.c que será el citado archivo en el que se habrá de
incluir la modificación explicada a continuación. Para su conveniente
localización, todas las secciones de código incluidas por este proyecto están
precedidas y seguidas de la cadena //rodgom. La estructura de esta
modificación es la siguiente:
58
Fig. 3.5: Estructura de rodgom.h
Archivo de definición rodgom.h
estructura access_control_rodgom {
Puntero a función
}
Definición de la función inútil {
No hacer nada
}
Cabeceras .h
3.2 Módulos de seguridad de Linux (LSM) y garfios.
✗ Lo primero que ha hecho es incluir en las cabeceras una entrada para el
archivo de definición sin el cual no se reconocerá la nueva estructura
access_control_rodgom.
70#include <net/rodgom.h> //Siempre que el archivo esté en include/net/
✗ El paso siguiente consiste en definir e inicializar una instancia de la
estructura en la sección de las variables globales de este archivo para
poder utilizarla en cualquiera de sus funciones. También debe estar
colocada en la sección de las variables globales para poder exportarla y
que posteriormente el módulo del núcleo pueda reconocerla. La
incialización pretende apuntar el puntero a función que contiene en su
interior a la función inútil que definimos en el fichero de definición
(modificación_tonta). En la Fig.3.6 se encuentra un resumen del proceso
que está siendo descrito.
90 struct access_control_rodgom access_cont = { 9192 .modificacion=modificacion_tonta, 9394 };9596 EXPORT_SYMBOL(access_cont);
59
Fig. 3.6: Estructura de modificación del archivo del núcleo
tcp_ipv4.c
Archivo del núcleo tcp_ipv4.c
#Inclusión del archivo de definición rodgom.h...En la declaración de las variables globales:1 Declaramos una estructura access_cont2 La definimos apuntando su función la inútil3 Exportamos dicha estructura...En el espacio a incluir la modificación:1 Llamamos a la función de la estructura
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
✗ Por último se incluye la llamada al puntero a función a través de la
estructura definida al inicio del archivo (access_cont) en el punto en el que
se pretende que el garfio actúe, es decir, en la línea de código en la que la
política de gestión de colas debe entrar en funcionamiento. A esta función
deben pasársele los argumentos necesarios que serán la información de la
que dispondrá el módulo de gestión para monitorizar la cola de servicio.
1480 access_cont.modificacion(sk,newsk);
Implementación del módulo
El módulo en sí debe estar estructurado de una manera en particular que
pasamos a detallar (Fig. 3.7):
✗ Debe incluir el archivo de definición de extensión .h en el que se define la
estructura access_control_rodgom, aparte de las cabeceras propias de los
módulos (kernel.h y module.h) y las necesarias para el funcionamiento de
la política de gestión de colas.
#include <linux/kernel.h> #include <linux/module.h> ...#include <net/rodgom.h> #include <linux/random.h>
60
Fig. 3.7: Estructura de creación del módulo del núcleo
Módulo hook.c
Cabeceras .hextern de access_contfunción útil {
política de gestión de colas}init_module{
guardamos el estado de access_contsu puntero a función lo apuntamos a la últil
}cleanup_module{
devolvemos a su estado anterior a access_cont}
3.2 Módulos de seguridad de Linux (LSM) y garfios.
✗ En el espacio destinado a la definición de las variables globales debe
definirse la estructura access_cont. Para que esta definición se reconozca
como el mismo objeto que se exportó desde el archivo del núcleo de
extensión .c deben cumplirse estos dos requisitos:
□ La definición debe ser precedida de la palabra clave extern.
□ Debe denominarse exactamente de la misma forma que se hizo en
el archivo del núcleo desde el que se exportó. En este caso en el
archivo net/ipv4/tcp_ipv4.c se define con el nombre de
access_cont de modo que este debe ser el nombre otorgado en el
módulo.
extern struct access_control_rodgom access_cont;
✗ Tras esta definición y las de las variables globales en general deberá
presentarse la declaración de la función que en su interior contiene la
ejecución de la política de gestión de colas. Esta función debe tener el
mismo formato que la definida en el archivo de definición en el interior de
la estructura access_control_rodgom.
int modificacion_real(struct sock *sk, struct sock *newsk) { IMPLEMENTACIÓN DE LA POLÍTICA DE GESTIÓN DE COLAS}
✗ El módulo desarrollado, por mucho se trate de un módulo que es llamado
a través de un garfio, sigue siendo un módulo y por tanto debe contener
las funciones int_module y cleanup_module:
□ La función init_module debe encargarse de almacenar una copia
de la de la estructura access_cont, para en la descarga del módulo
poder restaurarla a su valor anterior y también de apuntar el
puntero a función del interior de la estructura a la función de este
módulo que implementa la política de gestión de colas.
□ La función cleanup_module se encarga para apuntar de nuevo la
estructura access_cont al valor que tenía antes de que se llamara
al módulo dejando de esta forma el núcleo exactamente igual a
como estaba antes de ser cargada la política de gestión de colas.
61
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
int init_module(){ access_cont_backup=access_cont; access_cont.modificacion=&modificacion_real; printk(KERN_ERR "Se feliz\n"); return 0; }
void cleanup_module() { access_cont=access_cont_backup; printk(KERN_ERR "Nos vamos ya sea contentos o descontentos\n"); }
3.3 Implementación de una política de gestión de colas.
Como se comentó en el primer capítulo, en este proyecto se pretende
también es necesario generar una política de gestión de colas básica para
demostrar que el entorno presentado es capaz de incluir estas políticas en el
núcleo de Linux. A continuación se ilustrará la implementación ejemplo de
política de gestión de colas que se comentó en el Apartado 1.4.2.
Se presenta la Fig. 3.8 en la que se apoyará la explicación de la política de
gestión desarrollada.
62
Fig. 3.8: Diagrama de flujo de la política de
gestión de colas ejemplo
n_act>n_umb
Descartar Nal azar
Sí No hacer nada
Recogida del tamañode la cola (Monitorizar)
NoSi
3.3 Implementación de una política de gestión de colas.
✗ La información necesaria para realizar la monitorización de la cola de
servicio, que en este caso consiste en controlar el número de conexiones
encoladas, se consigue a través de los argumentos de la función
modificación_real.
✗ El siguiente punto es comprobar si el tamaño actual de la cola supera al
del umbral. En nuestro código el umbral es establecido al tamaño máximo
de la cola menos uno. De este modo la política es llevada a cabo cuando la
última posición de la cola de conexiones establecidas va a ser ocupada.
//Recogemos el tamaño actual de la cola sk_ack_backlog = sk>sk_ack_backlog; //Recogemos el tamaño máximo de la cola sk_max_ack_backlog = sk>sk_max_ack_backlog;
//El umbral es el tamaño máximo de la cola menos uno tam_max= sk_max_ack_backlog1; if(sk_ack_backlog>tam_max){
SE LLEVA A CABO EL DESCARTE DE N CONEXIONES AL AZAR }
✗ El último paso consiste en no hacer nada si aún no se ha superado el
umbral o descartar N conexiones al alzar si esta condición se ha cumplido.
for (i=0;i<N;i++){ //Recogemos el tamaño actual de la cola sk_ack_backlog = sk>sk_ack_backlog; //Generamos un número aleatorio de entre las conexiones
buffer=&tam_max; get_random_bytes((void *)buffer, 4); buffer=(unsigned int *)buffer; posicion=*buffer; posicion=posicion%sk_ack_backlog + 1;
//Obtenemos el socket elegido con una función que puede //consultarse en el Anexo C
child=obtener_socket(sk,queue,posicion); if(child==NULL) {
printk(KERN_ERR "Es NULL\n"); } else {
//Si se devuelve el socket de la posición elegida //se cierra esa conexión con tcp_close.
tcp_close(child,0); printk(KERN_ERR "Tiro una conexion: Se feliz\n");
}
}
63
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
El uso de tcp_close, implica un envío de un segmento Fin tras el cual se
inicia el cierre de conexión. De la misma forma que se ha utilizado esta
función en la política diseñada, otra distinta podría descartar la conexión de la
cola sin comunicárselo al cliente. El código completo de los archivos
implicados en la creación del módulo se encuentra en el Anexo A.
Anexo 3.A: Códigos desarrollado para la generación del entorno.
Código del archivo hook.c#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/tcp.h> #include <net/ip.h> #include <net/inet_connection_sock.h> #include <net/rodgom.h> #include <linux/random.h>
MODULE_LICENSE("Dual BSD/GPL");
//Estructura que contiene el garfio extern struct access_control_rodgom access_cont; static struct access_control_rodgom access_cont_secundaria; //Función utilizada para cerrar la conexión o conexiones que //determine la política de gestión de colas extern void tcp_close(struct sock *sk,long timeout); //Función utilizada para elegir un número aleatorio. extern void get_random_bytes(void *buf, int nbytes);
/* Función obtener_socket. * Esta función está destinada a devolver el socket que se encuentra * en el interior de la peticion almacenada en la posición (posicion) * dentro de la lista enlazada queue */ static struct sock *obtener_socket(struct sock *sk,struct request_sock_queue *queue, int posicion) {
//Entero i que controlará la búsqueda en la lista enlazada que forman las peticiones.
int i; //Punteros a la estructuras request_sock gracias a los que
recogeremos la petición elegida. struct request_sock *elegido, *anterior; struct inet_sock* inet; struct sock *child;
__u16 lport; __be16 rport; __be16 sport;
//Nos colocamos al inicio de la cola de peticiones (en la cabeza)
64
Anexo 3.A: Códigos desarrollado para la generación del entorno.
elegido = queue>rskq_accept_head; for(i=1;i<posicion;i++){
//Si elegido es NULL estamos ante un error así que salimos ç //del método indicándolo if(elegido!=NULL){
//printk(KERN_ERR "Antes de dl_next\n"); anterior = elegido; elegido = anterior>dl_next;
}
else{ printk(KERN_ERR "Devolvemos NULL\n"); return NULL;
} } if(posicion == 1){
//Si el elegido es la cabeza. La cabeza la apuntamos al siguiente del elegido
queue>rskq_accept_head=elegido>dl_next; } else{
//Apuntamos dl_next del anterior al siguiente del elegido anterior>dl_next=elegido>dl_next;
} inet = inet_sk(elegido>sk); lport = inet>num; rport = inet>dport; sport = inet>sport;
printk(KERN_ERR "La conexion numero: %d tiene lport: %d rport: %d sport: %d\n",i,lport,rport,sport);
child=elegido>sk; sk_acceptq_removed(sk); __reqsk_free(elegido);
return child; }
int modificacion_real(struct sock *sk, struct sock *newsk) {
int sk_ack_backlog, sk_max_ack_backlog,qlen,error,tam_max,r; unsigned int *buffer; unsigned int posicion; struct request_sock *req, *req_prev; struct sock *child; struct request_sock **prev;
struct inet_sock *newinet = inet_sk(newsk); struct request_sock_queue *queue = &inet_csk(sk)>icsk_accept_queue; __be32 laddr = newinet>rcv_saddr; __u16 lport = newinet>num;
__be32 saddr = newinet>saddr; __be32 raddr = newinet>daddr; __be16 rport = newinet>dport; __be16 sport = newinet>sport;
error=0; qlen=reqsk_queue_len(queue);
65
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
sk_ack_backlog = sk>sk_ack_backlog; sk_max_ack_backlog = sk>sk_max_ack_backlog;
//Cada vez que se entre en la modificación se imprime una serie de información útil printk(KERN_ERR "Inclusion 1 en tcp_v4_syn_recv_sock sk_ack_backlog: %d sk_max_ack_backlog: %d lport: %d rport: %d sport: %d laddr: %d raddr: %d saddr: %d qlen: %d\n",sk_ack_backlog,sk_max_ack_backlog,lport,rport,sport,laddr,raddr,saddr,qlen);
//El umbral es 2/3 del tamaño máximo de la cola tam_max=(2*sk_max_ack_backlog)/3; if(sk_ack_backlog>tam_max){
//Generamos un número aleatorio para tirar una conexión de la cola.
buffer=&tam_max; get_random_bytes((void *)buffer, 4); buffer=(unsigned int *)buffer; posicion=*buffer; posicion=posicion%sk_ack_backlog + 1; //Obtenemos el socket elegido al azar entre los que hay en
la cola child=obtener_socket(sk,queue,posicion); if(child==NULL) {
printk(KERN_ERR "Es NULL\n"); } else {
//Descartamos la conexión elegida al azar tcp_close(child,0); printk(KERN_ERR "Tiro una conexion: Se feliz\n");
}
}
return error;
}
static int garfio_init(void) { access_cont_secundaria=access_cont; access_cont.modificacion=&modificacion_real; printk(KERN_ERR "Se feliz\n"); return 0; }
static void garfio_exit(void) { access_cont=access_cont_secundaria; printk(KERN_ERR "Nos vamos ya sea contentos o descontentos\n"); }
module_init(garfio_init); module_exit(garfio_exit);
66
Anexo 3.A: Códigos desarrollado para la generación del entorno.
Código del archivo rodgom.h#include <net/sock.h>
//Definición de la estructura struct access_control_rodgom {
int (*modificacion) (struct sock *sk,struct sock *newsk);
};
inline int modificacion_tonta(struct sock *sk,struct sock *newsk){
printk(KERN_ALERT "Estoy pasando por tontolandia\n"); return 0;
}
Código del archivo net/ipv4/tcp_ipv4.c#include <net/rodgom.h> ...struct access_control_rodgom access_cont = {
.modificacion=modificacion_tonta,
};
EXPORT_SYMBOL(access_cont);...struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req, struct dst_entry *dst)
{ struct inet_request_sock *ireq; struct inet_sock *newinet; struct tcp_sock *newtp; struct sock *newsk;
#ifdef CONFIG_TCP_MD5SIG struct tcp_md5sig_key *key;
#endif
if (sk_acceptq_is_full(sk)) goto exit_overflow;
if (!dst && (dst = inet_csk_route_req(sk, req)) == NULL) goto exit;
newsk = tcp_create_openreq_child(sk, req, skb); if (!newsk)
67
Capítulo 3: Implementación del entorno para la inclusión de medidas de defensa en el núcleode Linux
goto exit;
newsk>sk_gso_type = SKB_GSO_TCPV4; sk_setup_caps(newsk, dst);
newtp = tcp_sk(newsk); newinet = inet_sk(newsk); ireq = inet_rsk(req); newinet>daddr = ireq>rmt_addr; newinet>rcv_saddr = ireq>loc_addr; newinet>saddr = ireq>loc_addr; newinet>opt = ireq>opt; ireq>opt = NULL; newinet>mc_index = inet_iif(skb); newinet>mc_ttl = ip_hdr(skb)>ttl; inet_csk(newsk)>icsk_ext_hdr_len = 0; if (newinet>opt)
inet_csk(newsk)>icsk_ext_hdr_len = newinet>opt>optlen; newinet>id = newtp>write_seq ^ jiffies;
tcp_mtup_init(newsk); tcp_sync_mss(newsk, dst_mtu(dst)); newtp>advmss = dst_metric(dst, RTAX_ADVMSS); tcp_initialize_rcv_mss(newsk); //rodgom access_cont.modificacion(sk,newsk); //rodgom
#ifdef CONFIG_TCP_MD5SIG /* Copy over the MD5 key from the original socket */ if ((key = tcp_v4_md5_do_lookup(sk, newinet>daddr)) != NULL) {
/* * We're using one, so create a matching key * on the newsk structure. If we fail to get * memory, then we end up not copying the key * across. Shucks. */ char *newkey = kmemdup(key>key, key>keylen, GFP_ATOMIC); if (newkey != NULL)
tcp_v4_md5_do_add(newsk, inet_sk(sk)>daddr, newkey, key>keylen);
} #endif
__inet_hash(&tcp_hashinfo, newsk, 0); __inet_inherit_port(&tcp_hashinfo, sk, newsk);
return newsk;
exit_overflow: NET_INC_STATS_BH(LINUX_MIB_LISTENOVERFLOWS);
exit: NET_INC_STATS_BH(LINUX_MIB_LISTENDROPS); dst_release(dst); return NULL;
}
68
Anexo 3.A: Códigos desarrollado para la generación del entorno.
Código del archivo Makefile# If KERNELRELEASE is defined, we've been invoked from the # kernel build system and can use its language. ifneq ($(KERNELRELEASE),)
objm := hook.o # Otherwise we were called directly from the command # line; invoke the kernel build system. else
KERNELDIR ?= /lib/modules/$(shell uname r)/build PWD := $(shell pwd)
default: $(MAKE) C $(KERNELDIR) M=$(PWD) modules
endif
69
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
Una vez que se han ilustrado los conceptos básicos del modelo de servidor y
los ataques DoS contra servidores, presentadas las estructuras del núcleo de
Linux que intervienen en el desarrollo de un entorno para la inclusión de
políticas de gestión de colas y analizado en sí el entorno y el método utilizado
para incluirlo en el núcleo del sistema operativo Linux, se puede abordar el
aspecto de evaluación del desarrollo realizado.
Es trascendental probar que el entorno presentado es capaz de incluir en el
núcleo de Linux cualquier medida de seguridad frente a ataques DoS que esté
basada en una política de gestión de colas. Esto se llevará a cabo mediante la
evaluación de resultados obtenidos ante la inclusión de la política de gestión
de colas cuya implementación se discute en el término del capítulo anterior.
No se pretende, por tanto, en el presente capítulo, demostrar que la política
de gestión de colas implementada es la mejor solución existente para mitigar
los efectos de los ataques DoS, ni siquiera que es realmente efectiva, lo que
se pretende demostrar es que el entorno desarrollado, objetivo real del
proyecto, es capaz de incluir esta política en el núcleo de Linux, consiguiendo
que su funcionamiento sea el esperado.
Para descubrir el proceso llevado a cabo, para la evaluación ,este capítulo
se divide en tres secciones. En el primer apartado se describe el desarrollo
realizado de trabajo en el que se ha probado el entorno y por consiguiente, en
el que se ha llevado a cabo la recogida de datos. En esta parte del capítulo
(Apartado 4.1) también se describe la aplicación desarrollada para probar la
política de seguridad implantada en el sistema gracias al entorno desarrollado.
71
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
En la segunda parte del capítulo (Apartado 4.2) se pasan a analizar todas las
variaciones introducidas en la configuración normal del sistema operativo
utilizado. Es importante resaltar que estas modificaciones no son necesarias
para que el entorno desarrollado sea capaz de implantar con éxito la política
deseada, sino que se han realizado para que la tarea de pruebas realizada con
la política de gestión de colas se pueda llevar a cabo de un modo más sencillo.
En la tercera y última parte (Apartado 4.3) se presentan los resultados de las
pruebas realizadas sobre la política de gestión de colas con el objetivo de
ilustrar de un modo más claro que el entorno para la implementación de
medidas de defensa frente a ataques DoS funciona correctamente.
4.1 Entorno de pruebas.
Para la realización de las pruebas ilustradas en el presente capítulo se ha
utilizado un ordenador portátil de la marca Airis con un procesador Intel ®
Pentium ® M a 1,86Ghz. En éste se instalado el sistema operativo Ubuntu en
su versión 8.04 Hardy Heron, siendo el núcleo Linux de versión 2.6.24-19.
Además se tiene otro núcleo compilado (en su misma versión) que puede
cargarse al inicio del sistema gracias al gestor de arranque Grub. El entorno de
escritorio utilizado es Gnome.
Para hacer posible la evaluación de la política de gestión de colas y por
ende, del entorno de inclusión de medidas de defensa ante ataques DoS ha
sido necesario implementar un sistema cliente-servidor multihebra. Éste
tendrá la finalidad de recoger datos del funcionamiento del sistema frente a
un ataque DoS de baja tasa, siendo aplicada la política de gestión de colas y
sin ser aplicada.
Se implementa un servidor en el archivo servidor.cpp que acepta
conexiones cada 5 segundos. Esto implica que cada 5 segundos, si hay alguna
conexión en la cola de conexiones establecidas (o en el modelo general en la
cola de servicio), se atenderá la primera conexión de la cola de modo que se
habilitará una posición más de ésta.
Se generan dos programas cliente. Uno de ellos será el atacante y otro el
cliente legítimo. Ambos clientes, para poder ser comparados deberán
encontrarse en el mismo contexto. Por este motivo, ambos tendrán la misma
tasa de generación de peticiones de conexión, siendo ésta 5 segundos. Esto
72
4.1 Entorno de pruebas.
provocará un desbordamiento de la cola de conexiones establecidas, ya que a
ésta llegará, en media, una petición de conexión cada 2.5 segundos, mientras
que la tasa de servicio es una cada 5 segundos. La cuestión consiste en
evaluar si ambos clientes tienen las mismas oportunidades de ser servidos.
Como es lógico, el atacante utilizará algún mecanismo que le permita tener
ventaja sobre el cliente legítimo y así dificultar o impedir las conexiones de
éste (objetivo del ataque DoS).
El cliente legítimo está implementado en el programa cliente_bueno.cpp. La
estructura de funcionamiento (Fig. 4.1) de éste queda descrita en los
siguientes puntos:
✗ El cliente crea una hebra. Esta hebra intenta realizar una conexión; si
puede realizarla actualiza la variable de peticiones servidas y la muestra
por pantalla junto a los intentos totales de conexión. En caso de no poder
realizar la conexión o de ser descartada tras realizarla no se realiza
ninguna acción.
✗ Tras crear la hebra se realiza una espera de un tiempo t que se ajusta a
una distribución de Poisson de media 5 segundos, antes de crear la
siguiente hebra. Este proceso se repite indefinidamente.
73
Fig. 4.1: Diagrama de flujo del cliente_bueno
Cliente_bueno.c
Crear hebra
Esperar una media de 5(distribución
Poisson)
Inicio hebra
Fin hebra
¿Servida?
Actualizaciónservidas
Intento de conexión
No
Si
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
El cliente atacante realiza un ataque de DoS a baja tasa contra el servidor
descrito con anterioridad. Todo el sistema está preparado, como se describe
en el siguiente punto, para que este ataque tenga éxito. El ataque consiste en
enviar una petición de conexión justo en los momentos en los que el servidor
libera una conexión de la cola. El conocimiento de esta liberación de la cola,
en la realidad, se consigue a través de un complejo mecanismo [1] [2]. En
nuestro caso, solamente se pretende simular que esto sucede así, de modo
que el servidor en los instantes en los que acepta una conexión y por tanto
libera una posición de la cola de conexiones establecidas, escribe en un
archivo llamado secreto. El cliente_malo (atacante) monitoriza este archivo y
en el instante en el que descubre una modificación en él, realiza un intento de
conexión con el servidor. De esta forma, el atacante logrará adelantarse al
cliente legítimo, capturando la inmensa mayoría de las posiciones de la cola,
denegando así su servicio. Otro dato a destacar es que, si el servidor acepta
conexiones cada 5 segundos y el cliente_malo realiza conexiones cuando el
servidor las acepta, el intento de realización de conexiones en el servidor se
producirá también cada 5 segundos. Podemos ver el diagrama de flujo que
realiza esta operación en la Fig. 4.2.
74
Fig. 4.2: Diagrama de flujo de cliente_malo.cpp
Cliente_malo.c
Si el archivosecreto se
modifica
Crear hebra
Inicio hebra
Fin hebra
¿Servida?
Actualizaciónservidas
Intento de conexión
No
Si
4.1 Entorno de pruebas.
A continuación describimos el proceso que realiza el programa
cliente_malo.cpp para emular el procedimiento descrito anteriormente:
✗ Monitoriza el archivo secreto para descubrir el momento en el que el
servidor libera una posición de la cola de conexiones aceptadas. En caso
de descubrir una modificación de este archivo pasa al siguiente paso.
✗ En este punto el cliente_malo crea una hebra para realizar un intento de
conexión con el servidor. El funcionamiento de la hebra es idéntico al
descrito en el cliente_bueno.
4.2 Modificaciones realizadas en el sistema
En la fase de pruebas se vio necesario modificar una serie de parámetros de
configuración en cuestiones de seguridad en redes, para que las aplicaciones
cliente-servidor diseñadas se comportaran del modo esperado. Por ejemplo, si
se ha supuesto que el cliente debe intentar realizar una conexión con una tasa
de un intento por cada cinco segundos es necesario que esta tasa se
mantenga así para que la información obtenida en la fase de pruebas sea
fiable.
✗ La primera modificación realizada en el sistema es la que permite cargar
el módulo que implementa el entorno desarrollado. Es necesario cargar la
imagen del núcleo compilada con las modificaciones mostradas en el
Apartado 3.2.3.
✗ La siguiente modificación tiene que ver con una orden de Linux que es
utilizada para modificar parámetros de configuración en tiempo de
ejecución. Las variables que se modifican mediante este parámetro son
tcp_syn_retries y tcp_synack_retries. Estas variables rigen el número de
retransmisiones permitidas para los paquetes syn y ack. Para mayor
control sobre las pruebas realizadas hacemos que estas dos variables
valgan cero, impidiendo las retransmisiones y garantizando que las tasas
de llegadas de conexión del cliente bueno tienen distribución de Poisson y
media 5 segundos. Para que esta modificación persista tras el reinicio del
sistema debe introducirse en el archivo de configuración. Este se
encuentra en /etc/sysctl.conf. En él se deben incluir dos nuevas
entradas una para cada variable a modificar:
75
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
net.ipv4.tcp_syn_retries=0 net.ipv4.tcp_synack_retries=0
✗ La próxima modificación de la configuración del sistema consiste en
encauzar los mensajes enviados por la función del núcleo printk,
que es similar a printf pero añadiéndole un nivel de prioridad que
indica la importancia del mensaje y por tanto donde debe ser
imprimido, si en la consola, o directamente guardado en un fichero,
etc. El archivo de configuración /etc/syslog.conf indica el lugar en
el que, dependiendo de la prioridad del mensaje, debe almacenarse.
Los mensajes incluidos en la modificación son todos de la prioridad
KERN_ERR. Se incluye una entrada para que los mensajes enviados
por la modificación realizada se dirijan a un mismo archivo.
kern.err /var/log/errProy.log
Sin las citadas modificaciones el entorno, es igualmente capaz de incluir en
el núcleo la política de gestión de colas deseada. Estas modificaciones se
realizan para tener un mayor control sobre la política de gestión de prueba,
para que las pruebas realizadas sean fiables. El objetivo de la nueva
configuración del sistema es que en la fase de pruebas sólo se generen las
peticiones aquí comentadas y no retransmisiones inesperadas que
modificarían la tasa de envío de peticiones.
4.3 Resultados de las pruebas realizadas
Las pruebas realizadas consisten en lanzar la aplicación servidora junto con
ambos clientes (bueno y malo), con el módulo descargado en primera
instancia. Tras un tiempo de ejecución suficientemente elevado para que la
información recogida sea relevante, se interrumpen todas las aplicaciones y se
lanzan de nuevo, pero en esta ocasión se carga el módulo lo que implica que
se aplicará la política de gestión de colas. El entorno cliente servidor imprime
en su ejecución unos mensajes indicando el número de peticiones servidas
con respecto a los intentos realizados.
Se realizan 2 pruebas: una con backlog a 2 y otra con backlog a 50, lo que
implica que el tamaño de la cola de conexiones establecidas es de 3 y 51
posiciones respectivamente. En las Fig. 4.3 y 4.4 se ilustran los resultados con
backlog a 2 y para un backlog de 50 se pueden ver en las Fig. 4.5 y 4.6.
76
4.3 Resultados de las pruebas realizadas
En las gráficas presentadas, el eje x representa el porcentaje de intentos de
conexión respecto del total (intentos_actuales/intentos_totales_finales) y el eje
y representa el porcentaje de peticiones servidas respecto a los intentos
totales. Esto quiere decir que todas las representaciones recorrerán todo el eje
x ya que van desde un porcentaje de intentos cero al total (100%). Se
presentan los resultados en porcentaje precisamente para que el eje x sea el
mismo tanto para el cliente_bueno como el cliente_malo. Esto se debe a que
no realiza los mismos intentos de conexión un cliente que el otro aún teniendo
la misma tasa. Al seguir esta tasa una distribución de Poisson, puntualmente,
el cliente_bueno, en ocasiones, realizará más de un intento cada cinco
segundo y en otras ocasiones menos.
Los resultados obtenidos para la ejecución de las pruebas, ya sea con
tamaño de cola 3 o 51, en el caso de no aplicarse la política de gestión de
colas, demuestran que el atacante o cliente_malo consigue realizar con éxito
su ataque DoS ya que el cliente no consigue realizar más conexiones desde la
saturación de la cola. Esto se debe a que el atacante captura con éxito todas
las posiciones que se liberan de la cola de conexiones establecidas y mantiene
siempre saturada dicha cola. Para el caso de backlog50, aún sin cargar la
política de seguridad, sucede lo mismo pero con la diferencia de que la cola de
conexiones establecidas es de un tamaño mucho mayor, permitiendo, de este
modo, observar el período en el que la cola es saturada. A partir de éste, el
atacante consigue capturar todas las posiciones que se liberan de la cola
evitando el acceso a ésta del cliente legítimo y denegando, por tanto, su
servicio. En las Fig. 4.3 y 4.5 puede verse que el cliente legítimo una vez se
han saturado las colas, no varía su porcentaje conexiones de servidas.
En el caso de ser cargada la modificación del núcleo y, por consiguiente, de
aplicarse la política de gestión de colas, el atacante no obtiene el mismo éxito
éxito en su ataque DoS, como puede verse en las Fig. 4.4 y 4.6. De hecho al
término de la ejecución de la aplicación, el porcentaje de conexiones servidas
respecto al de intentadas de ambos clientes ronda el 50 %, lo que implica que
la política ha conseguido otorgar las mismas posibilidades de acceso a la cola
a los dos clientes.
77
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
78
Fig. 4.4: Representación de los resultados con backlog a 2 y
aplicando la política
Fig. 4.3: Representación de los resultados con backlog a 2 y sin
aplicar la política
0 10 20 30 40 50 60 70 80 90 100
0
10
20
30
40
50
60
70
80
90
100
Resultados sin política
Backlog 2
Cliente bueno
Cliente malo
Porcentaje de intentos
Con
exio
nes
serv
idas
0 10 20 30 40 50 60 70 80 90 100
0
10
20
30
40
50
60
Resultados con polítciaBacklog 2
Cliente bueno
Cliente malo
Porcentaje de intentos
Con
exio
nes
serv
idas
4.3 Resultados de las pruebas realizadas
79
Fig. 4.5: Representación de los resultados con backlog a 50 y sin
política
Fig. 4.6: Representación de los resultados con backlog a 50 y
aplicando la política
0 10 20 30 40 50 60 70 80 90 1000
10
20
30
40
50
60
70
80
90
Resultados sin política
Backlog 50
Cliente bueno
Cliente malo
Porcentaje de intentos
Con
exio
nes
serv
idas
0 10 20 30 40 50 60 70 80 90 100
0
10
20
30
40
50
60
Resultados con políticaBacklog 50
Cliente bueno
Cliente malo
Porcentaje de intentos
Con
exio
nes
serv
idas
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
Una vez realizado el análisis de los resultados puede concluirse que la
política de gestión de colas utilizada ha servido para dificultar los efectos del
ataque DoS realizado. Aún así, lo que se pretende evaluar en este capítulo no
es la política de prueba implementada sino el correcto funcionamiento de la
modificación del núcleo que ha permitido incluir dicha política. A la vista de
estos resultados puede afirmarse que la política ha tenido efecto y, por tanto,
que la modificación del núcleo ha permitido incluirla correctamente.
Anexo 4.A Presentación del código del modelo cliente-servidor.
Archivo servidor.cpp// Estos son los ficheros de cabecera usuales #include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <iostream>#include <ctime>#include <string>#include <fstream>using namespace std;#define BACKLOG 0 // El número de conexiones permitidas
//Variable en la que se almacenará el texto enviadochar buf[1000];
static int i;//Definimos un fichero que será solamente de escriturastatic ofstream fich;
long escribir(long posicion,int i){
//Variable en la que almacenaremos la posicion final del puntero de //escritura. long final=0; ofstream fich; fich.open("/home/rafael/Escritorio/Proyecto/secreto");
//Escribimos en el fichero y recogemos el puntero de escritura. //fich.seekp(posicion); fich.seekp(posicion); fich << "He aceptado una conexión. Totales " << i << " Se feliz\n"; final = fich.tellp();
//fich.close(); return final;
80
Anexo 4.A Presentación del código del modelo cliente-servidor.
}
int main(int argc, char *argv[]){ //Los ficheros descriptores int fd, fd2; unsigned short int puerto; struct sockaddr_in server; // para la información de la dirección del servidor int backlog = BACKLOG; struct sockaddr_in client; // para la información de la dirección del cliente socklen_t sin_size;
//Variable para almacenar la posición del puntero de escritura del //fichero long final_fich = 0;
/*fich.open("/home/rafael/Escritorio/Proyecto/secreto"); fich << "He aceptado una conexión. Totales " << i << " Se feliz\n"; fich.close(); fich.open("/home/rafael/Escritorio/Proyecto/secreto");*/
if (argc == 3){
backlog=atoi(argv[2]);}else{
// Si el programa no tiene dos argumentos se ha producido un error ya que argv[0] es el // nombre del programa y el segundo argumento es el puerto. cout << "Uso: "<<argv[0]<<" <PUERTO> <BACKLOG>"<<endl; exit(1);
} cout << "El backlog vale: " << backlog << endl;
// A continuación la llamada a socket() if ((fd=socket(AF_INET, SOCK_STREAM, 0)) == 1 ) { cout << "Error en socket()" << endl; exit(1); }
server.sin_family = AF_INET; //Se recoge el valor de puerto pasado como argumento puerto=atoi(argv[1]); server.sin_port = htons(puerto); server.sin_addr.s_addr = INADDR_ANY; // INADDR_ANY coloca nuestra dirección IP automáticamente memset(&(server.sin_zero),0,8); // escribimos ceros en el reto de la estructura
// A continuación la llamada a bind() if(bind(fd,(struct sockaddr*)&server,sizeof(struct sockaddr))==1) { cout << "Error en bind()" << endl;
81
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
exit(1); }
if(listen(fd,backlog) == 1) { // llamada a listen() cout << "Error en listen()" << endl; exit(1); } else{
cout << "Escucho el fd" << fd << endl; } //ojo i=0; while(1){ //Cada cinco segundos acepta una conexión. sleep(5); sin_size=sizeof(struct sockaddr_in); // A continuación la llamada a accept() if ((fd2 = accept(fd,(struct sockaddr *)&client,&sin_size))==1) { cout << "Error en accept()" << endl; exit(1); }
//Como hemos aceptado una conexión hemos liberado un espacio en la
//cola de peticiones con lo que hemos de escribir en el archivo //secreto para que el cliente malicioso puede adelantarse al //cliente bueno. final_fich=escribir(final_fich,i+1); //String.valueOf(4);
cout << "Se obtuvieron " << i+1 <<". Puerto: " << ntohs(client.sin_port) << endl; //send(fd2,strcat(buf,(const char *)ntohs(client.sin_port)),22,0); sprintf( buf, "Bienvenido %d", ntohs(client.sin_port) ); send(fd2,(const char *)buf,22,0); // que enviará el mensaje de bienvenida al cliente close(fd2); // cierra fd2 i++;
} close(fd);}
Archivo cliente_bueno.cpp#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <netdb.h>#include <string>#include <ctime>#include <iostream>#include <unistd.h>#include <math.h>#include <stdlib.h>#include <time.h>
82
Anexo 4.A Presentación del código del modelo cliente-servidor.
#include <fstream>#include <pthread.h>
using namespace std;/* netbd.h es necesitada por la estructura hostent */
#define MAXDATASIZE 10000/* El número máximo de datos en bytes */
//Variables globales//Ficheros descriptores int numbytes,fd;int fdd[10000];int puerto_;
//Variable para almacenar el puertounsigned short int puerto;
//Intentos de conexiónint intentos = 0;
//Variable que indica el fichero descriptor en es que estamosint i = 0;
//Estructura que recibirá información sobre el nodo remoto struct hostent *he;
//Estructura que recibirá información sobre la dirección del servidor struct sockaddr_in server;
//Identificadores de la hebrapthread_t id[1000];
//Mutex para acceder a la variable global servidas.//pthread_mutext_t mutex;
//Inicializamos el mutex//pthread_mutex_init(mutex,NULL);
//Tiempo en media entre cada proceso y por tanto cada peticiónint media = 5;
//int conex;
//Tiempo que espera el proceso padre antes de generar al siguiente hijodouble t;
//Variable global que almacena las conexiones que han tenido éxito.int servidas = 0; //Variable para almacenar la posición del puntero de escritura del//ficherolong final_fich = 0;
//Variable en la que se almacenarán los segundos del reloj para//hacer aleatoria realmente los números entregados por rand()time_t segundos;
83
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
int obtener_puerto(int sockfd){ struct sockaddr_in ss; struct sockaddr_in *in; int puerto; socklen_t len; len=sizeof(ss); if(getsockname(sockfd, (struct sockaddr *)&ss, &len) < 0) return 1; in=(struct sockaddr_in *)&ss; puerto = ntohs(in>sin_port); return puerto; }
//Función que ejecutarán las hebras.void* ejecuta_hebra (void* identificador){ //Variable en la que se almacenará el texto recibido char buf[MAXDATASIZE]; //Identificador int actual = (int)identificador;
//Llamada a socket() if ((fdd[actual]=socket(AF_INET, SOCK_STREAM, 0))==1){ //cout << "socket() error" << endl; return 0; }
server.sin_family = AF_INET; server.sin_port = htons(puerto); //he>h_addr pasa la información de ``*he'' a "h_addr" server.sin_addr = *((struct in_addr *)he>h_addr); memset(&(server.sin_zero),0,8); //Llamada a connect() if(connect(fdd[actual], (struct sockaddr *)&server,sizeof(struct sockaddr))==1){ //cout << "connect() error " << "Intentos " << i+1 << " Puerto: " << puerto_ << endl; return 0; } else { //puerto_ = obtener_puerto(fdd[i]); //cout << "Intentos " << i+1 << " Puerto: " << puerto_ << endl; } puerto_ = obtener_puerto(fdd[actual]); cout << " Lanzada hebra " << actual+1 <<" Puerto: " << puerto_<< endl; //Llamada a recv() if ((numbytes=recv(fdd[actual],buf,MAXDATASIZE,0)) == 1){ //cout << "Error en recv()" << endl; return 0; } //Si el buffer de recepción está vacío esto implica que
84
Anexo 4.A Presentación del código del modelo cliente-servidor.
//la conexión ha sido tirada. if (buf[0] != NULL && buf[1]!=NULL&& buf[2]!=NULL){ //Abrimos el fichero en el que escribiremos //Bloqueamos el mutex //pthread_mutex_lock(mutex); //Como la recepción del mensaje ha sido exitosa añadimos uno a servidas servidas++; //Desbloqueamos el mutex //pthread_mutex_unlock(mutex); } puerto_ = obtener_puerto(fdd[actual]); cout << "Servidas: " << servidas << " Intentos: " << i+1 << " Puerto: " << puerto_<< " Mensaje: " << buf << endl; //Cerramos fd close(fdd[actual]); return 0;}
int main(int argc, char *argv[]){ //Recogemos el valor actual del reloj time(&segundos); //Cambiamos la semilla de la función rand para aleatorizarla realmente. srand((unsigned int) segundos); if (argc !=3) { //Nuestro programa necesitará la IP y el puerto cout << "Uso: " << argv[0] << " <Dirección IP> <PUERTO>" << endl; exit(1);
}
if ((he=gethostbyname(argv[1]))==NULL){ //Llamada a gethostbyname() cout << "gethostbyname() error" << endl; exit(1); }
//Recogemos el puerto sobre el que realizaremos la conexion puerto = atoi(argv[2]); //Conexiones //conex = atoi (argv[3]); //Inicializamos el índice del array de ficheros descriptores a 0 i=0; while (1){ intentos=i; pthread_create(&id[intentos],NULL,ejecuta_hebra,(void*)intentos); //Calculamos el tiempo de espera que cumple para procesos //de Poisson de media de 5 segundos. t=(1)*(media)*log((double)rand()/RAND_MAX); //Dormimos el proceso padre durante t segundos con una media de 5.
85
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
sleep(t); //Aumentamos en uno el contador de ficheros descriptores. i++;
}}
Archivo cliente_malo.cpp#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <netdb.h>#include <string>#include <ctime>#include <fstream>#include <iostream>#include <pthread.h>using namespace std;/* netbd.h es necesitada por la estructura hostent */
#define NUMCONEXIONES 4#define MAXDATASIZE 10000/* El número máximo de datos en bytes */
//Ficheros descriptores int numbytes,fd;int puerto_;
//Conexiones servidas con éxito.static int servidas = 0;int fdd[1000];
//Variable para almacenar la posición del puntero de lectura del//ficherolong final_fich = 0;
//Variable para almacenar el puertounsigned short int puerto;
//Intentos de conexiónint intentos = 0;
//Variable que indica el fichero descriptor en es que estamosint i = 0;
//Estructura que recibirá información sobre el nodo remotostruct hostent *he;
//Estructura que recibirá información sobre la dirección del servidor struct sockaddr_in server;
//Definimos un fichero que será solamente de escrituraofstream fich;
86
Anexo 4.A Presentación del código del modelo cliente-servidor.
//Identificadores de la hebrapthread_t id[1000];
int obtener_puerto(int sockfd){ struct sockaddr_in ss; struct sockaddr_in *in; int puerto; socklen_t len; len=sizeof(ss); if(getsockname(sockfd, (struct sockaddr *)&ss, &len) < 0) return 1; in=(struct sockaddr_in *)&ss; puerto = ntohs(in>sin_port); return puerto; }
long leer(long posicion){//No hagas que posicion sea 0 sino ios::in string frase; long final; ifstream fi; //size_t encontrado; //cout << "Dentro de leer" << endl; fi.open("/home/rafael/Escritorio/Proyecto/secreto"); //Posicionamos el puntero al final del archivo para inicializar final. fi.seekg(0,ios::end); final=fi.tellg(); fi.close(); //Si la posición es cero hacemos posición igual //a final para esperar un cambio en el archivo if (posicion == 0) posicion=final; while (final == posicion) { fi.open("/home/rafael/Escritorio/Proyecto/secreto", ios::in); fi.seekg(0,ios::end); final=fi.tellg(); fi.close(); } return final;}
//Función que ejecutarán las hebras.void* ejecuta_hebra (void* identificador){ //Variable en la que se almacenará el texto recibido char buf[MAXDATASIZE]; //Identificador int actual = (int) identificador;
//Llamada a socket() if ((fdd[actual]=socket(AF_INET, SOCK_STREAM, 0))==1){ //cout << "socket() error" << endl;
87
Capítulo 4: Evaluación de políticas de gestión de colas para la defensa.
//exit(1); return 0; }
server.sin_family = AF_INET; server.sin_port = htons(puerto); //he>h_addr pasa la información de ``*he'' a "h_addr" server.sin_addr = *((struct in_addr *)he>h_addr); memset(&(server.sin_zero),0,8);
//Llamada a connect() if(connect(fdd[actual], (struct sockaddr *)&server,sizeof(struct sockaddr))==1){ //cout << "connect() error " << "Intentos " << i +1 << endl; return 0; } else { //cout << "Intentos " << i +1 << endl; } puerto_ = obtener_puerto(fdd[actual]); cout << " Lanzada hebra " << actual+1 <<" Puerto: " << puerto_<< endl; //Llamada a recv() if ((numbytes=recv(fdd[actual],buf,MAXDATASIZE,0)) == 1){ //cout << "Error en recv()" << endl; return 0; } //Si el buffer de recepción está vacío esto implica que //la conexión ha sido tirada. if (buf[0] != NULL && buf[1]!=NULL&& buf[2]!=NULL){ servidas++; } puerto_ = obtener_puerto(fdd[actual]); cout << "Servidas: " << servidas << " Intentos: " << i+1 << " Puerto: " << puerto_<< " Mensaje: " << buf << endl; //Cerramos fd close(fdd[actual]); return 0;}
int main(int argc, char *argv[]){
if (argc !=3) {
//Nuestro programa necesitará la IP y el puerto cout << "Uso: " << argv[0] << " <Dirección IP> <PUERTO>" << endl; exit(1);
}
if ((he=gethostbyname(argv[1]))==NULL){ //Llamada a gethostbyname() cout << "gethostbyname() error" << endl; exit(1);
88
Anexo 4.A Presentación del código del modelo cliente-servidor.
} //Recogemos el puerto sobre el que realizaremos la conexion puerto = atoi(argv[2]); while (1) {
//Leemos del fichero que indica si el servidor ha tirado o no una //conexión. final_fich = leer(final_fich); //Creamos un proceso hijo. //cout << "Creamos un proceso hijo" << endl; intentos=i; pthread_create(&id[intentos],NULL,ejecuta_hebra,(void *)intentos); //Aumentamos en uno el contador de ficheros descriptores. i++;
}
}
89
Capítulo 5: Planificación del proyecto.
En este capítulo se pretende discutir la planificación que se ha seguido para
la realización del presente proyecto y el modo en el que éste ha sido realizado.
En la primera parte del capítulo (Apartado 5.1) se presentan los objetivos
planteados y las tareas en las que estos objetivos se han dividido. En la
segunda parte (Apartado 5.2) se indican los recursos que ha sido necesario
utilizar para la consecución definitiva de los objetivos que se plantearon. En la
última parte (Apartado 5.3) se muestra la planificación temporal realizada
mediante un diagrama de Gantt.
5.1 Objetivos y tareas en las que se han dividido
Los objetivos que se plantearon para este proyecto se detallan a
continuación:
✗ Llevar a cabo un estudio en profundidad de la problemática actual de los
ataques DoS contra aplicaciones para desde esta perspectiva poder
plantear alguna solución.
✗ Una vez determinada la solución (políticas de gestión de colas) es posible
realizar un estudio de la gestión del establecimiento de conexión del
protocolo TCP en el núcleo de Linux. Localizando los lugares clave en los
que el proyecto se centrará a la poste.
✗ Implementación de un entorno para la inclusión de políticas de gestión de
colas que impidan o dificulten los efectos nocivos de los ataques DoS.
91
Capítulo 5: Planificación del proyecto.
✗ Garantizar que este entorno es verdaderamente capaz de incluir en el
núcleo de Linux una política de gestión de colas. Para esto es necesaria la
existencia de una fase de pruebas.
✗ Realizar una documentación en la que se describa el trabajo realizado.
Para la realización de cada uno de estos objetivos se plantearon tareas
específicas cuya finalización supusiera la consecución de un objetivo. Estas
tareas son:
✗ Análisis del estado del arte.
□ Análisis del problema: se procede al estudio de la problemática de
los ataques DoS en la actualidad.
□ Búsqueda de soluciones: se estudian posibles soluciones frente a
la problemática anterior.
✗ Estudio del núcleo de Linux.
□ Estudio de TCP en Linux: una vez resuelta la solución se estudia en
el núcleo de Linux el protocolo TCP, con énfasis en el proceso para
el establecimiento de la conexión, al ser el proceso clave en la
solución propuesta (políticas de gestión de colas).
□ Estudio de las estructuras implicadas: es necesario conocer el
funcionamiento de las estructuras que intervienen en el
establecimiento de la conexión para poder actuar sobre ellas.
□ Búsqueda del lugar a modificar: se investiga el núcleo para hallar
la localización en la que se incluirá la modificación que permita la
inclusión de políticas de gestión de colas.
✗ Desarrollo del entorno
□ Estudio de las modificaciones en el núcleo: se estudian las
posibilidades de modificación del núcleo de Linux para hallar una
que cumpla las necesidades del presente proyecto.
□ Implementación mediante garfios: se implementa, a través de la
modificación encontrada (garfios), el entorno objetivo de este
trabajo.
□ Implementación de una política de gestión de colas: con objeto de
probar el correcto funcionamiento del entorno desarrollado.
92
5.1 Objetivos y tareas en las que se han dividido
✗ Fase de pruebas
□ Implementación del cliente-servidor: se desarrolla una aplicación
cliente-servidor que será la base de las pruebas en esta fase.
□ Adecuación de la configuración: se modifican ciertas variables del
sistema para asegurar la fiabilidad de las pruebas.
□ Recogida de datos y generación de gráficas: se efectúan las
pruebas en sí recogiéndose los datos necesarios para demostrar
que el entorno es capaz de incluir políticas de gestión de colas que
mitigan los efectos de los ataques DoS.
✗ Fase de documentación
□ Redacción: se redacta el trabajo realizado para su posterior
presentación.
□ Revisión: se revisa este documento con objeto de mejorarlo en
cuestiones de coherencia argumentativa y de corrección sintáctica.
5.2 Recursos utilizados
Los recursos utilizados para la consecución de los objetivos citados en el
apartado anterior se han dividido en dos: recursos humanos y recursos
materiales.
5.2.1 Recursos humanos
En este proyecto los recursos humanos utilizados se resumen en dos
personas principalmente:
✗ Un director del proyecto y persona que ha guiado el proceso de
organización de tareas y planificación temporal. También ha sido el
creador de la idea desarrollada y se ha encargado de revisar la presente
memoria.
✗ Un alumno se ha encargado de investigar el camino para conseguir los
objetivos planteados, es autor de la solución propuesta y de la presente
memoria.
93
Capítulo 5: Planificación del proyecto.
5.2.2 Recursos materiales
Para la ejecución del presente proyecto se ha necesitado unos recursos
materiales que se pueden agrupar en dos tipos: hardware y software.
Recursos hardware
El hardware utilizado en este proyecto ha consistido en un ordenador
portátil de la marca Airis con las siguientes características:
✗ Procesador Intel ® Pentium ® M a 1,86Ghz.
✗ Sistema operativo Ubuntu en su versión 8.04 Hardy Heron.
✗ Contiene dos imágenes del núcleo Linux de versión 2.6.24-19. Una
compilada con las modificaciones planteadas en el proyecto y la otra
instalada por los mecanismos tradicionales.
Recursos software
Los programas utilizados para el desarrollo del proyecto completo,
incluyendo, por tanto, esta memoria son:
✗ Geany: un editor de textos utilizado para todas las tareas de
programación.
✗ vim: es un editor de textos que carga el texto en la terminal. Se ha
utilizado para visualizar el contenido de archivos del núcleo con mayor
agilidad y para modificar los parámetros de configuración en
/etc/syslog.conf y /etc/sysctl.conf.
✗ OpenOffice 2.4 Writer: se ha utilizado este software para redactar la
memoria del proyecto.
✗ OpenOffice 2.4 Impress: esta herramienta ha sido utilizada para la
creación de las imágenes presentes en este documento.
✗ OpenOffice 2.4 Calc: este programa se ha utilizado en la creación de las
gráficas ilustradas en el Capítulo 4.
✗ Planner 0.14.2: utilizado para generar la planificación temporal.
94
5.2 Recursos utilizados
✗ Wireshark 1.0: esta herramienta ha hecho posible analizar el tráfico de red
generado en la fase de pruebas para de esta forma contrastar que todo
funcionaba de la manera esperada.
5.3 Planificación temporal y diagrama de Gantt
Una vez se han presentado los objetivos y sus respectivas tareas y los
recursos que se han utilizado para llevarlas a cabo, se puede presentar la
organización temporal que se ha seguido. Para esto se ha visto útil presentar
el diagrama de Gantt realizado.
Se presenta una tabla resumen con la información detallada en el diagrama
de Gantt (Fig. 5.1).
95
Capítulo 5: Planificación del proyecto.
96
Fig. 5.2: Diagrama de Gantt del proyecto
Capítulo 6: Conclusiones
En este capítulo se presentan las principales conclusiones que se derivan de
su realización. Puede entenderse como un resumen de los conceptos y
resultados más importantes que se han ilustrado en los capítulos anteriores.
En la primer lugar (Apartado 6.1) se muestran las principales conclusiones que
se derivan de los capítulos precedentes. Para finalizar (Apartado 6.2), se
ilustran las líneas de posibles trabajos futuros que puedan continuar con el
realizado en este proyecto fin de carrera.
6.1 Conclusiones acerca del trabajo realizado.
Las conclusiones extraídas de la realización de este proyecto son las
siguientes:
✗ Se ha propuesto un modelo de servidor modificado con respecto al
propuesto en [2]. La modificación consiste en la adicción de un módulo
nuevo que realiza la tarea de gestionar las colas de servicio.
✗ Se ha propuesto una solución para la problemática asociada a los ataques
DoS contra aplicaciones: las políticas de gestión de colas.
✗ Un entorno de trabajo flexible en el cual es posible implementar cualquier
medida de defensa frente a ataques DoS basada en políticas de gestión de
colas.
✗ Se ha diseñado la arquitectura software para la implementación. Para ello
se ha propuesto el uso de módulos del núcleo, apoyados en la utilización
de los llamados garfios [2].
97
Capítulo 6: Conclusiones
✗ Se ha ilustrado el funcionamiento de la implementación del entorno,
detallando todos sus aspectos de funcionamiento y justificando las
decisiones de diseño adoptadas.
✗ En la búsqueda del lugar en el código del núcleo de Linux en el que
realizar los desarrollos que han permitido la implementación del citado
entorno se ha ilustrado de forma detallada la manera en que el núcleo
gestiona el establecimiento de conexiones TCP.
✗ Para probar el entorno implementado se ha propuesto una política de
gestión de colas básica, que ha sido utilizada para la evaluacióndel
funcionamiento del desarrollo realizado.
✗ Se ha presentado un conjunto de resultados con los que se muestra que la
política de gestión de colas probada mejora el comportamiento del
servidor los efectos de los ataques DoS. Pero sobretodo estos resultados
demuestran que el marco desarrollado posibilita la inclusión de políticas
de gestión de colas en el núcleo.
6.2 Líneas futuras
Durante el desarrollo de este proyecto se han descubierto caminos para la
realización de avances interesantes en las que se tiene como base el trabajo
realizado en este proyecto.
✗ Adaptación del entorno para la inclusión de medidas de defensa frente a
ataque DoS basadas en políticas de gestión de colas a otros sistemas
operativos de código abierto como puede ser OpenBSD.
✗ Generación de políticas de seguridad que introduzcan probabilidades de
descarte en casos concretos, que utilicen mecanismos de detección de
atacantes para realizar el descarte de la conexión, etc. Evaluación de
estas políticas para determinar aquella que dificulte de la mejor forma
posible los efectos de la realización de un ataque DoS.
98
Bibliografía
[1] Maciá Fernández, G., Díaz-Verdejo, J.E., García-Teodoro, P., Evaluation of a Low-Rate DoS
Attack Against Application Servers, Aceptado en Computers & Security, 2008.
[2] Maciá-Fernández G., Díaz-Verdejo JE., García-Teodoro P., Evaluation of a low-rate DoS attack
against iterative servers. Computer Networks 2007;51(4):1013-30
[3] Wright, C., Cowan, C., Morris, J., Smalley, S., Kroah-Hartman, G., Linux security modules:
General security support for the linux kernel proceeding of the 11th USENIX Security Symposium
[4] Stevens, W.R., Fenner, B., and Rudoff, A. M., (2004) UNIX Network Programming, the sockets
networking API, Volume 1. Addison-Wesley Professional, 3 edition, ISBN: 0-13-141155-1.
[5] Kuzmanovic, A., Knightly E., Low-rate TCP-targeted denial of service attacks and counter
strategies. IEEE/ACM Trans Network August 2006;14(4):683-96
[6] Guirguis M, Bestavros A, Matta I, Zhang Y. Reduction of quality (RoQ) attacks on internet
end-systems, INFOCOM 2005. In: 24th Annual joint conference of the IEEE computer and
communications societies; 2005, p. 1362-72
[7] Guirguis M., Bestavros A., Matta I., Zhang Y., Reduction of quality (RoQ) attacks on dynamic
load balancers: vulnerability assessment and design tradeoffs, INFOCOM 2007. In: 26th IEEE
international conference on computer communications; 2007, p. 857-65
[8] Guirguis M., Bestavros A., Matta I., Explaining the transients of adaptation for RoQ attacks on
internet resources. Proceedings of International Conference on Network Protocols 2004: 184-95.
[9] J. Mirkovic, P., Reiher, A taxonomy of DdoS attack and DdoS defense mechanisms, ACM
SIGCOMM Computer Communication Review 34 (2) (2004) 312-321
[10] Geng X, Whinston AB. Defeating distributed denial of service attacks, IEEE IT Professional
2000;2(4):36-42
[11] C. Douligeris, A. Mitrokotsa, DdoS attacks and defense mechanisms: classification and
state-of-the-art, Computer Networks 44 (2004) 643-646
[12] X. Geneng, A.B. Whinston, Defeating Distributed Denial of Service attacks, IEEE IT
Professional 2 (4) (200) 36-42
[13] S. Axelsson, Intrusion detection systems: a survey and taxonomy, Department of Computer
Engineering, Chalmers University, Goteborg, Sweden, Technical Report 99-15
ACTA DE VALORACIÓN DEL PROYECTO
El tribunal constituido para la evaluación del PFC titulado:
DESARROLLO DE UN ENTORNO PARA LA INCLUSIÓN DE MEDIDAS
DE DEFENSA FRENTE A ATAQUES DE DENEGACIÓN DE SERVICIO
BASADAS EN POLÍTICAS DE GESTIÓN DE COLAS.
Relizado por el alumno: Rafael Alejandro Rodríguez Gómez
Y dirigido por el tutor: Gabriel Maciá Fernández
Ha resuelto asignarle la calificación de:
SOBRESALIENTE (9 - 10 puntos)
NOTABLE (7 - 8.9 puntos)
APROBADO (5 - 6.9 puntos)
SUSPENSO
Con la nota1: puntos
El Presidente:______________________________
El Secretario:______________________________
El Vocal:___________________________________
Granada, a __ de ___________ de 200__
1 Solamente con un decimal