10 octubre 2011

Manipulando tráfico



Hasta ahora hemos visto maneras de desviar tráfico de su legítimo trayecto y espiar su contenido. También hemos practicado maneras de generar tráfico con la estructura exacta de bajo nivel que deseemos. Conociendo ambas cosas se podría pensar que la manipulación de tráfico es una mera combinación de las dos técnicas: se desvía el tráfico con un ataque de ARP Spoofing y se usa Scapy para leer los paquetes desviados y manipular su contenido.

A priori, esta combinación de recursos debería funcionar pero a la hora de ponerla en práctica tropezaremos con la cruda realidad de cómo se comporta Scapy con respecto a la pila TCP/IP. Y es que cuando Scapy (o tcpdump) escucha un paquete recibido no lo desvía de su trayecto a través de la pila TCP/IP sino que saca una copia del paquete y lo muestra al usuario. Eso quiere decir que aunque manipulemos la copia del paquete, el original se procesará en nuestra pila TCP/IP haciendo que esta lo envíe. Así, lo que ocurrirá es que al enviar el paquete modificado a la víctima en realidad mandaremos dos paquetes: el manipulado y el original. Demasiado ruido para que el ataque funcione correctamente.


Existe un técnica mucho más elegante para realizar este tipo de ataque. Básicamente se trataría de "sacar" los paquetes desviados recibidos de la pila TCP/IP para evitar que se procesen al margen de nuestra manipulación. Mucha gente sabe que el cortafuegos más popular en el mundo Linux es iptables, lo que es menos conocido es que iptables pertenece al ecosistema de netfilter. En realidad, netfilter es el nombre oficial del proyecto que abarca todas las capacidades de filtrado y manipulación de tráfico de red que ofrece el kernel de Linux. Netfilter cuenta además con una API para aplicar cualquier tipo de función en las diversas fases de la pila TCP/IP. Iptables es la aplicación de línea de comandos que aprovecha las funcionalidades ofrecidas por netfilter para comunicar al kernel de Linux las reglas de filtrado de tráfico introducidas por el usuario. Simplificando, se puede decir que iptables es el subconjunto de netfilter que permite a los usuarios introducir reglas de filtrados de tráfico en el kernel.

Lo que vamos a hacer es usar iptables para mover los paquetes desviados a una cola de netfilter. Una vez en dicha cola, usaremos la API de netfilter para aplicar las funciones de modificación que deseemos al paquete y al finalizar sacaremos el paquete de la cola y lo devolveremos la pila de TCP/IP para ser enviado.

Como en otras ocasiones, utilizaremos Python y en concreto la librería python-nfqueue que no es sino un interfaz a la API de colas de netfilter. El programa que vamos a preparar recibe tráfico desviado mediante un ataque de ARP-Spoofing y busca en dicho tráfico una palabra concreta, si la encuentra la sustituye por otra palabra que decidamos nosotros (con la única limitación de que dicha palabra debe ser de igual longitud que la palabra original) y luego remite el tráfico modificado a su legítimo destinatario. El programa principal no puede ser más sencillo:

###########
# PROGRAMA 
###########

parseArguments()

#Le mandamos a IPtables una regla para que redirija el trafico que queremos manipular a una cola de Netfilter. 
os.system("iptables -A FORWARD -d " + target_address.strNormal() + " -p TCP -j NFQUEUE --queue-num 0")

#Creamos una cola de netfilter donde aparcaremos aquellos paquetes a manipular. 
q = nfqueue.queue()
q.open()
q.bind(socket.AF_INET)

#Asociamos una funcion a la cola. Esta funcion se aplicará a todos los paquetes que entren en la cola por lo 
#que sera aquella en la que pondremos las operaciones de modificacion de trafico que queremos realizar. 
q.set_callback(replace)

#Activamos la cola y le damos el identificador 0. 
q.create_queue(0)
try:
print "Replacing content. Press Control-C to end." 
  q.try_run()
except KeyboardInterrupt:
print "Program ended by user..." 

#Finalizado el programa deshacemos las configuraciones realizadas en el entorno. 
#os.system("iptables -D FORWARD -d " + target_address.strNormal() + " -p TCP -j NFQUEUE") 
os.system("iptables -F")
q.unbind(socket.AF_INET)
q.close()

Como se puede comprobar, creamos una cola y le asignamos el identificador 0. A esa cola redirigimos el tráfico que vaya dirigido a la víctima mediante una regla de iptables (que al acabar el programa retiramos mediante un "iptables -F"). Con la orden set_callback asociamos a la cola una función de nuestra cosecha (en este caso replace) que se aplicará a todos los paquetes de red que entren en la cola 0. Una vez configurada de esta manera la cola, la activamos mediante la orden try_run. Es importante resaltar que nfqueue usa identificadores para sus colas porque puede trabajar con varias. Nada nos habría impedido crear varias colas, asignándoles a cada una de ellas funciones diferentes, si nos interesase crear colas diferentes que pudiesen procesar distintos tipos de tráfico redirigidos por sendas reglas de iptables o, incluso, colas que fuesen procesando el mismo tráfico sucesivamente como en una cadena de montaje.

La función replace es donde incluimos lo que queremos hacer con cada paquete:


#Esta es la función que vamos a asociar a la cola de netfilter para que se 
#aplique a todos los paquetes que entren en ella.
def replace(dummy, payload):
  print "############################################################"
  print "Recibido paquete desviado: "
  #Usamos Scapy para convertir el paquete en crudo en una estructura 
  #manejable de manera comoda.
  packet = IP(payload.get_data())
  print "Resumen: " + packet.summary()
  #Que sea un paquete TCP no significa que lleve carga. Los paquetes del 
  #treeway handshake no la llevan, asi que para evitar excepciones 
  #modificando un atributo inexistente comprobamos que el paquete 
  #realmente lo tiene.
  if Raw in packet:
    print "El paquete tiene payload."
    content = packet[Raw].load
    #Sustituimos una cadena por otra...
    if string_to_replace in content:
      print "Cadena a reemplazar encontrada en: "
      print "\t " + content
      content = content.replace(string_to_replace, replacing_string)
      print "Contenido sustituido por: "
      print "\t" + content
      packet[Raw].load = content
      #Reconstruimos el paquete. Hay que tener especial cuidado con el 
      #checksum TCP ya que si hemos cambiado el contenido del paquete 
      #habrá que recalcularlo.
      del packet[TCP].chksum
      #Al recrear el paquete pero con el TCP chacksum borrado este se 
      #recalcula en el constructor IP() de Scapy.
      new_packet = IP(str(packet))
      packet = new_packet
  #Y volvemos a reintroducir el paquete en la pila TCP/IP.
  payload.set_verdict_modified(nfqueue.NF_ACCEPT, str(packet), len(packet))
  print "############################################################"


Como se puede ver, nfqueue le pasa a las funciones asignadas como callback dos parámetros: dummy y payload. El parámetro dummy reconozco que es un misterio para mi, en la escueta documentación de python-nfqueue no parece que se le mencione y tampoco parece que se le de uso alguno en los ejemplos, así que lo dejaremos atrás hasta que descubramos para qué sirve. El que sí resulta ser un parámetro crítico es payload que es el que contiene el paquete recibido. Es importante señalar que nfqueue remite a la función de usuario el paquete a partir del nivel IP, por lo que le habrá quitado todas las cabeceras de Ethernet. Así que si queremos jugar con los paquetes a nivel IP o superior (TCP o UDP) nfqueue puede ser una buena opción pero sin embargo si lo que queremos es tocar a nivel Ethernet es posible que tengamos que buscar una alternativa o complemento. Para convertir el payload en un objeto de Scapy que podamos usar como hasta ahora nos basta con llamar al constructor IP() y pasarle como parámetro payload.get_data(). A partir de ahí el comportamiento de la función es evidente: se mira si en la carga del paquete aparece la palabra que queremos sustituir y, en ese caso, la sustituimos por la otra y recalculamos el checksum del paquete TCP. Por último, se usa la función set_verdict_modified para decirle al netfilter que devolvemos a la pila TCP/IP un paquete pero que este es distinto que el que entró en la cola. El parámetro de aceptación es nfqueue. NF_ACCEPT, y el nuevo paquete y su longitud son los otros dos parámetros que se pasan a set_verdict_modified.

Si hubiésemos pasado un parámetro de aceptación con valor nfqueue.NF_QUEUE podríamos remitir el paquete a una cola diferente aunque el identificador de la cola lo tendríamos que "fundir" con el valor nfqueue.NF_QUEUE haciendo un OR binario de los 16 bits superiores de esta constante y el valor binario del identificador de la nueva cola. Suena alambicado pero python-nfqueue no es sino un mero binding a las librerías en C de netfilter y hay algunas cosas que no cubre y no queda más remedio que hacerlo a lo C-way-of-living-style. Para el que no se lo crea que eche un vistazo al doc de netfilter cuando habla de NF_QUEUE.

El recálculo del checksum es crítico ya que la función de esta cabecera del paquete TCP es permitir comprobar que el paquete recibido no ha cambiado (se ha corrompido) por un fallo de transmisión durante el trayecto. En realidad esta cabecera es una reminiscencia de cuando las redes de datos eran mucho menos fiables que las actuales y había que implementar estos mecanismos de integridad en los extremos para solicitar la retransmisión de los paquetes que se corrompiesen por el camino. Reminiscencia o no, lo cierto es que este mecanismo nos afecta directamente si lo que estamos haciendo es cambiar el contenido de los paquetes a mitad del trayecto, ya que si dejamos el checksum original el extremo receptor detectará que el paquete se ha corrompido y lo descartará. Afortunadamente, Scapy se encargará de recalcular el checksum del paquete si se lo pedimos. Bastará con borrar el parámetro checksum del paquete que hemos modificado y usar este para crear uno nuevo. El constructor IP() de Scapy detectará que falta la cabecera checksum en el parámetro que le pasamos y la calculará él mismo.

El resto del programa no tiene mayor misterio, por lo que no merece la pena explicarlo. Puede ser descargado de mi repositorio de aplicaciones para examinar su código detenidamente.

Para probarlo vamos a recurrir a una versión ligeramente modificada de la maqueta de netkit que usamos para el artículo de ARP-Spoofing (también descargable desde el repositorio, cuenta además con un README para su instalación). Como en aquella ocasión, el mapa de red de la maqueta es:



Como se puede ver, la maqueta cuenta con una red de gestión (192.168.20.0/24) pensada para acceder por SSH a los distintos elementos del simulacro sin que el tráfico de gestión se mezcle con el tráfico del experimento. Como las líneas de comandos que arranca netkit a cada una de las máquinas virtuales son bastante limitadas, le recomiendo que desde la consola del PC anfitrión lance una sesión de SSH contra Alice y al menos dos sesiones de SSH contra Sniffer (en concreto contra los interfaces de esos dos equipos en la red de gestión).

Para comenzar la práctica, desde una de las sesiones de SSH abiertas contra Sniffer active un ataque de ARP-Spoofing contra Alice:

PC-Sniffer:~# ./scripts/arpoison.py 192.168.0.1 192.168.0.2

Hasta ahora todo es exactamente igual que en nuestra práctica sobre ARP-Spoofing. A partir de la activación de arpoison todo el tráfico que Alice intercambia con Internet pasa a través de PC-Sniffer, por lo que Sniffer puede o escuchar el tráfico... o modificarlo como vamos a hacer nosotros. Para ello, desde la otra sesión de SSH abierta contra Sniffer vamos a activar pyreplace y le vamos a decir que siempre que en el tráfico dirigido a Alice aparezca la palabra "Mozart" sustituya dicha palabra por "Robert".

PC-Sniffer:~# ./scripts/pyreplace.py 192.168.0.2 "Mozart" "Robert"

Como ya hemos dicho, resulta crítico que ambas palabras sean de la misma longitud. Si quisiéramos sustituir una palabra por otra de longitud diferente nuestro programa se complicaría notablemente ya que no sólo habría que modificar las cabeceras de longitud de los paquetes sino que si sustituimos por palabras de mayor longitud existe la posibilidad de que se supere la longitud máxima de paquete lo que obligaría a distribuir el paquete modificado en dos y a partir de ahí habría que añadir un offset a los números de secuencia (¡y restárselo a los ACK de respuesta!). En fin, sería bastante más trabajoso y no añadiría ningún concepto nuevo pero no imposible para un atacante decidido y con el suficiente interés.

Observe también que siempre utilizamos con los scripts las direcciones de producción de los distintos elementos de red y no su interfaz de gestión.

Ahora vamos a ver si el ataque funciona. Para ello navegaremos desde el PC de Alice hacia Google y buscaremos en él el término "Mozart". Como la sesión que tenemos abierta por SSH con Alice es textual usaremos el navegador textual lynx, mediante la orden:


PC-Alice:~# lynx www.google.com


Como puede ver en las imágenes de abajo (pinche en ellas para ampliarlas), el resultado de Google parece que habla constantemente de un tal "Wolfgan Amadeus... Robert", señal de que pyreplace ha funcionado correctamente.




Como se puede ver, la modificación de tráfico durante el trayecto no resulta complicada. Podría pensar que la limitación de sustituir palabras por otras de la misma longitud resta importancia a este ataque pero por un lado, como hemos dicho más arriba, extender nuestro programa para que pueda usar palabras de sustitución de diferente longitud no resulta conceptualmente complicado y por el otro existen multitud de "campos" de longitud fija cuya manipulación puede resultar en una grave brecha de seguridad: cuentas bancarias, números de DNI, matrículas, etc...

Las salvaguardas contra este tipo de ataques pasan por utilizar mecanismos de cifrado y de integridad. El cifrado evitaría que el atacante localizase la palabra que quiere sustituir y los mecanismos de integridad asegurarían que aunque el atacante lograse modificar el tráfico nuestro sistemas detectaría que el tráfico recibido no es el originalmente enviado y lo rechazaría. Un protocolo como SSL, correctamente utilizado, ofrece estos mecanismos de defensa aunque en determinadas situaciones, que espero poder explicar en artículos posteriores, tampoco son la panacea.

1 comentario:

Anónimo dijo...

Hola, excelente programa, tengo una inquietud, al ejecutar el programa ./pyreplace.py va todo bien la primera vez, pero a veces la segunda o tercera vez recibo este mensaje: Traceback (most recent call last):
File "./pyreplace.py", line 119, in
q.bind(socket.AF_INET)
File "/usr/lib/python2.7/dist-packages/nfqueue.py", line 94, in bind
def bind(self, *args): return _nfqueue.queue_bind(self, *args)
RuntimeError: error during nfq_bind_pf()

El cual me impide la correcta ejecución del script y la única solución es reiniciar el PC. Me gustaría saber si a alguien mas le sucede y que solución se puede dar, para no tener que reiniciar el computador.

Gracias