Mapa de la lección
Lección 04 publicada. Mapa, ejemplo, EX008.
flowchart TD
N0["Resultados de aprendizaje y conexión"]
N1["4.1 Los programas como descripciones precisas"]
N2["4.2 Implementar la suma de verificación de un protoc"]
N3["4.3 Análisis sintáctico defensivo"]
N4["4.5 Actividad práctica: Lab 3"]
N5["Síntesis del capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5
N5 --> EX["Ejemplo resuelto"]
EX --> Q["Ejercicio EX008"]
Dibujo lógico
flowchart LR
N0["Resultados de aprendizaje y conexión"]
N1["4.1 Los programas como descripciones precisas"]
N2["4.2 Implementar la suma de verificación de un protoc"]
N3["4.3 Análisis sintáctico defensivo"]
N4["4.5 Actividad práctica: Lab 3"]
N5["Síntesis del capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> Q["EX008"]
[Resultados de aprendizaje y conexión]
|
v
[4.1 Los programas como descripciones precisas]
|
v
[4.2 Implementar la suma de verificación de un protoc]
|
v
[4.3 Análisis sintáctico defensivo]
|
v
[4.5 Actividad práctica: Lab 3]
|
v
[Síntesis del capítulo]
|
v
[Ejemplo resuelto] --> [Ejercicio EX008]
Resultados de aprendizaje y conexión
Al terminar este capítulo, usted podrá (1) escribir programas pequeños y correctos en Python con funciones, bucles, condicionales, bytes y diccionarios; (2) implementar un algoritmo publicado, el CRC-16 de Modbus RTU, y probarlo contra un vector conocido; (3) analizar entradas binarias no confiables de forma defensiva, rechazando los datos malformados en lugar de repararlos; y (4) explicar por qué el código de análisis sintáctico es crítico para la seguridad en la infraestructura. Convierte en código la lógica del capítulo 2 y prepara el capítulo 9.
4.1 Los programas como descripciones precisas
Un programa es una descripción de un cálculo lo bastante precisa para una máquina. El software de infraestructura debe ser preciso en tres cosas que los principiantes confunden: los tipos (un byte no es un carácter; un registro entero no es un número de coma flotante), los límites (qué ocurre en el primer y el último elemento, en cero, en el máximo) y la falla (qué hace el programa con una entrada que no esperaba). En este libro se usa Python porque su biblioteca estándar cubre bytes, hashing y simulación sin instalar nada. Su tipo bytes es inmutable y, al indexarlo, devuelve enteros; int.from_bytes(b, "big") convierte con un orden de bytes explícito.
4.2 Implementar la suma de verificación de un protocolo
Modbus es un protocolo industrial muy longevo; su forma serie (RTU) termina cada trama con una verificación de redundancia cíclica de 16 bits, enviada con el byte bajo primero, tal como la definen las especificaciones del protocolo de aplicación Modbus y de línea serie publicadas por la Modbus Organization. Un CRC detecta con alta probabilidad la corrupción accidental, como el ruido en una línea serie. No es un mecanismo de seguridad: cualquiera puede calcular un CRC válido para una trama falsificada. Aprenda pronto la diferencia: integridad frente a la naturaleza frente a integridad frente a un adversario (capítulo 19).
El CRC estándar de Modbus empieza en 0xFFFF, aplica XOR de cada byte sobre los bits bajos y desplaza a la derecha ocho veces, aplicando XOR con 0xA001 cuando el bit expulsado es 1. Un vector de prueba muy difundido es la petición 01 03 00 00 00 0A (unidad 1, leer 10 registros de retención desde la dirección 0), cuyos bytes de CRC son C5 CD. Probar contra un vector antes de usar su función no es negociable.
4.3 Análisis sintáctico defensivo
Un analizador que lee datos de un dispositivo de campo, de una red o de un archivo es la frontera entre el mundo exterior y las suposiciones de su programa. Reglas: compruebe la longitud antes de indexar; compruebe la integridad (CRC) antes de confiar en cualquier campo; compruebe cada campo contra lo que usted pidió (unidad, función, número de bytes); compruebe los valores contra los límites físicos; ante cualquier falla, rechace e informe: no adivine. Las listas de debilidades comunes de OWASP y CWE (CWE Top 25) están dominadas por errores de manejo de entradas: lecturas y escrituras fuera de límites, validación de entradas inadecuada, desbordamiento de enteros.
4.4 Ejemplo resuelto: lectura del nivel de Hollin Hill (sintético)
La RTU de LRWA en PS3 devuelve dos registros: el nivel del depósito en centímetros y la presión de salida en kPa. El Lab 3 construye la trama de petición y luego analiza cinco respuestas: una correcta, una con un solo bit invertido, una excepción Modbus, un valor fuera del rango físico del tanque (65535 cm, un registro sin inicializar) y una trama truncada. Cada caso erróneo se rechaza con un motivo sobre el que un operador o un analista de registros puede actuar.
Cada motivo de rechazo está redactado para que apunte a una causa: una falla de CRC sugiere ruido en la línea, una excepción apunta al propio estado de la RTU y una falla de rango sugiere una avería o un registro sin inicializar que mantenimiento debe investigar.
4.5 Actividad práctica: Lab 3
# Lab 3 - Modbus RTU CRC-16 and a defensive response parser (offline)
def crc16(data: bytes) -> int:
crc = 0xFFFF
for b in data:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc
def frame(body: bytes) -> bytes:
c = crc16(body); return body + bytes([c & 0xFF, c >> 8]) # low byte first
req = frame(bytes.fromhex("01030000000A"))
print("request:", req.hex(" "))
def parse(resp: bytes, unit=1, count=2, lo=0, hi=4000):
if len(resp) < 5: return "reject: short"
if crc16(resp[:-2]) != resp[-2] | resp[-1] << 8: return "reject: crc"
if resp[0] != unit: return "reject: unit"
if resp[1] & 0x80: return f"reject: exception {resp[2]}"
if resp[1] != 3 or resp[2] != 2 * count or len(resp) != 5 + 2 * count: return "reject: shape"
vals = [int.from_bytes(resp[3 + 2*i:5 + 2*i], "big") for i in range(count)]
if any(not lo <= v <= hi for v in vals): return f"reject: range {vals}"
return f"ok {vals}"
good = frame(bytes.fromhex("010304") + (3150).to_bytes(2, "big") + (212).to_bytes(2, "big"))
tests = {"good": good, "bitflip": good[:4] + bytes([good[4] ^ 1]) + good[5:],
"exception": frame(bytes.fromhex("018302")), "outrange": frame(bytes.fromhex("010304FFFF00D4")),
"truncated": good[:6]}
for k, v in tests.items(): print(f"{k:9} {parse(v)}")
Salida esperada:
request: 01 03 00 00 00 0a c5 cd
good ok [3150, 212]
bitflip reject: crc
exception reject: exception 2
outrange reject: range [65535, 212]
truncated reject: crc
La trama truncada se rechaza en la comprobación de CRC porque sus dos últimos bytes no son un CRC; un analizador que comprobara primero la forma daría un motivo más claro. Ambos órdenes son seguros; la propiedad importante es que nada se acepta con evidencia parcial.
Observe que la respuesta correcta se decodifica como un nivel de 3.150 cm y una presión de 212 kPa, ambos dentro de los límites físicos configurados en el analizador. El caso fuera de rango muestra por qué reparar es peligroso: recortar 65.535 al máximo de 4.000 cm informaría de un depósito lleno cuando el registro nunca se había escrito. Rechazarlo con el valor bruto en el motivo da al personal de mantenimiento la pista que necesita y da a un investigador evidencia de que el valor no era simplemente ruido en la línea.
4.6 Errores comunes y límites
- Errores de orden de bytes (los registros Modbus son big-endian; el CRC se transmite con el byte bajo primero).
- Indexar antes de comprobar la longitud; confiar en un campo de longitud dentro del mensaje.
- Tratar un CRC como autenticación.
- «Reparar» una entrada errónea (recortar 65535 a 4000) en lugar de rechazarla, lo que oculta fallas y ataques.
- Escribir pruebas solo para el camino feliz.
Cada punto tiene una prueba correspondiente en el Lab 3 o en el EX008. Un analizador que supera esas pruebas pero nunca se comprobó contra una trama truncada o malformada solo se ha probado para el camino feliz, y la primera línea serie ruidosa de una estación de bombeo mostrará qué hace con una entrada que no esperaba. Escriba primero las pruebas de falla, porque son las que importan cuando algo sale mal en el campo.
Ejercicios — capítulo 4
EX007 — CRC a mano y con código. Objetivo: verificar una implementación. Entradas: Lab 3. Pasos: (1) calcule con la función el CRC de 01 06 00 01 00 03 (escribir un único registro); (2) invierta un bit de los datos y muestre que el CRC cambia; (3) explique en dos frases por qué el CRC no detiene a un atacante en la línea serie. Evidencia esperada: valores hexadecimales y explicación. Rúbrica: 3 = CRC correcto (verifíquelo con el resultado resuelto), demostración, afirmación de seguridad correcta; 2 = falta uno; 1 = código no ejecutado; 0 = nada.
EX008 — Endurecimiento del analizador. Objetivo: mejorar el informe de errores sin debilitar las comprobaciones. Entradas: parse(). Pasos: reordene las comprobaciones para que una trama truncada informe «short» o «shape» antes del CRC, añada una comprobación que rechace una respuesta cuyo identificador de unidad sea 0 (difusión) y añada dos casos de prueba. Evidencia esperada: diff del código y salidas. Rúbrica: 3 = se conservan todos los rechazos originales, comprobaciones nuevas correctas, pruebas incluidas; 2 = se pierde un rechazo; 1 = sin probar; 0 = nada.
Síntesis del capítulo
Escriba código explícito sobre tipos, límites y fallas. Implemente los algoritmos publicados contra los vectores de prueba publicados. Trate cada analizador como una frontera de seguridad: longitud, integridad, forma y, después, significado físico; y rechace con un motivo en lugar de adivinar. Mantenga separadas en su mente la detección de errores accidentales (CRC) y la integridad frente a adversarios (MAC y firmas).