◇ OpenCyber Academy
Técnico · Capítulo 2 · Español

Capítulo 2 — Redes y protocolos

Aprendizaje gratuito y abierto para quienes desean proteger a las personas y la tecnología. No se necesita una cuenta para estudiar ni consultar las respuestas.

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:

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

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.

Respuestas desarrolladas

EX005 — Decodificación a mano. Trama 1: tid 0x0010 = 16, protocolo 0, longitud 6, unidad 1, función 16 (escribir varios registros), dirección inicial 10, cantidad 2; pero la función 16 debe llevar además un recuento de bytes (4) y cuatro bytes de valores, así que la PDU está incompleta; un analizador correcto la rechaza aunque la longitud MBAP coincida con los bytes presentes. Trama 2: tid 17, longitud 6, unidad 1, función 15 (escribir varias bobinas), dirección 0, cantidad 8; también le faltan su recuento de bytes (1) y el byte de bobinas. La máxima puntuación señala que las comprobaciones MBAP por sí solas no validan una PDU; cada función tiene su propia estructura.

EX006 — Amplíe el decodificador. En la rama de escritura: if fc in (15, 16): bc = b[12]; data = b[13:13 + bc] y, para la 16, vals = struct.unpack(f">{qty}H", data) tras comprobar bc == 2 * qty and len(data) == bc. Para la trama de 10.30.3.7 ...011000070002040064000a, el recuento de bytes es 4 y los valores son 100 y 10: dos registros a partir del 7 (el punto de consigna y el registro siguiente).

EX007 — Política de escritura como datos. Normal: (10.20.0.10, 10.10.0.0/16, {1, 2, 3, 4}). Ventana de mantenimiento: añadir (10.20.0.15, la estación nombrada en la incidencia, {3, 5, 6, 15, 16}) solo durante la ventana. La guardia del KVROC activa y desactiva la política conforme a una incidencia aprobada por operaciones de Kestrel Water; la conmutación queda registrada y la ventana caduca automáticamente.

EX008 — ¿Reconocimiento o funcionalidad? La consulta diaria desde scada-01 es coherente con una funcionalidad de inventario; confírmelo en la documentación del fabricante del SCADA y añádala al modelo como flujo esperado. La ráfaga de tres segundos desde 10.20.0.31 es prioritaria: un equipo desconocido en Z2 que consulta la identificación de varias estaciones tiene forma de reconocimiento. Las primeras comprobaciones son de solo lectura: registros de DHCP y del conmutador para esa dirección, el inventario de activos, los registros del servidor de salto y del directorio para ese equipo. No lo sondee. El capítulo 17 muestra lo que hizo después.