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.

Prerequisitos (clik para extender)

Diseño del Ataque

El “Kill Chain” habitual de un ciberataque se suele definir de la siguiente manera:

Cybersecurity Killchain
Cybersecurity Killchain

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:

  1. Esnifar las comunicaciónes legítimas entre la RTU (FactoryIO) y el PLC (OpenPLC)
  2. 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)
  3. Generar un paquete falso que cumpla con la secuencia TCP y la estructura de valores esperada por FactoryIO, pero con unos comandos Modbus malintencionados
  4. Inyectar el paquete
Diseño del ataque de inyección de paquetes
Diseño del ataque de inyección de paquetes

 


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.

Import de Scapy
Import de Scapy

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.

Diferencia entre funciones Send y Sendp
Diferencia entre funciones Send y Sendp

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).

Rojo: tiempo entre mensajes durante una única iteración del bucle de comunicaciones de OpenPLC / Verde: tiempo entre bucles de comunicación de OpenPLC
Rojo: tiempo entre mensajes durante una única iteración del bucle de comunicaciones de OpenPLC / Verde: tiempo entre bucles de comunicación de OpenPLC

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
Verde: parte fija que identifica un paquete ModbusTCP con la función "Write Coils" // Rojo: parte variable (estado deseado de las salidas)
Superior: paquete ModbusTCP capturado con Wireshark // Inferior Izquierda: paquete ModbusTCP esnifado con Scapy // Inferior Derecha: paquete no ModbusTCP esnifado con Scapy
Verde: parte fija que identifica un paquete ModbusTCP con la función “Write Coils” // Rojo: parte variable (estado deseado de las salidas)

Así que se utiliza la siguiente lógica para identificar este patrón:

  1. 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
  2. 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”
  3. 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
  4. Si no se cumplen esas condiciones, se capturan otros 4 mensajes
Esnifado de paquetes con Scapy
Esnifado de paquetes con Scapy

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:

Configuración de capas TCP e IP con Scapy
Configuración de capas TCP e IP con Scapy

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:

Implementación de capas Modbus TCP y Modbus con Scapy
Fondo Oscuro: Implementación de capas ModbusTCP y Modbus con Scapy //  Fondo Claro: ejemplo de paquete Modbus extraído de Wireshark

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).

Identificacion de parámetros Shortfield y ByteField mediante Wireshark
Identificacion de parámetros ShortField (entero de 2 bytes) y ByteField (entero de 1 byte) mediante Wireshark

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().

Adición de capas ModbusTCP y Modbus, e inyección del paquete
Adición de capas ModbusTCP y Modbus, e inyección del paquete

 

 


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:

Comprobacion de respuesta al paquete inyectado
Comprobacion de respuesta al paquete inyectado

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.

Izquierda: paquete inyectado con los valores de secuencia TCP predichos // Derecha: paquete legítimo con los mismos valores de secuencia TCP es marcado como erróneo al llegar más tarde
Izquierda: paquete inyectado con los valores de secuencia TCP predichos // Derecha: paquete legítimo con los mismos valores de secuencia TCP es marcado como erróneo al llegar más tarde