Capítulo 2 — Redes y protocolos
Mapa de la lección
Lección 02 publicada. Mapa, ejemplo, EX005.
flowchart TD
N0["Resultados de aprendizaje y conexión"]
N1["2.1 Protocolos industriales y confianza"]
N2["2.2 El modelo de datos de Modbus"]
N3["2.3 La cabecera MBAP"]
N4["2.5 Actividad práctica"]
N5["Síntesis del capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Ejemplo resuelto"]
EX --> Q["Ejercicio EX005"]
Dibujo lógico
flowchart LR
N0["Resultados de aprendizaje y conexión"]
N1["2.1 Protocolos industriales y confianza"]
N2["2.2 El modelo de datos de Modbus"]
N3["2.3 La cabecera MBAP"]
N4["2.5 Actividad práctica"]
N5["Síntesis del capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX005"]
[Resultados de aprendizaje y conexión]
|
v
[2.1 Protocolos industriales y confianza]
|
v
[2.2 El modelo de datos de Modbus]
|
v
[2.3 La cabecera MBAP]
|
v
[2.5 Actividad práctica]
|
v
[Síntesis del capítulo]
|
v
[Ejemplo resuelto] --> [Ejercicio EX005]
Resultados de aprendizaje y conexión
Al terminar este capítulo debería ser capaz de:
- explicar por qué la mayoría de los protocolos industriales no llevan autenticación y qué significa eso para cualquiera que pueda alcanzar el puerto de un controlador;
- decodificar una trama Modbus/TCP a mano y con código: cabecera MBAP, identificador de unidad, código de función, dirección y cantidad;
- distinguir las funciones de lectura de las de escritura y reconocer las funciones de diagnóstico y de identificación que merecen revisión;
- convertir un decodificador en una primera detección: escrituras desde cualquier equipo que no sea el origen de ingeniería autorizado.
El capítulo 1 dibujó las rutas. Este capítulo abre los paquetes de las rutas de control y de supervisión para que los capítulos posteriores puedan filtrarlos, verificarlos y detectarlos.
2.1 Protocolos industriales y confianza
Protocolos como Modbus y DNP3 se diseñaron para redes serie cerradas en las que se suponía que todo lo que había en el cable pertenecía a ellas. Sus versiones TCP/IP mantuvieron esa suposición: un dispositivo Modbus/TCP clásico acepta una escritura válida de cualquier equipo que pueda abrir una conexión TCP al puerto 502. Existen variantes seguras (la Modbus Organization publica una especificación Modbus/TCP Security que usa TLS (Modbus specifications), y DNP3 dispone de Secure Authentication (DNP Users Group overview)), pero a menudo los dispositivos instalados no las admiten. Mientras no lo hagan, poder alcanzar equivale a estar autorizado, y los controles de red de los capítulos 10 y 11 son la defensa principal.
2.2 El modelo de datos de Modbus
Un dispositivo Modbus expone cuatro tablas: bobinas (un bit, lectura/escritura), entradas discretas (un bit, solo lectura), registros de entrada (16 bits, solo lectura) y registros de retención (16 bits, lectura/escritura). Lo que significa un registro (el nivel en centímetros, el punto de consigna de una bomba, un indicador de modo) lo define la configuración del dispositivo y se documenta en un mapa de registros. El protocolo en sí transporta números, no significados, de modo que un decodificador puede decirle «escritura en el registro de retención 7», pero solo el mapa de registros le dice que el registro 7 es el punto de consigna de la bomba 2.
Los códigos de función públicos más comunes son 1 (leer bobinas), 2 (leer entradas discretas), 3 (leer registros de retención), 4 (leer registros de entrada), 5 (escribir una bobina), 6 (escribir un registro), 15 (escribir varias bobinas), 16 (escribir varios registros), 8 (diagnóstico) y 43/14 (leer la identificación del dispositivo). Los códigos de función se definen en la especificación Modbus Application Protocol que publica la Modbus Organization (Modbus specifications).
2.3 La cabecera MBAP
Sobre TCP, cada petición Modbus empieza con una cabecera MBAP de 7 bytes seguida de la unidad de datos del protocolo (PDU):
| Bytes | Campo | Notas |
|---|---|---|
| 0–1 | Identificador de transacción | Empareja una respuesta con su petición. |
| 2–3 | Identificador de protocolo | Siempre 0 en Modbus. |
| 4–5 | Longitud | Número de bytes que siguen a este campo (id. de unidad + PDU). |
| 6 | Identificador de unidad | Direcciona un dispositivo situado tras una pasarela; a menudo 1 o 255. |
| 7 | Código de función | Qué hacer. |
| 8– | Datos | Para los códigos 1–6: una dirección de 2 bytes y una cantidad o un valor de 2 bytes. |
Todos los campos de varios bytes son big-endian. El módulo struct de Python los decodifica con el formato >HHHBB (Python struct). Por tanto, la trama 00 03 00 00 00 06 01 06 00 07 01 2c se lee así: transacción 3, protocolo 0, siguen seis bytes, unidad 1, función 6 (escribir un registro), registro 7, valor 0x012C = 300.
Dos comprobaciones de coherencia detectan la mayor parte del tráfico mal formado o no Modbus en el puerto 502: el identificador de protocolo debe ser 0 y el campo de longitud debe ser igual al número de bytes que le siguen. Una herramienta de detección que omite estas comprobaciones interpretará basura como órdenes.
2.4 Ejemplo resuelto: una captura de KW-04 (sintético)
Un técnico exportó ocho peticiones enviadas a la RTU de KW-04 (10.10.4.2) durante una mañana de mantenimiento. La política del KVROC dice que solo la estación de ingeniería eng-ws-15 (10.20.0.15) puede escribir en los controladores de las estaciones; scada-01 (10.20.0.10) solo lee. Lab 2 (guárdelo como lab02.py y ejecútelo con python3 lab02.py):
# Lab 2 - decode Modbus/TCP frames and flag writes (offline, standard library only)
# Synthetic capture from Kestrel Water station KW-04; addresses are private (RFC 1918).
import struct
ENGINEERING = {"10.20.0.15"} # only the engineering workstation may write
WRITE_FC = {5: "write single coil", 6: "write single register",
15: "write multiple coils", 16: "write multiple registers"}
READ_FC = {1: "read coils", 2: "read discrete inputs", 3: "read holding registers", 4: "read input registers"}
capture = [ # (source, destination, frame as hex)
("10.20.0.10", "10.10.4.2", "000100000006010300000004"),
("10.20.0.10", "10.10.4.2", "000200000006010400100002"),
("10.20.0.15", "10.10.4.2", "00030000000601060007012c"),
("10.30.3.7", "10.10.4.2", "000400000006010500a0ff00"),
("10.30.3.7", "10.10.4.2", "00050000000b011000070002040064000a"),
("10.20.0.10", "10.10.4.2", "0006000000060103"),
("10.20.0.10", "10.10.4.2", "000700010006010300000004"),
("10.20.0.10", "10.10.4.2", "000800000006012b0e010000"),
]
def decode(hexframe):
b = bytes.fromhex(hexframe)
if len(b) < 8: return None, "short frame (< 8 bytes)"
tid, pid, length, unit, fc = struct.unpack(">HHHBB", b[:8])
if pid != 0: return None, f"protocol id {pid} is not 0 (not Modbus)"
if length != len(b) - 6: return None, f"length field {length} but {len(b) - 6} bytes follow"
addr, qty = struct.unpack(">HH", b[8:12]) if len(b) >= 12 else (None, None)
return (tid, unit, fc, addr, qty), None
for src, dst, h in capture:
d, err = decode(h)
if err: print(f"{src:<11} -> {dst} MALFORMED {err}"); continue
tid, unit, fc, addr, qty = d
if fc in WRITE_FC:
verdict = "ok (engineering)" if src in ENGINEERING else "ALERT unauthorized write"
print(f"{src:<11} -> {dst} tid={tid} unit={unit} fc={fc:<2} {WRITE_FC[fc]:<24} addr={addr} value/qty={qty} {verdict}")
elif fc in READ_FC:
print(f"{src:<11} -> {dst} tid={tid} unit={unit} fc={fc:<2} {READ_FC[fc]:<24} addr={addr} qty={qty} ok")
else:
print(f"{src:<11} -> {dst} tid={tid} unit={unit} fc={fc:<2} {'other function':<24} REVIEW (not on read/write list)")
Salida esperada (Python 3.13.5):
10.20.0.10 -> 10.10.4.2 tid=1 unit=1 fc=3 read holding registers addr=0 qty=4 ok
10.20.0.10 -> 10.10.4.2 tid=2 unit=1 fc=4 read input registers addr=16 qty=2 ok
10.20.0.15 -> 10.10.4.2 tid=3 unit=1 fc=6 write single register addr=7 value/qty=300 ok (engineering)
10.30.3.7 -> 10.10.4.2 tid=4 unit=1 fc=5 write single coil addr=160 value/qty=65280 ALERT unauthorized write
10.30.3.7 -> 10.10.4.2 tid=5 unit=1 fc=16 write multiple registers addr=7 value/qty=2 ALERT unauthorized write
10.20.0.10 -> 10.10.4.2 MALFORMED length field 6 but 2 bytes follow
10.20.0.10 -> 10.10.4.2 MALFORMED protocol id 1 is not 0 (not Modbus)
10.20.0.10 -> 10.10.4.2 tid=8 unit=1 fc=43 other function REVIEW (not on read/write list)
Leamos la salida línea por línea: las lecturas del SCADA son normales; la estación de ingeniería escribió el valor 300 en el registro 7, lo esperado si existe una incidencia de cambio; un puesto corporativo (10.30.3.7) puso la bobina 160 en ON (0xFF00 = 65280) y después escribió dos registros a partir del 7. Esas dos líneas son el hallazgo de la mañana: un equipo de la zona corporativa puede alcanzar el controlador de una estación y modificarlo. Dos tramas están mal formadas (una truncada y otra con un identificador de protocolo distinto de cero), y la función 43 (identificación del dispositivo) no es ni una lectura de datos del proceso ni una escritura, así que se marca para revisión: una consulta de identificación desde un servidor SCADA conocido puede ser una funcionalidad del fabricante; la misma consulta desde un equipo desconocido es un reconocimiento de manual (MITRE incluye el descubrimiento de información de sistemas remotos entre las técnicas de ICS: ATT&CK for ICS).
2.5 Actividad práctica
Decodifique a mano las tres tramas siguientes, añádalas después al Lab 2 y confirme el resultado: 000900000006010100000010, 000a0000000601050003 0000 (quite el espacio), 000b00000006020300640001. Anote la función, la dirección y la cantidad o el valor de cada una, y si el identificador de unidad es el que espera para KW-04. La segunda trama lleva la función 05, la escritura de una bobina, así que anote si se espera que el servidor SCADA envíe alguna vez una a KW-04. Si una trama tiene un espacio en el hexadecimal, quítelo antes de pegarla; los espacios no forman parte de la captura.
2.6 Errores comunes y límites
- Tratar el tráfico del puerto 502 como Modbus sin comprobar el identificador de protocolo y la longitud.
- Leer los números de registro como significados sin el mapa de registros del dispositivo.
- Alertar de todas las consultas de la función 43, o de ninguna; lo que decide es el contexto (origen, frecuencia).
- Olvidar que las respuestas también importan: una respuesta de excepción (código de función + 0x80) le dice que una petición fue rechazada (capítulo 12).
-
Un decodificador solo ve lo que ve su punto de captura: el tráfico entre dos dispositivos del mismo conmutador de estación nunca llega a un sensor del centro de control.
-
Alertar de todas las escrituras sin comprobar si el origen es la estación de ingeniería que nombra la política.
- Tratar una trama mal formada como un ataque cuando puede ser una exportación truncada o una sonda que no es Modbus; notifíquela primero como mal formada.
Ejercicios — capítulo 2
EX005 — Decodificación a mano. Objetivo: leer los campos MBAP y PDU sin código. Entradas: 0010000000060110000a0002 está truncada; 001100000006010f00000008. Pasos: decodifique todos los campos que pueda e indique si cada trama es válida. Evidencia esperada: tablas de campos con razonamiento. Rúbrica: 3 = reconoce la primera como una función 16 a la que le faltan el recuento de bytes y los valores (la longitud 6 es coherente con lo presente, pero una fc 16 válida necesita más), y la segunda como una función 15 en la dirección 0 para 8 bobinas, también incompleta sin recuento de bytes ni datos; 2 = campos correctos, validez no detectada; 1 = parcial; 0 = nada.
EX006 — Amplíe el decodificador. Objetivo: mejorar una herramienta de detección. Entradas: el Lab 2. Pasos: (1) para la función 16, decodifique también el recuento de bytes y los valores de los registros; (2) imprima los valores de la trama de 10.30.3.7. Evidencia esperada: cambio de código y nueva línea de salida. Rúbrica: 3 = se imprimen el recuento de bytes 4 y los valores 100 y 10, con una comprobación de longitud de que el recuento de bytes es igual a 2 × cantidad; 2 = valores sin comprobación; 1 = intentado; 0 = nada.
EX007 — Política de escritura como datos. Objetivo: expresar una lista de permitidos con precisión. Entradas: la política del KVROC. Pasos: escriba una lista de permitidos con entradas (origen, rango de destino, códigos de función) para la operación normal y para una ventana de mantenimiento aprobada, e indique quién conmuta entre ambas. Evidencia esperada: dos tablas breves y un responsable. Rúbrica: 3 = SCADA con códigos de solo lectura hacia todas las estaciones; escrituras de ingeniería solo hacia la estación nombrada en la incidencia y solo durante la ventana; conmutación a cargo de la guardia del KVROC con aprobación de Kestrel Water; 2 = sin ventana; 1 = vago; 0 = nada.
EX008 — ¿Reconocimiento o funcionalidad? Objetivo: razonar sobre tráfico ambiguo. Entradas: consultas de la función 43 desde scada-01 cada 24 h a todas las estaciones, y la misma consulta desde 10.20.0.31 a cuatro estaciones en tres segundos. Pasos: decida cuál requiere actuar y qué comprobaría primero. Evidencia esperada: una nota breve. Rúbrica: 3 = la consulta diaria del SCADA tratada como probable funcionalidad de inventario que debe confirmarse con el fabricante; la ráfaga desde un equipo desconocido tratada como hallazgo prioritario: identificar 10.20.0.31 (el capítulo 17 vuelve sobre él) sin sondearlo; 2 = prioridad correcta, comprobaciones débiles; 1 = ambas ignoradas o ambas alarmadas; 0 = nada.
Síntesis del capítulo
Los protocolos industriales clásicos confían en quien pueda alcanzarlos, de modo que la posición en la red es autoridad. Las tramas Modbus/TCP tienen una estructura fija y comprobable; decodificarlas correctamente, incluidas las comprobaciones de longitud y de protocolo, es la base de todos los filtros y detecciones posteriores. Las escrituras desde el lugar equivocado son la primera regla; las funciones de identificación y de diagnóstico necesitan contexto; y un decodificador solo ve lo que ve su punto de captura.
Siga estudiando — capítulo 02 de 18
← Capítulo 01 · Índice de capítulos del curso · Capítulo 03 →
Continúe con la siguiente lección escrita.