Mapa da aula
Aula 04 publicada. Mapa, exemplo, EX008.
flowchart TD
N0["4.1 Programas como descrições precisas"]
N1["4.2 Implementando o checksum de um protocolo"]
N2["4.3 Interpretação defensiva"]
N3["4.5 Atividade prática: Laboratório 3"]
N4["Síntese do capítulo"]
N0 --> N1 --> N2 --> N3 --> N4
N4 --> EX["Exemplo resolvido"]
EX --> Q["Exercício EX008"]
Desenho lógico
flowchart LR
N0["4.1 Programas como descrições precisas"]
N1["4.2 Implementando o checksum de um protocolo"]
N2["4.3 Interpretação defensiva"]
N3["4.5 Atividade prática: Laboratório 3"]
N4["Síntese do capítulo"]
N0 --> N1 --> N2 --> N3 --> N4 --> Q["EX008"]
[4.1 Programas como descrições precisas]
|
v
[4.2 Implementando o checksum de um protocolo]
|
v
[4.3 Interpretação defensiva]
|
v
[4.5 Atividade prática: Laboratório 3]
|
v
[Síntese do capítulo]
|
v
[Exemplo resolvido] --> [Exercício EX008]
Objetivos de aprendizagem e conexão
Ao fim deste capítulo você será capaz de (1) escrever programas Python pequenos e corretos com funções, laços, condicionais, bytes e dicionários; (2) implementar um algoritmo publicado — o CRC-16 do Modbus RTU — e testá-lo contra um vetor conhecido; (3) interpretar entradas binárias não confiáveis de forma defensiva, rejeitando em vez de consertar dados malformados; e (4) explicar por que o código de interpretação (parsing) é crítico para a segurança em infraestrutura. Ele transforma em código a lógica do Capítulo 2 e prepara o Capítulo 9.
4.1 Programas como descrições precisas
Um programa é a descrição de uma computação, precisa o bastante para uma máquina. Software de infraestrutura precisa ser preciso sobre três coisas que os iniciantes confundem: tipos (um byte não é um caractere; um registrador inteiro não é um número de ponto flutuante), limites (o que acontece no primeiro e no último elemento, no zero, no máximo) e falha (o que o programa faz com uma entrada que não esperava). Este livro usa Python porque sua biblioteca padrão cobre bytes, hashing e simulação sem instalação. Seu tipo bytes é imutável e indexável por inteiros; int.from_bytes(b, "big") converte com uma ordem de bytes explícita.
4.2 Implementando o checksum de um protocolo
O Modbus é um protocolo industrial de longa vida; sua forma serial (RTU) termina cada quadro com uma verificação de redundância cíclica de 16 bits, enviada com o byte menos significativo primeiro, conforme definido nas especificações do protocolo de aplicação Modbus e da linha serial publicadas pela Modbus Organization. Um CRC detecta corrupção acidental — ruído numa linha serial — com alta probabilidade. Ele não é um mecanismo de segurança: qualquer pessoa pode calcular um CRC válido para um quadro forjado. Aprenda a diferença cedo: integridade contra a natureza versus integridade contra um adversário (Capítulo 19).
O CRC padrão do Modbus começa em 0xFFFF, faz XOR de cada byte nos bits menos significativos e desloca para a direita oito vezes, fazendo XOR com 0xA001 quando o bit deslocado para fora é 1. Um vetor de teste amplamente publicado é a requisição 01 03 00 00 00 0A (unidade 1, ler 10 registradores holding a partir do endereço 0), cujos bytes de CRC são C5 CD. Testar contra um vetor antes de usar sua função é inegociável.
4.3 Interpretação defensiva
Um interpretador (parser) que lê dados de um dispositivo de campo, de uma rede ou de um arquivo é a fronteira entre o mundo externo e as suposições do seu programa. Regras: verifique o comprimento antes de indexar; verifique a integridade (CRC) antes de confiar em qualquer campo; confira cada campo contra o que você pediu (unidade, função, contagem de bytes); confira os valores contra os limites físicos; diante de qualquer falha, rejeite e informe — não adivinhe. As listas da OWASP e da CWE de fraquezas comuns (CWE Top 25) são dominadas por erros de tratamento de entrada: leituras e escritas fora dos limites, validação inadequada de entrada, estouro de inteiros.
4.4 Exemplo resolvido: lendo o nível de Hollin Hill (sintético)
A RTU da LRWA na PS3 devolve dois registradores: o nível do reservatório em centímetros e a pressão de saída em kPa. O Laboratório 3 monta o quadro de requisição e depois interpreta cinco respostas: uma boa, uma com um único bit invertido, uma exceção Modbus, um valor fora da faixa física do tanque (65535 cm — um registrador não inicializado) e um quadro truncado. Cada caso ruim é rejeitado com um motivo sobre o qual um operador ou um analista de logs consegue agir.
Cada motivo de rejeição é escrito para apontar uma causa: uma falha de CRC sugere ruído na linha, uma exceção aponta para o estado da própria RTU, e uma falha de faixa sugere um defeito ou um registrador não inicializado que a manutenção deve investigar.
4.5 Atividade prática: Laboratório 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)}")
Saída 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
O quadro truncado é rejeitado pela verificação de CRC porque seus dois últimos bytes não são um CRC; um interpretador que verificasse o formato primeiro daria um motivo mais claro. As duas ordens são seguras; a propriedade importante é que nada seja aceito com base em evidência parcial.
Observe que a resposta boa é decodificada como um nível de 3.150 cm e uma pressão de 212 kPa, ambos dentro dos limites físicos configurados no interpretador. O caso fora da faixa mostra por que consertar é perigoso: limitar 65.535 ao máximo de 4.000 cm informaria um reservatório cheio quando o registrador nunca tinha sido escrito. Rejeitá-lo com o valor bruto no motivo dá à equipe de manutenção a pista de que precisa, e dá a um investigador a evidência de que o valor não era simplesmente ruído na linha.
4.6 Erros comuns e limites
- Erros de ordem de bytes (os registradores Modbus são big-endian; o CRC é transmitido com o byte menos significativo primeiro).
- Indexar antes de verificar o comprimento; confiar num campo de comprimento dentro da mensagem.
- Tratar um CRC como autenticação.
- "Consertar" uma entrada ruim (limitar 65535 a 4000) em vez de rejeitá-la, o que esconde falhas e ataques.
- Escrever testes apenas para o caminho feliz.
Cada item tem um teste correspondente no Laboratório 3 ou no EX008. Um interpretador que passa nesses testes, mas nunca foi conferido contra um quadro truncado ou malformado, só foi testado no caminho feliz, e a primeira linha serial ruidosa numa estação de bombeamento vai mostrar o que ele faz com uma entrada que não esperava. Escreva primeiro os testes de falha, porque são eles que importam quando algo dá errado em campo.
Exercícios — capítulo 4
EX007 — CRC à mão e por código. Objetivo: verificar uma implementação. Entradas: Laboratório 3. Passos: (1) calcule com a função o CRC de 01 06 00 01 00 03 (escrever um registrador); (2) inverta um bit nos dados e mostre que o CRC muda; (3) explique em duas frases por que um atacante na linha serial não é impedido pelo CRC. Evidência esperada: valores hexadecimais e explicação. Rubrica: 3 = CRC correto (confira na resolução), demonstração, afirmação de segurança correta; 2 = um item ausente; 1 = código não executado; 0 = nada.
EX008 — Endurecimento do interpretador. Objetivo: melhorar o relato de erros sem enfraquecer as verificações. Entradas: parse(). Passos: reordene as verificações para que um quadro truncado informe "short" ou "shape" antes do CRC, acrescente uma verificação que rejeite uma resposta cujo identificador de unidade seja 0 (broadcast) e acrescente dois casos de teste. Evidência esperada: diff do código e saídas. Rubrica: 3 = todas as rejeições originais preservadas, novas verificações corretas, testes incluídos; 2 = uma rejeição perdida; 1 = sem testes; 0 = nada.
Síntese do capítulo
Escreva código explícito sobre tipos, limites e falhas. Implemente algoritmos publicados contra vetores de teste publicados. Trate todo interpretador como uma fronteira de segurança: comprimento, integridade, formato e, por fim, significado físico, e rejeite com um motivo em vez de adivinhar. Mantenha separadas, na sua cabeça, a detecção de erros acidentais (CRC) e a integridade contra adversários (MACs e assinaturas).