Inyección de Paquetes Modbus TCP con Scapy
En este post se describe cómo efectuar un ataque de predicción de secuencia TCP para inyectar paquetes Modbus TCP en el Laboratorio Virtual de Ciberseguridad Industrial.
Diseño del Ataque
El “Kill Chain” habitual de un ciberataque se suele definir de la siguiente manera:
En esta simulación se parte de un escenario en el cual el atacante ya cuenta con acceso a la red, y por ello se ejecutan directamente las acciones sobre objetivos sin necesidad de pasar por todas las fases previas.
El ataque está orientado a parar o perturbar la línea para interrumpir la produccion, y la manera más directa de conseguirlo es intentar interactuar de alguna manera con la unidad que controla los sensores y actuadores (RTU – FactoryIO), así que con esto en mente se define el plan de ataque:
- Esnifar las comunicaciónes legítimas entre la RTU (FactoryIO) y el PLC (OpenPLC)
- Utilizar uno de los mensajes intercambiados como semilla para predecir los valores de la secuencia TCP (preferiblemente el último mensaje del bucle de comunicaciones de manera que se tengan 100ms disponibles para generar e inyectar el paquete antes de que OpenPLC vuelva a comunicarse con FactoryIO e invalide la secuencia calculada)
- Generar un paquete falso que cumpla con la secuencia TCP y la estructura de valores esperada por FactoryIO, pero con unos comandos Modbus malintencionados
- Inyectar el paquete
Preparación de Herramientas
Scapy es una libreria de python que permite utilizar múltiples funciones de manipulación de paquetes. Está disponible por defecto en las distribuciones de Kali Linux, por lo que no será necesario realizar ninguna instalación adicional. En caso de estar utilizando otra distribución de Linux para simular la maquina atacante será neceario referirse a la documentación de instalación.
Adicionalmente, es recomendable (aunque opcional) instalar un IDE para facilitar la programacion del script de python. Visual Studio Code (VS Code) es gratis y bastante completo y se puede conseguir aquí, junto con unas instrucciones para su instalación en Kali Linux.
Inyección de Paquete Modbus TCP con Scapy
El primer paso será crear un script de python e importar la librería de Scapy para poder acceder a todas las clases y funciones que aporta. A partir de este momento, el objetivo será construír una a una todas las capas de la trama a inyectar.
Antes de comenzar, es necesario saber que Scapy ofrece dos funciones diferentes para inyectar paquetes: send() y sendp(). Con sendp() se estarán enviando paquetes de capa 2, mientras que en el caso de send() se estarán enviando paquetes de capa 3 y Scapy se encargará del generar los valores adecuados de la capa 2.
En este ataque se utilizará send() y por lo tanto no será necesario configurar la capa Ethernet.
Configuración de Capas IP y TCP
Durante el bucle de comunicación estándar, los mensajes se intercambian cada aproximadamente 0,5ms y esto es un problema ya que se necesita un tiempo para procesar e inyectar el nuevo paquete.
Para maxmizar las probabilidades de éxito del ataque se va explotar el conocimiento adquirido previamente respecto al tiempo entre ciclos de comunicación de OpenPLC con FactoryIO (100ms).
Del análisis previo también se sabe que:
- El penúltimo mensaje de todos los bucles de comunicaciones es una “Query:Write Coils” seguido de un último ACK dirigidos desde OpenPLC a la FactoryIO
- Todos los mensajes de “Query:WriteCoils” tienen una parte fija en la capa de modbus y una parte variable correspondiente a las salidas a modificar en cada momento
Así que se utiliza la siguiente lógica para identificar este patrón:
- Mediante la función sniff() se capturan 4 mensajes consecutivos (recomendable capturar el doble de mensajes de los necesarios -en este caso dos mensajes: Query, y ACK- para prevenir el aliasing) con los siguientes filtros:
- Tiene capa TCP
- Destinado a la IP de FactoryIO
- Se comprueba si el penúltimo de los mensajes capturados:
- Tiene una capa de raw. Los paquetes de modbus tienen esta capa, pero los ACK no (como se puede ver en la imagen superior). Se hace esto para prevenir errores en la siguiente comprobación al intentar abrir una capa que no existe.
- Que esa capa de capa contiene la cadena “x0f\x00\x00\x00\x05\x01”
- Si se cumplen ambas condiciones, se considera que el último mensaje capturado será el de final de bucle y por lo tanto se utilizará como semilla
- Si no se cumplen esas condiciones, se capturan otros 4 mensajes
Como la semilla tiene una longitud (len) 0, no será necesario incrementar los valores de ACK y SEQ, así que con esto ya se tiene toda la información necesaria para crear las capas IP y TCP del nuevo paquete:
Implementación y Configuración de Capas Modbus y Modbus TCP
Scapy soporta múltiples protocolos de manera nativa, pero no es el caso de Modbus TCP o Modbus. Sin embargo, esto no es un problema ya que la librería cuenta con las funciones necesarias para implementar protocolos a medida desde 0.
En la captura de Wireshark se ha comprobado que existen dos capas diferentes, la capa Modbus TCP, y la capa Modbus. Teniendo esto en cuenta se crean dos clases nuevas en el script con los mismos nombres que se pueden observar en la captura (esto último no es estrictamente necesario pero ayudará a que la estructura del paquete inyectado sea lo más parecido posible a uno legítimo:
Una vez definidas las clases, se crean todos los campos adecuados utilizando como pantilla una captura genérica de la función “Write Coils”:
- Capa ModbusTCP
- Transaction Identifier
- Protocol Identifier
- Length
- Unit Identifier
- Capa Modbus
- Function Code
- Reference Number
- Bit Cout
- Byte Count
- Data: estado deseado de las salidas, en este caso se utilizará el valor de “0”, que implicará apagar todas las salidas
Scapy ofrece diferentes tipos de datos a la hora de definir cada uno de los campos del protocolo a implementar. Para identificar qué tipo asignar a cada campo se hace click en cada uno de ellos dentro de un paquete similar capturado en Wireshark y se comprueba qué set de números se resalta dentro de la visualización Hexadecimal. Todos los campos que resalten un set de 2 números serán del tipo ByteField (entero de 1 byte), mientras que los que resalten 2 sets de 2 números serán del tipo ShortField (2-byte integer).
Inyección del Paquete
Con todos los parámetros calculados, solo queda encadenar todas las capas dentro de un paquete mediante el operador “/”, y programar la inyección mediante la funcion send().
Resultados
En este punto está ya todo preparado para ejecutar el script y comprobar los resultados del ataque:
Adicionalmente, es conveniente monitorizar el ataque con Wireshark en paralelo para comprobar que el comportamiento anómalo no ha sido casualidad:
En esta captura se puede verificar que efectivamente se ha inyectado un paquete con el Identificador de Transaccion Modbus número 1337 (cuando el adecuado sería el 395), y que existe una respuesta por parte de FactoryIO confirmado que la orden se ha ejecutado correctamente. De esto se deduce que FactoryIO no valida el orden de secuencia de los comandos de Modbus, sólo de los paquetes TCP y por lo tanto el número de transacción es indiferente.
También se puede comprobar cómo al haber utilizado unos valores de secuencia TCP legítimos, cuando OpenPLC intenta utilizar esos mismos valores en su siguiente petición se genera un error que acaba reseteando la sesión entre OpenPLC y FactoryIO.