Defendiendo la disponibilidad de un servicio con iptables

La famosa sigla CID define el trípode en el cual se sustenta la seguridad informática: Confidencialidad, Integridad, Disposnibilidad.

Es decir, al interrumpir la disponibilidad de un servicio se quiebra la seguridad de uno o más servicios.

El famoso ataque de denegación de servicio puede colgar aplicaciones de un servidor o inundarlos de tráfico afectando seriamente el rendimiento. Muchas veces ese tipo de ataque proviene de direcciones IP falsificadas y desde más de un origen. Existen variantes de este tipo de agresiones que pueden incluso aprovechar vulnerabilidades de los dispositivos víctima, de manera tal que los atacantes pueden por ejemplo hacer reemplazar el firmware original por otro malicioso.

El módulo xt_recent

Como vimos, bloquear o limitar el tráfico de una lista estática de direcciones IP no es conveniente. Exige en cambio adoptar una defensa dinámica y proactiva.

El módulo de iptables xt_recent es una de las opciones a las que podemos recurrir en estos casos. Fue creado por Stephen Frost en 2003^1   y lo mantiene actualmente el equipo de iptables.^2

El presente artículo intenta mostrar de manera sencilla y clara el funcionamiento del módulo, no es un curso de iptables ni una receta para diseñar una política de firewall. El propósito es mostrar como el propio kernel posee instrumentos para defenderse contra un (D)DOS.

Como funciona y usos

El modulo xt_recent o más comúnmente conocido recent crea una conjunto de direcciones IPs de acuerdo a ciertos criterios que queramos establecer. Por ejemplo podríamos crear un regla que coincida con paquetes de tipo icmp pertenecientes a conexiones nuevas.

Es cierto, tal vez no podamos hablar estrictamente de conexiones nuevas para este tipo de protocolo. Pero recordemos que iptables es un firewall de estado es decir, no solamente analiza y/o filtra paquetes de manera aislada. Sino que además puede examinar su historial: saber si un paquete pertenece a una conexión nueva (NEW), establecida (ESTABLISHED), relacionada (RELATED) o no válida (INVALID).

Por ejemplo al realizar un echo-request con ping y recibir desde el otro host un echo-reply, iptables, o mejor dicho, netfilter considera que se estableció una conexión.

Si queremos contrarrestar un icmp-flood de manera efectiva lo correcto es limitar las paquetes nuevos.

Reglas sencillas para probarlo

Las siguientes reglas permiten aplicar un límite de un paquete nuevo icmp por minuto (es un ejemplo extremo, pero útil para explicar el funcionamiento del módulo)

iptables -A INPUT -p icmp -m state –state NEW -m recent –update –seconds 60 –hitcount 1 –name icmpflood -j DROP
iptables -A INPUT -p icmp -m state –state NEW -m recent –set –name icmpflood

Como siempre es importante el orden de las reglas, podríamos invertirlas pero en la documentación de primera mano^1 está en ese orden (y las cosas cambiarían, podés probarlo vos el resultado).

Pero veamos las cosas tal como están. Supongamos que no han llegado paquetes nuevos icmp: Al arribar uno, no coincidirá con la primer regla. Explicaremos la razón:

El parámetro –seconds– indica el intervalo de tiempo que examinaremos, mientras que –hitcount evalúa la cantidad de paquetes de este tipo desde la misma dirección. Es importante entender que esta regla no incrementa el contador. Solamente examina cantidad de paquetes por unidad de tiempo. Como hasta ahora el contador está en 0, el paquete podrá ingresar (asumimos que la política predeterminada es ACCEPT, lo cual puede – en principio – ser inseguro, pero nuestro propósito es explicar el funcionamiento de recent, de manera que el lector está advertido). Insistimos: esta regla NO penaliza, es decir no incrementa el contador. ¿Quedó claro?

Vayamos a la segunda regla: esta regla crea un conjunto llamado icmpflood e incrementa el contador para los paquetes que cumplan con los requisitos estipulados, es decir: con protocolo icmp y estado NEW. De manera que al llegar el primer paquete se agrega la dirección IP al set icmpflood y el número de hits es ahora 1.

Recién al llegar el segundo paquete en menos de 60 segundos se habrán cumplidos los requisitos de la primer regla y por lo tanto el paquete será descartado.

Método sencillo para testear las reglas

Lo podemos probar sencillamente con los siguientes comandos desde otro host:

pingwait(){ ping -q -c1 $1 & sleep 31; }
IP=192.168.80.148
pingwait $IP; pingwait $IP; ping -q -c1 192.168.80.148

Es decir lo que hacemos es enviar dos paquetes nuevos icmp cada 31 segundos.

El resultado en el host de destino es:

Every 2,0s: iptables -L INPUT -vn  

Chain INPUT (policy ACCEPT 63 packets, 3612 bytes)
 pkts bytes target     prot opt in     out     source               destination
    2   168 DROP       icmp —  *      *       0.0.0.0/0            0.0.0.0/0            state NEW recent: UPDATE seconds:
 60 hit_count: 1 name: icmpflood side: source mask: 255.255.255.255
    1    84            icmp —  *      *       0.0.0.0/0            0.0.0.0/0            state NEW recent: SET name: icmpflood side: source mask: 255.255.255.255

Explicación amigable de rcheck y update

No explicamos en absoluto el significado de UPDATE (–update) hasta ahora y la diferencia con CHECK (–rcheck). No es sorpresa que la mejor explicación esté en la documentación original. Muchos de los sitios que intentan explicarlo en mi modesta opinión no lo hacen de la manera más clara. Así que trataré de hacerme entender, ya que son dos parámetros claves. Pero antes de hacerlo y sin querer eludir la explicación, mostraré el resultado al cambiar UPDATE por CHECK:

iptables -F && iptables -X && iptables -Z
iptables -A INPUT -p icmp -m state –state NEW -m recent –rcheck –seconds 60 –hitcount 1 –name icmpflood -j DROP
iptables -A INPUT -p icmp -m state –state NEW -m recent –set –name icmpflood
Every 2,0s: iptables -L INPUT -vn                                                                       Thu Jan  7 16:49:41 2016

Chain INPUT (policy ACCEPT 57 packets, 3250 bytes)
 pkts bytes target     prot opt in     out     source               destination
    1    84 DROP       icmp —  *      *       0.0.0.0/0            0.0.0.0/0            state NEW recent: CHECK seconds:
60 hit_count: 1 name: icmpflood side: source mask: 255.255.255.255
    2   168            icmp —  *      *       0.0.0.0/0            0.0.0.0/0            state NEW recent: SET name: icmpf
lood side: source mask: 255.255.255.255

Supongamos que tenés una bolsa con 10 caramelos y un nene te pide que se los des. Aceptás con una condición, solamente podrá comer 2 por hora. Entonces, podrías adoptar dos políticas con el pequeño:

Laxa (–rcheck)

  • Hora 15:00 Viene el nene por primera vez y te pide 2 caramelos. Le das un caramelo.
  • Hora 15:15: ¡Regresa el niño en busca de más caramelos! Está ok, le regalás el dulce, pero le decís que es el último durante esa hora.
  • Hora 15:30: Otra vez el simpático nene te pido caramelos, le explicás que ya comió los dos caramelos que corresponden a esa hora, recién a las 16:00 se lo darás.
  • Hora 16:00: Ahora sí tal lo prometido le podrás dar 1 o 2 caramelos durante las 16:00 hasta las 16:59.

Rígida (–update)

  • Hora 15:00 Viene el nene por primera vez y te pide 2 caramelos. Le das un caramelo.
  • Hora 15:15: ¡Regresa el niño en busca de más caramelos! Está ok, le regalás el dulce, pero le decís que es el último durante esa hora.
  • Hora 15:30: Otra vez el simpático nene te pido caramelos, le explicás que ya comió los dos caramelos que corresponden a esa hora… Pero que durante una hora a partir de ahora no le podrás dar más caramelos.
  • Hora 15:45: El chico insiste quiere más caramelos. Como disciplina le decís que por no esperás volvés a extender el tiempo de espera. De manera que tendrá que esperar hasta las 16.45 para recibir otro caramelo.
  • Hora 16:00 El pobre nene insiste, entonces extendés la penalidad una hora más. Y el nino recién ahí comprende la indicación y espera una hora.
  • Hora 17:00: Ahora sí tal lo prometido le podrás dar 1 o 2 caramelos durante los siguientes 60 minutos.

Así que esas son las diferencias entre –rcheck, –update, cuanto menos respete el atacante el tiempo estipulado en –seconds peor será le penalidad. En el ejemplo que estuvimos viendo –update significa que el otro host deberá esperar sí o sí 1 minuto entre ping y ping. En cambio –rcheck es más permisivo, por cada nuevo minuto nuevo que comience podrá enviar la cantidad estipulada en –hitcounts que en nuestro ejemplo fue 1 paquete.

Archivos de listas en /proc/net/xt_recent

root@raspberrypi:~#  cat  /proc/net/xt_recent/icmpflood src=192.168.80.250 ttl: 64 last_seen: 29694939 oldest_pkt: 3 29688739, 29691839, 29694939

Hay un archivo por lista (set). Vemos en el archivo /proc/net/xt_recent/icmpflood la dirección de origen del paquete y además la última vez que fue vista usando la unidad de tiempo jiffies.

Parámetros del kernel

Además se le pueden pasar parámetros al módulo

ip_list_tot: Número de direcciones IPs que puede recordar por lista
ip_pkt_list_tot: Número de paquetes que puede recordar por IP (el máximo es 255)

Conclusión

Como vemos si bien existen otros métodos para mitigar un DDOS o DOS, este tiene la ventaja de usar directamente netfilter, sin la necesidad de instalar una herramienta adicional.

 

Instalar Linux desde un archivo ISO

En el artículo anterior habíamos visto el potencial de la shell de GRUB 2. Allí habíamos visto que podíamos arrancar directamente desde una imagen ISO sin necesidad de tener que grabarlo en un disco óptico ni copiarlo a una unidad USB.

Ahora bien, ¿podríamos además instalar un sistema operativo utilizando ese método? La respuesta es sí. En este caso puntual veremos como instalar Fedora 23.

Aquí, tendremos que tener en cuenta los dos software involucrados principalmente para poder realizar esta tarea. Estos programas son GRUB 2 (naturalmente) y dracut.

Recordemos que en el momento del arranque se utiliza un sistema de archivos temporal en memoria hasta que se pueda realizar el montaje del definitivo sistema de archivos raiz.

dracut es tanto una infraestructura de initramfs manejada por eventos como así también una herramienta que se usa para crear una imagen de disco en memoria.  El comando dracut copiar herramientas y archivos de un sistema instalado y lo combina con el framework dracut.

Estos archivos se pueden ver en fedora en el directorio /usr/lib/dracut/modules.d:

 


[sergio@hope ~]$ ls /usr/lib/dracut/modules.d
00bash               30convertfs  90dm                      90qemu-net    95fstab-sys     95zfcp            99base
00systemd            40network    90dmraid                  91crypt-gpg   95iscsi         95zfcp_rules      99fs-lib
00systemd-bootchart  45ifcfg      90dmsquash-live           91crypt-loop  95nbd           95znet            99img-lib
01systemd-initrd     45url-lib    90kernel-modules          95cifs        95nfs           97biosdevname     99kdumpbase
02systemd-networkd   50drm        90kernel-network-modules  95dasd        95resume        98dracut-systemd  99shutdown
03modsign            50plymouth   90livenet                 95dasd_mod    95rootfs-block  98ecryptfs        99uefi-lib
03rescue             80cms        90lvm                     95dasd_rules  95ssh-client    98pollcdrom
04watchdog           90bcache     90mdraid                  95debug       95terminfo      98selinux
05busybox            90btrfs      90multipath               95fcoe        95udev-rules    98syslog
10i18n               90crypt      90qemu                    95fcoe-uefi   95virtfs        98usrmount

Por lo tanto aquí la clave es pasar los parámetros del kernel y opciones para dracut correctas. Y para agregar la entrada al menú de grub utilizamos el archivo /etc/grub.d/40_custom.

 exec tail -n +3 $0
 # This file provides an easy way to add custom menu entries. Simply type the
 # menu entries you want to add after this comment. Be careful not to change
 # the ‘exec tail’ line above.

menuentry “Live Fedora 23″ –class fedora {
 set isofile=”/Fedora-Live-KDE-x86_64-23-10.iso”
 loopback loop (hd0,gpt8)$isofile
 linuxefi (loop)/isolinux/vmlinuz0 iso-scan/filename=${isofile} root=live:CDLABEL=Fedora-Live-KDE-x86_64-23-10 rootfstype=auto ro rd.live.image quiet rhgb rd.luks=0 rd.md=0 rd.dm=0
 initrdefi (loop)/isolinux/initrd0.img
 }

Aquí los comandos y opciones claves son respectivamente loopback e iso-scan.

El comando loopback sirve para especificar la ruta a un archivo ISO para que sea tomado como un dispositivo de bloques. Mientras tanto el parámetro iso-scan sirve para montar el dispositivo de loopback y pasarle los parámetros de kernel y dracut apropiadas.

¿Cómo sabemos las opciones y parámetros correctos? Las podemos averiguar montando el archivo ISO e inspeccionando el archivo isolinux.cfg:


[root@hope sergio]# mount -o loop /usr/Fedora-Live-KDE-x86_64-23-10.iso /media
mount: /dev/loop0 está protegido contra escritura; se monta como sólo lectura
[root@hope sergio]# grep -m1 append  /media/isolinux/isolinux.cfg 
  append initrd=initrd0.img root=live:CDLABEL=Fedora-Live-KDE-x86_64-23-10 rootfstype=auto ro rd.live.image quiet  rhgb rd.luks=0 rd.md=0 rd.dm=0

Sencillamente debemos copiar lo que está a continuación de append initrd=initrd0.img.

Además, se pueden apreciar los comandos linuxefi e initrdefi que son equivalente de manera respectiva a las ordenes linux e initrd al utilizar un sistema con UEFI.

Finalmente, debemos ejecutar el comando para generar el nuevo archivo de GRUB 2:

[root@hope sergio]# grub2-mkconfig -o /boot/efi/EFI/fedora/grub.cfg

Reiniciamos, elegimos la opción Live Fedora 23 y luego podremos instalar el sistema operativo.

Instalar desde un archivo ISO

La partición importa

La partición en la que está ubicado el archivo ISO importa. Existe un bug en el módulo de python para la configuración de almacenamiento del sistema llamado blivet que impide la instalación del sistema operativo si la ISO está en alguna partición marcada en el editor de particiones, más allá de que esta se formatee o no. Como solución provisoria, se puede emplear una partición sin que el instalador la tenga en cuenta. De hecho, hice eso mismo. Luego, posterior a la instalación usé dicha partición para el directorio /usr, usando rysnc, editando el archivo /etc/fstab y borrando el /usr original.

Conclusión

Una vez más, podemos apreciar el potencial de GRUB 2 y como podemos prescindir de otros medios para instalar sistemas operativos. De hecho, dado el gran tamaño de los discos en la actualidad, podríamos tener una partición específicamente para tener ISOS.

 

La poderosa shell de GRUB 2

En el artículo anterior hemos visto que la línea de comandos de GRUB nos permitía reparar el cargador de arranque sin necesidad de recurrir a un cargador de arranque.

GRUB2 como es de suponer también permite hacerlo:

grub> set root=(hd0,1)
grub> linux /vmlinuz-3.16.0 root=/dev/mapper/centos-root ro
grub> initrd /initramfs-3.16.0.img
grub> boot

Como vemos cambian algunos comandos, sintaxis y la nomenclatura de particiones pero la idea es la misma.

Desde ya contamos con la tecla TAB​ para autocompletar rutas de archivo y el comando cat:

Comando cat en GRUB2
Arrancar directo desde un archivo ISO

Bajamos una distro y no queremos ni tenemos tiempo para grabarla en un CD/DVD. Perfecto, GRUB2 en la mayoría de los casos puede arrancar también una imagen ISO:

 

 

grub> set isofile=/TinyCore.iso 
grub> loopback loop (hd0,1)/$isofile vmlinuz 
grub> linux (loop)/boot/vmlinuz cde loglevel=3 
grub> initrd (loop)/boot/core.gz

Tener acceso a un archivo de un LVM

Sí, estimados, GRUB2 puede también leer un volumen lógico, ¿Cómo? Así:

LVM y GRUB2

Algo más

¿Más? Sí, mas todavía, por ejemplo listar dispositivos en el bus PCI:

Comando lspci en GRUB2

Como se puede apreciar GRUB es bastante más que un boot loader y su shell lo suficientemente potente como para prescindir de un LiveCD (al menos en soporte físico) en más de una ocasión.

Información complementaria