OpenCyber Academy
Engenharia · Capítulo 4 · Português
Código do curso: OCA-138Capítulo 04: OCA-138-C004

Capítulo 4 — Programação I

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

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

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).

Respostas comentadas

EX007. 01 06 00 01 00 03 → bytes de CRC 98 0B (quadro 01 06 00 01 00 03 98 0b). Trocar o último byte de dados para 02 dá 59 CB. Um atacante na linha serial calcula o CRC de um quadro forjado exatamente como o mestre legítimo, porque o algoritmo não tem segredo; o CRC só detecta corrupção acidental.

EX008. Mova len(resp) < 5 e uma verificação de comprimento total (len(resp) == 5 + 2*count para respostas normais, 5 para exceções) para antes do CRC; depois o CRC; depois if resp[0] == 0: return "reject: broadcast" e a verificação de unidade; depois função/contagem de bytes; depois a faixa. Testes: quadro truncado → "reject: shape"; quadro com unidade 0 e CRC válido → "reject: broadcast". Todos os casos originais mantêm uma rejeição. (Requisições de broadcast não recebem resposta no Modbus; uma resposta da unidade 0 é malformada.)