◇ OpenCyber Academy
Técnico · Capítulo 12 · Português

Capítulo 12 — Laboratório contido I

Ensino gratuito e aberto para quem deseja proteger pessoas e tecnologias. Não é preciso criar uma conta para estudar ou consultar as respostas.

Capítulo 12 — Laboratório contido I

Mapa da aula

Aula 12 publicada. Mapa, exemplo, EX045.

flowchart TD
  N0["12.1 O que contido significa"]
  N1["12.2 O trabalho do respondedor"]
  N2["12.3 Registro dentro do dispositivo"]
  N3["12.5 Rubrica de avaliação de um laboratório contido"]
  N4["12.6 O que o laboratório não modela"]
  N5["12.7 Atividade prática"]
  N6["Síntese do capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6
  N6 --> EX["Exemplo resolvido"]
  EX --> Q["Exercício EX045"]

Desenho lógico

flowchart LR
  N0["12.1 O que contido significa"]
  N1["12.2 O trabalho do respondedor"]
  N2["12.3 Registro dentro do dispositivo"]
  N3["12.5 Rubrica de avaliação de um laboratório contido"]
  N4["12.6 O que o laboratório não modela"]
  N5["12.7 Atividade prática"]
  N6["Síntese do capítulo"]
  N0 --> N1 --> N2 --> N3 --> N4 --> N5 --> N6 --> Q["EX045"]
[12.1 O que contido significa]
    |
    v
[12.2 O trabalho do respondedor]
    |
    v
[12.3 Registro dentro do dispositivo]
    |
    v
[12.5 Rubrica de avaliação de um laboratório contido]
    |
    v
[12.6 O que o laboratório não modela]
    |
    v
[12.7 Atividade prática]
    |
    v
[Síntese do capítulo]
    |
    v
[Exemplo resolvido] --> [Exercício EX045]

Objetivos de aprendizagem e conexão

Ao final deste capítulo, você deverá ser capaz de:

Os Capítulos 1–11 analisaram tráfego e configuração. Este capítulo constrói algo que você pode quebrar com segurança. É o primeiro de dois laboratórios avaliados; o Capítulo 17 é o segundo.

12.1 O que "contido" significa

Um laboratório contido não consegue afetar nada fora de si mesmo. Neste livro, isso é garantido tecnicamente, não por boas intenções: o Laboratório 12 usa socket.socketpair(), que cria dois sockets conectados dentro de um único processo (Python socket). Nada escuta numa porta, nada é roteado, e o programa não consegue alcançar um controlador real nem por engano. Laboratórios maiores usam redes virtuais isoladas, sem rota para fora, ou hardware reserva numa bancada, com o cabo de rede não indo a lugar nenhum. Anote a propriedade de contenção de cada laboratório que construir, e como você a verificou.

12.2 O trabalho do respondedor

Um controlador real decide se executa uma escrita com base na sua própria lógica, o que, nos dispositivos Modbus clássicos, muitas vezes significa "sempre". Alguns dispositivos e gateways de protocolo permitem proteção contra escrita por faixa de registradores; muitos não. O respondedor do laboratório torna a política explícita:

Uma resposta normal à função 6 ecoa a requisição. Uma resposta normal à função 3 carrega uma contagem de bytes e os valores dos registradores.

12.3 Registro dentro do dispositivo

O respondedor registra toda leitura, toda escrita aceita, com o valor antigo e o novo, e toda exceção. Em equipamentos reais, esses são exatamente os registros que você gostaria de ter tido no Capítulo 7. Uma rajada de respostas de exceção 02 vindas de uma única origem é um forte sinal de alguém mapeando o espaço de registradores; uma sequência de exceções 03 é alguém testando limites.

Em equipamentos reais, esses logs muitas vezes não existem, e é por isso que o laboratório os torna visíveis. Guarde o log do laboratório junto com as respostas em hexadecimal, para que um revisor possa associar cada exceção a uma requisição.

12.4 Exemplo resolvido: o respondedor de bancada (sintético)

Laboratório 12 (salve como lab12.py e execute com 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)

Saída 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

Leia as respostas em hexadecimal. A primeira é uma leitura normal: 03 06 (função 3, seis bytes), seguido de 0x0156 = 342, 0x003D = 61 e 0. A escrita de 300 no registrador 7 é ecoada, e o log registra 250 → 300. Escrever 900 devolve 86 03 — função 6 + 0x80, exceção 3, valor ilegal. Escrever no registrador 0 devolve 86 02, endereço ilegal — o registrador 0 é uma medição, não é gravável. A função 8 (diagnóstico) devolve 88 01, função ilegal. Uma leitura de cinco registradores a partir do 8 ultrapassa a tabela de 10 registradores e devolve 83 02. Os registradores no final mostram que só a escrita aprovada teve efeito.

12.5 Rubrica de avaliação de um laboratório contido

Critério 3 (excelente) 1 (fraco)
Contenção Tecnicamente impossível alcançar um dispositivo real; propriedade declarada e verificada Depende de "não vou apontá-lo para nada"
Reprodutibilidade Entradas fixas; saída exata registrada com a versão do Python A saída varia ou não é registrada
Declaração de fidelidade Lista o que o laboratório modela e o que não modela Afirma representar dispositivos reais
Evidências Logs e saídas guardados, com hash, vinculados aos exercícios Apenas capturas de tela
Segurança das extensões Mudanças revisadas para que não possam remover a contenção Acrescenta código de rede real sem revisão

12.6 O que o laboratório não modela

Os dispositivos reais diferem: alguns ignoram os identificadores de unidade, alguns aceitam escritas em qualquer registrador, alguns travam com requisições malformadas, e poucos registram alguma coisa. O respondedor trata uma requisição de cada vez, não tem concorrência, timeouts nem leituras parciais. O seu valor está em tornar visíveis a política e as exceções, não em prever o comportamento de um fornecedor em particular. Nunca use os resultados de um laboratório para decidir como um dispositivo em produção responderá a um teste.

Se o respondedor devolve a exceção 2 para uma leitura dos registradores 8 a 12, isso nada diz sobre o que uma RTU real da KW-04 faria. Os dispositivos reais diferem no tratamento do identificador de unidade, na proteção contra escrita e no registro, e muitos travam com requisições malformadas. Declare essas lacunas na avaliação e mantenha a verificação em produção restrita aos métodos com escopo e autorizados do Capítulo 11.

12.7 Atividade prática

Acrescente ao respondedor a função 16 (escrever vários registradores), com a mesma política: todo registrador da faixa precisa ser gravável e todo valor estar dentro dos limites, ou a escrita inteira é recusada. Teste quatro casos: tudo permitido; um endereço não gravável; um valor fora da faixa; contagem de bytes inconsistente com a quantidade.

Registre as quatro respostas em hexadecimal e as linhas de log ao lado delas. Por fim, envie uma escrita com a função 16 que inclua um registrador que o seu tratamento da função 6 já recusa (como o registrador 0), e confirme que o log dá o mesmo motivo para as duas recusas.

12.8 Erros comuns e limites

Exercícios — capítulo 12

EX045 — Leia as respostas. Objetivo: decodificar respostas de exceção e normais. Insumos: a saída do Laboratório 12. Passos: para cada uma das seis respostas, dar o id de transação, a função (com ou sem o bit de exceção) e o significado. Evidência esperada: seis linhas. Rubrica: 3 = as seis corretas, incluindo 0x86 = exceção à 6, 0x88 = exceção à 8, 0x83 = exceção à 3; 2 = uma errada; 1 = apenas as respostas normais; 0 = nada.

EX046 — Implemente a função 16. Objetivo: estender o respondedor com segurança. Insumos: atividade 12.7. Passos: implementar e executar os quatro casos. Evidência esperada: código e saídas. Rubrica: 3 = comportamento tudo ou nada; exceção 02 para um endereço não gravável, 03 para um valor fora da faixa, 03 (ou um tratamento de requisição malformada, com justificativa) para uma contagem de bytes inconsistente; a resposta normal ecoa apenas o endereço e a quantidade; linhas de log para cada caso; 2 = funciona, com um código errado; 1 = aceita escritas parciais; 0 = nada.

EX047 — Detecte tentativas de mapeamento. Objetivo: usar logs do lado do dispositivo. Insumos: o log do respondedor. Passos: escrever uma regra que alerte sobre comportamento de mapeamento ou de teste de limites e testá-la acrescentando cinco leituras de endereços fora da faixa a partir de uma sequência de transações. Evidência esperada: regra e saída. Rubrica: 3 = por exemplo, "≥3 respostas de exceção 02 em 60 s vindas de uma única origem"; demonstra que ela dispara no teste e não nas seis requisições originais (que contêm apenas duas respostas de exceção 02; contar 02 e 03 juntas já chegaria a três e dispararia); 2 = dispara nos dois; 1 = apenas a regra; 0 = nada.

EX048 — Avalie um laboratório. Objetivo: aplicar a rubrica. Insumos: o laboratório de um colega que executa um servidor Modbus em 0.0.0.0:502 num notebook "usado só no escritório". Passos: pontuá-lo em contenção e fidelidade e propor uma correção. Evidência esperada: pontuações e correção. Rubrica: 3 = contenção 1 (alcançável a partir de qualquer rede em que o notebook entre, e o notebook pode ser levado às estações); correção com socketpair ou vinculação apenas ao loopback, mais firewall de host, ou uma rede de VMs isolada; declaração de fidelidade exigida; 2 = correção sem raciocínio; 1 = aceita; 0 = nada.

Síntese do capítulo

Um laboratório contido é aquele em que a segurança é uma propriedade técnica. O Laboratório 12 constrói um respondedor Modbus que aplica uma política de escrita e devolve exceções adequadas, tornando visível o que a maioria dos dispositivos reais esconde: quais requisições foram recusadas, e por quê. Avalie todo laboratório quanto a contenção, reprodutibilidade, evidências e uma declaração franca do que ele não modela.

Continue estudando — capítulo 12 de 18

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

Siga para a próxima lição escrita.