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

Capítulo 12 — Laboratorio contenido I

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 12 — Laboratorio contenido I

Mapa de la lección

Lección 12 publicada. Mapa, ejemplo, EX045.

flowchart TD
  N0["Resultados de aprendizaje y conexión"]
  N1["12.1 Qué significa «contenido»"]
  N2["12.2 La tarea del respondedor"]
  N3["12.3 El registro dentro del dispositivo"]
  N4["12.5 Rúbrica de evaluación de un laboratorio conteni"]
  N5["12.6 Lo que el laboratorio no modela"]
  N6["12.7 Actividad práctica"]
  N7["Síntesis del capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7
  N7 --> EX["Ejemplo resuelto"]
  EX --> Q["Ejercicio EX045"]

Dibujo lógico

flowchart LR
  N0["Resultados de aprendizaje y conexión"]
  N1["12.1 Qué significa «contenido»"]
  N2["12.2 La tarea del respondedor"]
  N3["12.3 El registro dentro del dispositivo"]
  N4["12.5 Rúbrica de evaluación de un laboratorio conteni"]
  N5["12.6 Lo que el laboratorio no modela"]
  N6["12.7 Actividad práctica"]
  N7["Síntesis del capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> N7 --> Q["EX045"]
[Resultados de aprendizaje y conexión]
    |
    v
[12.1 Qué significa «contenido»]
    |
    v
[12.2 La tarea del respondedor]
    |
    v
[12.3 El registro dentro del dispositivo]
    |
    v
[12.5 Rúbrica de evaluación de un laboratorio conteni]
    |
    v
[12.6 Lo que el laboratorio no modela]
    |
    v
[12.7 Actividad práctica]
    |
    v
[Síntesis del capítulo]
    |
    v
[Ejemplo resuelto] --> [Ejercicio EX045]

Resultados de aprendizaje y conexión

Al terminar este capítulo debería ser capaz de:

Los capítulos 1–11 analizaron el tráfico y la configuración. Este capítulo construye algo que se puede romper sin peligro. Es el primero de dos laboratorios evaluados; el capítulo 17 es el segundo.

12.1 Qué significa «contenido»

Un laboratorio contenido no puede afectar a nada fuera de sí mismo. En este libro eso se garantiza técnicamente, no con buenas intenciones: el Lab 12 usa socket.socketpair(), que crea dos sockets conectados dentro de un mismo proceso (Python socket). Nada escucha en un puerto, nada se encamina, y el programa no puede alcanzar un controlador real ni siquiera por error. Los laboratorios mayores usan redes virtuales aisladas sin ruta hacia fuera, o hardware de repuesto en un banco con su cable de red sin conectar a ningún otro sitio. Anote la propiedad de contención de cada laboratorio que construya y cómo la comprobó.

12.2 La tarea del respondedor

Un controlador real decide si ejecuta una escritura según su propia lógica, que en los dispositivos Modbus clásicos a menudo significa «siempre». Algunos dispositivos y pasarelas de protocolo permiten proteger contra escritura por rangos de registros; muchos no. El respondedor del laboratorio hace explícita la política:

Una respuesta normal a la función 6 repite la petición. Una respuesta normal a la función 3 lleva un recuento de bytes y los valores de los registros.

12.3 El registro dentro del dispositivo

El respondedor registra cada lectura, cada escritura aceptada con su valor anterior y el nuevo, y cada excepción. En los equipos reales, estos son exactamente los registros que habría deseado tener en el capítulo 7. Una ráfaga de respuestas de excepción 02 desde un mismo origen es una señal clara de que alguien está cartografiando el espacio de registros; una racha de excepciones 03 es alguien que prueba los límites. En los equipos reales esos registros suelen faltar, y por eso el laboratorio los hace visibles. Guarde el registro del laboratorio junto con las respuestas en hexadecimal para que un revisor pueda emparejar cada excepción con una petición.

12.4 Ejemplo resuelto: el respondedor de banco (sintético)

Lab 12 (guárdelo como lab12.py y ejecútelo con python3 lab12.py):

# Lab 12 - contained Modbus/TCP responder with a write policy (offline, standard library only)
# Uses socket.socketpair(): both ends live inside this process; nothing touches a real network.
import socket, struct
regs = [0] * 10; regs[0], regs[1], regs[7] = 342, 61, 250      # level cm, chlorine x100, pump setpoint
WRITABLE = {7: (100, 400)}                                      # register: allowed value range
log = []
def respond(req: bytes) -> bytes:
    tid, pid, length, unit, fc = struct.unpack(">HHHBB", req[:8])
    def exc(code):
        log.append(f"tid={tid} fc={fc} exception {code}")
        return struct.pack(">HHHBBB", tid, 0, 3, unit, fc | 0x80, code)
    if pid != 0 or length != len(req) - 6: return exc(3)
    if fc == 3:
        addr, qty = struct.unpack(">HH", req[8:12])
        if not 1 <= qty <= 125 or addr + qty > len(regs): return exc(2)
        data = struct.pack(f">{qty}H", *regs[addr:addr + qty])
        log.append(f"tid={tid} read {addr}..{addr + qty - 1}")
        return struct.pack(">HHHBBB", tid, 0, 3 + len(data), unit, 3, len(data)) + data
    if fc == 6:
        addr, val = struct.unpack(">HH", req[8:12])
        if addr not in WRITABLE: return exc(2)
        lo, hi = WRITABLE[addr]
        if not lo <= val <= hi: return exc(3)
        old, regs[addr] = regs[addr], val
        log.append(f"tid={tid} WRITE reg {addr} {old}->{val}")
        return req[:12]                                         # echo is the normal fc 6 reply
    return exc(1)
client, server = socket.socketpair()
def call(tid, fc, a, b):
    client.sendall(struct.pack(">HHHBBHH", tid, 0, 6, 1, fc, a, b))
    server.sendall(respond(server.recv(260)))
    r = client.recv(260)
    return r.hex()
print("read 0..2     ", call(1, 3, 0, 3))
print("write 7=300   ", call(2, 6, 7, 300))
print("write 7=900   ", call(3, 6, 7, 900))
print("write 0=0     ", call(4, 6, 0, 0))
print("fc 8 diag     ", call(5, 8, 0, 0))
print("read 8..12    ", call(6, 3, 8, 5))
client.close(); server.close()
print("registers", regs[:3], "setpoint", regs[7])
for entry in log: print("log:", entry)

Salida esperada (Python 3.13.5):

read 0..2      0001000000090103060156003d0000
write 7=300    00020000000601060007012c
write 7=900    000300000003018603
write 0=0      000400000003018602
fc 8 diag      000500000003018801
read 8..12     000600000003018302
registers [342, 61, 0] setpoint 300
log: tid=1 read 0..2
log: tid=2 WRITE reg 7 250->300
log: tid=3 fc=6 exception 3
log: tid=4 fc=6 exception 2
log: tid=5 fc=8 exception 1
log: tid=6 fc=3 exception 2

Lea las respuestas en hexadecimal. La primera es una lectura normal: 03 06 (función 3, seis bytes) seguida de 0x0156 = 342, 0x003D = 61 y 0. La escritura de 300 en el registro 7 se repite y el registro anota 250 → 300. Escribir 900 devuelve 86 03: función 6 + 0x80, excepción 3, valor ilegal. Escribir el registro 0 devuelve 86 02, dirección ilegal: el registro 0 es una medición y no se puede escribir. La función 8 (diagnóstico) devuelve 88 01, función ilegal. Una lectura de cinco registros a partir del 8 se sale de la tabla de 10 registros y devuelve 83 02. Los registros del final muestran que solo surtió efecto la escritura aprobada.

12.5 Rúbrica de evaluación de un laboratorio contenido

Criterio 3 (excelente) 1 (débil)
Contención Es técnicamente imposible alcanzar un dispositivo real; la propiedad se declara y se comprueba Depende de «no lo apuntaré a nada»
Reproducibilidad Entradas fijas; salida exacta registrada con la versión de Python La salida varía o no se registra
Declaración de fidelidad Enumera lo que el laboratorio modela y lo que no Afirma representar dispositivos reales
Evidencia Registros y salidas guardados, con hash, vinculados a los ejercicios Solo capturas de pantalla
Seguridad de las ampliaciones Cambios revisados para que no puedan eliminar la contención Añade código de red real sin revisión

12.6 Lo que el laboratorio no modela

Los dispositivos reales difieren: algunos ignoran los identificadores de unidad, algunos aceptan escrituras en cualquier registro, algunos se bloquean con peticiones mal formadas, y pocos registran algo. El respondedor atiende una petición cada vez y no tiene concurrencia, tiempos de espera ni lecturas parciales. Su valor está en hacer visibles la política y las excepciones, no en predecir el comportamiento de un fabricante concreto. No use nunca los resultados de un laboratorio para decidir cómo responderá un dispositivo en servicio a una prueba.

Si el respondedor devuelve la excepción 2 para una lectura de los registros 8 a 12, eso no dice nada sobre lo que haría una RTU real de KW-04. Los dispositivos reales difieren en el tratamiento del identificador de unidad, la protección contra escritura y el registro, y muchos se bloquean con peticiones mal formadas. Indique esas lagunas en la evaluación y limite la verificación en servicio a los métodos delimitados y autorizados del capítulo 11.

12.7 Actividad práctica

Añada al respondedor la función 16 (escribir varios registros) con la misma política: todos los registros del rango deben poder escribirse y todos los valores deben estar dentro de sus límites, o se rechaza la escritura completa. Pruebe cuatro casos: todo permitido; una dirección que no se puede escribir; un valor fuera de rango; un recuento de bytes incoherente con la cantidad. Anote las cuatro respuestas en hexadecimal y, junto a ellas, las líneas del registro. Por último, envíe una escritura con la función 16 que incluya un registro que su tratamiento de la función 6 ya rechaza (como el registro 0), y confirme que el registro da el mismo motivo para ambos rechazos.

12.8 Errores comunes y límites

Ejercicios — capítulo 12

EX045 — Lea las respuestas. Objetivo: decodificar respuestas normales y de excepción. Entradas: la salida del Lab 12. Pasos: para cada una de las seis respuestas, dé el identificador de transacción, la función (con o sin el bit de excepción) y su significado. Evidencia esperada: seis filas. Rúbrica: 3 = las seis correctas, incluidas 0x86 = excepción a la 6, 0x88 = excepción a la 8, 0x83 = excepción a la 3; 2 = una errónea; 1 = solo las respuestas normales; 0 = nada.

EX046 — Implemente la función 16. Objetivo: ampliar el respondedor con seguridad. Entradas: la actividad 12.7. Pasos: impleméntela y ejecute los cuatro casos. Evidencia esperada: código y salidas. Rúbrica: 3 = comportamiento de todo o nada; excepción 02 para una dirección que no se puede escribir, 03 para un valor fuera de rango, 03 (o un tratamiento de petición mal formada justificado) para un recuento de bytes incoherente; la respuesta normal repite solo la dirección y la cantidad; líneas de registro para cada caso; 2 = funciona con un código erróneo; 1 = acepta escrituras parciales; 0 = nada.

EX047 — Detecte intentos de cartografiado. Objetivo: usar los registros del lado del dispositivo. Entradas: el registro del respondedor. Pasos: escriba una regla que alerte del comportamiento de cartografiado o de prueba de límites y pruébela añadiendo cinco lecturas de direcciones fuera de rango desde una misma secuencia de transacciones. Evidencia esperada: regla y salida. Rúbrica: 3 = por ejemplo, «≥3 respuestas de excepción 02 en 60 s desde un mismo origen»; demuestra que se dispara con la prueba y no con las seis peticiones originales (que solo contienen dos respuestas de excepción 02; contar juntas la 02 y la 03 ya llegaría a tres y la dispararía); 2 = se dispara en ambos casos; 1 = solo la regla; 0 = nada.

EX048 — Evalúe un laboratorio. Objetivo: aplicar la rúbrica. Entradas: el laboratorio de un colega que ejecuta un servidor Modbus en 0.0.0.0:502 en un portátil que «solo se usa en la oficina». Pasos: puntúelo en contención y fidelidad y proponga una corrección. Evidencia esperada: puntuaciones y corrección. Rúbrica: 3 = contención 1 (alcanzable desde cualquier red a la que se una el portátil, y el portátil puede llevarse a las estaciones); corrección con socketpair o con enlace solo a la interfaz de bucle invertido más cortafuegos del equipo, o una red de máquinas virtuales aislada; se exige una declaración de fidelidad; 2 = corrección sin razonamiento; 1 = lo acepta; 0 = nada.

Síntesis del capítulo

Un laboratorio contenido es aquel en el que la seguridad es una propiedad técnica. El Lab 12 construye un respondedor Modbus que aplica una política de escritura y devuelve las excepciones adecuadas, haciendo visible lo que ocultan la mayoría de los dispositivos reales: qué peticiones se rechazaron y por qué. Evalúe todo laboratorio en cuanto a contención, reproducibilidad, evidencia y una declaración franca de lo que no modela.

Siga estudiando — capítulo 12 de 18

← Capítulo 11 · Índice de capítulos del curso · Capítulo 13 →

Continúe con la siguiente lección escrita.

Respuestas desarrolladas

EX045 — Lea las respuestas. tid 1: función 3, normal, 6 bytes: 342, 61, 0. tid 2: función 6, repetición normal; se escribió 300 en el registro 7. tid 3: 0x86 = excepción a la función 6, código 03 valor de datos ilegal (900 fuera de 100–400). tid 4: 0x86, código 02 dirección de datos ilegal (el registro 0 no se puede escribir). tid 5: 0x88 = excepción a la función 8, código 01 función ilegal. tid 6: 0x83 = excepción a la función 3, código 02 (el rango 8–12 excede la tabla).

EX046 — Implemente la función 16. Analice addr, qty, bc = struct.unpack(">HHB", req[8:13]); si bc != 2 * qty or len(req) != 13 + bc, devuelva la excepción 03; decodifique los valores; si alguna dirección de addr..addr+qty-1 no está en WRITABLE, devuelva 02; si algún valor está fuera de su rango, devuelva 03; en caso contrario, escriba todos y devuelva struct.pack(">HHHBBHH", tid, 0, 6, unit, 16, addr, qty). Registre cada resultado. Ninguna escritura parcial.

EX047 — Detecte intentos de cartografiado. Regla: tres o más respuestas de excepción 02 a un mismo origen en 60 s. Las seis peticiones originales producen dos respuestas de excepción 02 (tid 4 y tid 6), así que no se dispara; añadir cinco lecturas fuera de rango produce siete y se dispara. Contar juntas la 02 y la 03 ya llegaría a tres en la ejecución original (tids 3, 4, 6) y se dispararía con errores corrientes; mantener aparte la regla de prueba de límites (excepción 03) da alertas más claras.

EX048 — Evalúe un laboratorio. Contención 1: un servidor en 0.0.0.0:502 es alcanzable desde cualquier red a la que se una el portátil, incluida la red de una estación si el portátil se usa allí. Fidelidad: sin declaración, puntuación 1. Corrección: socketpair() o enlace a 127.0.0.1 con una regla del cortafuegos del equipo, o una red de máquinas virtuales aislada sin ruta hacia fuera; añadir una declaración de fidelidad; registrar las salidas con la versión de Python.