12 - Hunting de Rede com Zeek e Suricata
video: https://youtu.be/... no frontmatter da nota.Hunting de Rede com Zeek e Suricata
Nota 12 do capítulo 08 (Técnicas de Hunting por TTPs), que transpõe as técnicas por tática para a fonte de rede.
Trata Zeek como gerador de logs estruturados por protocolo e Suricata como motor de assinaturas com saída EVE JSON.
Dialoga com as notas 13 e 14 deste capítulo, com a nota 04 do capítulo 02 e com os controles DAT.2 e TH.12 do capítulo 15.
1. Contexto e Posicionamento na Biblioteca
As notas anteriores do capítulo 08 definem o que caçar por tática e por técnica do MITRE ATT&CK; esta nota responde onde, na prática, o sinal de rede aparece. O endpoint cega em três situações recorrentes: host sem agente, malware que vive apenas em memória e tráfego que imita aplicações legítimas com bibliotecas comuns. Nesses vazios, a telemetria de rede é frequentemente a única testemunha do comportamento adversário, e por isso o bloco final do capítulo começa por ela. A nota assume que o leitor já domina a formulação de hipóteses do capítulo 07 e conhece o inventário de fontes do capítulo 02.
Zeek e Suricata são os dois sensores canônicos de rede desta biblioteca e cumprem papéis diferentes sobre o mesmo cabo. Zeek observa o tráfego, interpreta protocolos e produz logs tabulares estáveis por conexão, consulta DNS, requisição HTTP, handshake TLS e arquivo transferido; é a base de qualquer NDR (Network Detection and Response) aberto. Suricata aplica assinaturas em tempo real e registra eventos detalhados em EVE JSON, cobrindo os mesmos protocolos. São visões complementares: o alerta de Suricata diz que algo casou com uma assinatura; o log de Zeek descreve a sessão inteira que contém o alerta. O desenho mais comum nos ambientes alvo é rodar os dois no mesmo ponto de observação, por TAP ou SPAN.
2. Zeek como Fonte de Caça: Logs e Campos Essenciais
O Zeek grava logs TSV por protocolo, normalmente em /opt/zeek/logs/current, com rotação diária e cabeçalho #fields declarando as colunas de cada arquivo. Todo registro carrega uid e ts, e o uid é o elo que costura a mesma conexão entre conn, dns, http, ssl e files. A extração de colunas é feita com zeek-cut, que lê a entrada por pipe e devolve apenas os campos pedidos, em qualquer ordem. A tabela abaixo concentra os seis logs de maior retorno em caça e os campos que sustentam as hipóteses do restante da nota.
| Log | O que descreve | Campos centrais para caça |
|---|---|---|
| conn | Cada conexão TCP, UDP ou ICMP observada | id.orig_h, id.resp_h, id.resp_p, proto, service, duration, orig_bytes, resp_bytes, conn_state, history |
| dns | Consultas e respostas DNS | query, qtype_name, rcode_name, answers, TTLs, rtt |
| http | Requisições e respostas HTTP | method, host, uri, user_agent, status_code, request_body_len, response_body_len |
| ssl | Handshakes TLS | server_name, version, cipher, validation_status, subject, issuer, ja3, ja3s |
| files | Arquivos vistos sobre qualquer transporte | source, mime_type, filename, md5, sha256, seen_bytes, total_bytes |
| weird | Anomalias de protocolo que não viraram evento estruturado | name, source, addl |
Dois campos do conn merecem leitura treinada porque resumem a conexão em poucos caracteres. O conn_state codifica o desfecho do handshake e do encerramento: SF indica conexão normal com encerramento limpo, S1 indica conexão estabelecida ainda aberta no fim da observação, REJ indica recusa por RST do destino e S0 indica tentativa sem qualquer resposta. O history guarda a sequência de eventos da conexão letra a letra, e padrões como origem que nunca envia dados com destino respondendo RST denunciam varredura antes de qualquer agregação estatística. Nas versões recentes do Zeek, o ssl.log já entrega ja3 e ja3s nativamente; o JA4 chega por plugins da comunidade ou pelo Suricata 7, como tratam a nota 14 deste capítulo.
3. Análise Prática por Log
A análise em Zeek é iterativa e roda bem em notebook comum: carregue o dia de logs, aplique filtros com zeek-cut, agregue com sort, uniq e awk e refine a hipótese a cada rodada. Para reproduzir um achado histórico, rode zeek -r captura.pcap local sobre um pcap retido e o Zeek recria os logs do período. O zeek-cut aceita a opção -d para converter timestamps epoch em data legível. Uma armadilha prática: campos como user_agent contêm espaços, e o awk com separador padrão desalinha colunas nesses casos; mantenha tais campos como última coluna ou processe a linha inteira com uniq.
O primeiro exemplo de conn.log lista os maiores envios de dados por conexão, filtro inicial de exfiltração que a nota 13 aprofunda em escala de fluxo.
cat conn.log | zeek-cut id.orig_h id.resp_h orig_bytes resp_bytes duration |
awk '$3 > 100000000 { printf "%s %s upload_MB=%.0f dur=%.0fs\n", $1, $2, $3/1048576, $5 }' |
sort -k3 -rn | head -15
O segundo exemplo usa conn_state para achar origens internas gerando volumes de recusa e de tentativa sem resposta, sinal clássico de varredura ou de serviço morto explorado por ferramenta.
cat conn.log | zeek-cut conn_state id.orig_h id.resp_h id.resp_p |
awk '$1 == "REJ" || $1 == "S0" { print $2, $3, $4 }' |
sort | uniq -c | sort -rn | head -20
No http.log, o filtro abaixo cruza método, tamanho de corpo enviado e destino, apontando uploads volumétricos e possíveis webshells, que a nota 14 detalha por campo.
cat http.log | zeek-cut method host uri request_body_len |
awk '$1 == "POST" { print $4, $2, $3 }' | sort -rn | head -15
No files.log, o interesse recai sobre executáveis e contentores de script que atravessaram a rede em qualquer protocolo, inclusive SMTP e HTTP.
cat files.log | zeek-cut source mime_type sha256 total_bytes |
awk '$2 == "application/x-dosexec" { print $3, $4, $1 }' | head -15
O weird.log é o log mais negligenciado e um dos mais baratos de caçar, porque registra o que o Zeek não conseguiu interpretar, exatamente onde ferramentas de evasão de protocolo deixam rastro.
cat weird.log | zeek-cut name source | sort | uniq -c | sort -rn | head -15
Nomes de weird repetidos em muitos hosts indicam anomalia de parser ou de firmware; nomes novos em um só host merecem o pcap correspondente. Cada achado por log deve voltar ao conn.log pelo uid ou pela 5-tupla, porque é no conn que duração, bytes e desfecho dão peso ao achado.
4. Suricata: EVE JSON e Regras Customizadas
O Suricata grava tudo em eve.json, um JSON por linha, com o campo event_type separando alert, dns, http, tls, fileinfo e flow. O alerta carrega signature_id, signature, severity e category, e cada evento de protocolo traz os campos específicos já normalizados, como http.http_user_agent e tls.sni. A manipulação padrão é feita com jq, e a agregação abaixo produz o ranking de alertas por assinatura com origem e destino, base da triagem diária e da escolha de sementes de caça.
jq -c 'select(.event_type=="alert") |
{ts, src: .src_ip, dst: .dest_ip, sid: .alert.signature_id,
sig: .alert.signature, sev: .alert.severity}' /var/log/suricata/eve.json |
sort | uniq -c | sort -rn | head -20
As regras customizadas seguem a sintaxe clássica de IDS: cabeçalho com ação, protocolo, redes e portas, seguido das opções entre parênteses. Palavras-chave de buffer como http.method, http.uri e http.user_agent permitem regras cirúrgicas; detection_filter aplica limiar de frequência por origem, útil para cadência de beacon. Regras da casa usam sid a partir de 1000000, ficam em repositório versionado com msg descritiva e falsepositives anotados, e toda alteração passa por suricata -T -c /etc/suricata/suricata.yaml antes do deploy. A regra de exemplo abaixo dispara quando um host interno faz POST frequente para PHP com user agent de ferramenta de linha de comando, padrão compatível com webshell e com beacon de C2 em HTTP.
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:"CTH: POST recorrente para PHP com UA de ferramenta"; flow:established,to_server; http.method; content:"POST"; http.user_agent; content:"python-requests"; http.uri; pcre:"/\.php(\?|$)/"; detection_filter: track by_src, count 12, seconds 300; sid:1000004; rev:1;)
5. Combinando Zeek e Suricata na Prática
A combinação dos dois sensores no mesmo ponto de observação é o padrão recomendado, e o valor vem do fluxo de trabalho entre eles. O alerta de Suricata é a semente; os logs de Zeek entregam o contexto que a assinatura não carrega, como user agent, fingerprint TLS, domínio consultado e bytes transferidos. O caminho inverso também é valioso: padrão confirmado em caça vira regra de Suricata para detecção contínua. Como o flow_id do Suricata e o uid do Zeek não coincidem, a junção é feita pela 5-tupla com janela temporal curta, o que exige relógios sincronizados por NTP nos dois sensores.
| Aspecto | Zeek | Suricata |
|---|---|---|
| Papel principal | Entendimento de protocolo e log estruturado | Assinatura em tempo real e alerta priorizado |
| Saída | Logs TSV por protocolo, costurados por uid | EVE JSON por evento, incluindo alert, dns, http, tls, fileinfo e flow |
| Força na caça | Consultas iterativas e correlação por uid e 5-tupla | Semente de hipótese e validação de IOC conhecido |
| Custo típico | CPU de análise de protocolo em links altos | CPU de inspeção de conteúdo e memória de regras |
O script abaixo enriquece cada alerta do dia com o resumo das conexões correspondentes no conn.log, primeiro passo de qualquer triagem séria de alerta.
jq -r 'select(.event_type=="alert") | [.src_ip, .dest_ip] | @tsv' eve.json |
sort -u | while IFS=$'\t' read -r s d; do
echo "== $s para $d"
cat conn.log | zeek-cut id.orig_h id.resp_h proto service duration orig_bytes resp_bytes conn_state |
awk -v s="$s" -v d="$d" '$1==s && $2==d'
done | head -60
6. Caso de Caça: Beaconing com conn.log
A hipótese do caso segue a gramática do capítulo 07: SE um host executa malware com C2 em HTTP ou HTTPS, ENTÃO ele estabelece conexões periódicas para um destino externo com pequena variância de intervalo, mapeando T1071 e T1041 do ATT&CK. O método é agrupar conexões por par origem, destino e porta, calcular intervalos entre conexões sucessivas e medir o coeficiente de variação. O script abaixo consome o extrato do conn.log preparado na primeira linha e imprime apenas pares com pelo menos vinte conexões, intervalo médio entre 20 e 900 segundos e variação abaixo de 10 por cento, limiares que excluem tráfego humano e sessões longas legítimas.
cat conn.log | zeek-cut ts id.orig_h id.resp_h id.resp_p orig_bytes resp_bytes > conn_flows.tsv
python3 hunt_beacon.py conn_flows.tsv
# hunt_beacon.py: pares com ritmo periodico e baixa variância de intervalo
import sys, statistics as st
from collections import defaultdict
par = defaultdict(list)
with open(sys.argv[1]) as f:
for linha in f:
campos = linha.split()
if len(campos) < 6:
continue
par[(campos[1], campos[2], campos[3])].append(float(campos[0]))
for (origem, destino, porta), ts in sorted(par.items()):
ts.sort()
if len(ts) < 20:
continue
intervalos = [b - a for a, b in zip(ts, ts[1:])]
media = st.mean(intervalos)
if media <= 0:
continue
cv = st.pstdev(intervalos) / media
if cv < 0.10 and 20 <= media <= 900:
print(f"{origem} -> {destino}:{porta} n={len(ts)} media={media:.1f}s cv={cv:.3f}")
O caso realista da biblioteca: a caça aponta 10.20.7.51 com 60 conexões em uma hora para 203.0.113.44:443, média de 60,2 segundos e cv de 0,03. A qualificação puxa os demais logs pelo host e pela janela do dia, como abaixo, e verifica o que explica a comunicação.
cat dns.log | zeek-cut id.orig_h query rcode_name | awk '$1 == "10.20.7.51"' | tail -25
cat http.log | zeek-cut id.orig_h host uri user_agent | awk '$1 == "10.20.7.51"' | tail -25
cat ssl.log | zeek-cut id.orig_h server_name validation_status ja3 | awk '$1 == "10.20.7.51"' | tail -25
O resultado fecha o quadro: dns.log mostra cdn-metrics-update.net, domínio registrado nove dias antes; http.log mostra user agent python-requests com respostas 200 e corpos pequenos; ssl.log mostra ja3 ausente do baseline do segmento. Beacon legítimo fica descartado porque o destino não tem histórico corporativo, o user agent não casa com software inventariado e o intervalo é de relógio, não de cron documentado. O encaminhamento abre caso de IR com contenção do host, e o padrão vira duas detecções permanentes: regra de Suricata com o URI e o user agent, e consulta agendada no SIEM reproduzindo o script com os limiares do caso. Exclusões do baseline, como agentes de inventário e telemetria de SaaS documentados, entram versionadas para que a próxima execução produza menos ruído.
7. Erros Comuns e Pontos de Atenção
- Caçar rede sem exclusões do baseline; efeito: fila de falsos positivos com agentes legítimos que também falam em intervalos regulares; correção: exceções versionadas com domínio, user agent e intervalo esperado.
- Tratar o alerta de Suricata como veredito; efeito: fechamento precoce baseado em reputação limpa de IP reciclado; correção: enriquecer todo alerta com Zeek e endpoint antes de decidir.
- Revisar consultas após atualização do Zeek; efeito: pipeline quebra em silêncio quando colunas mudam de ordem ou de nome; correção: conferir o cabeçalho #fields de cada log depois de cada upgrade.
- Ignorar a diferença entre alerta e sessão; efeito: contagem de alertas substitui análise de comportamento e esconde mimetização; correção: sempre puxar o conn e os logs de protocolo da mesma janela.
- Misturar fusos e relógios entre sensores; efeito: correlação por janela temporal falha e o achado parece inconsistente; correção: NTP em todos os sensores e verificação periódica de skew.
- Escrever regras customizadas sem sid único e sem registro; efeito: conflito de assinaturas e regras órfãs sem dono; correção: repositório versionado com revisão, justificativa e data.
- Deixar o weird.log fora da rotina; efeito: perda de sinais de evasão de protocolo e de falhas de parser que antecedem incidentes; correção: triagem semanal de nomes novos com retenção do pcap.
8. Integração com o Restante da Biblioteca
Esta nota é a camada de sensor do capítulo 08: a nota 13 estende a cobertura com fluxo onde o pacote completo não chega, e a nota 14 desce aos campos de DNS, HTTP e TLS que aqui aparecem apenas como âncora. O desenho de coleta e a justificativa de custos vivem na nota 04 do capítulo 02 e no controle DAT.2 do capítulo 15, e o processo de hipótese, execução e registro segue o capítulo 07, com o TH.12 do capítulo 15 definindo as salvaguardas por maturidade. A conversão de caças confirmados em regras permanentes é o trabalho do capítulo 18, e as métricas de cobertura e de tempo de detecção alimentam o capítulo 11.
Um detalhe operacional fecha a nota com ganho composto: trate cada script desta nota como item do catálogo de caça, com identificador da hipótese, dono, data da última execução e resultado resumido. Programas maduros rodam o beacon de conn.log em cadência fixa do ciclo PEAK e mantêm as exclusões vivas; o efeito acumulado é que cada execução custa menos e cobre mais, e a biblioteca de hunts da organização passa a valer mais do que qualquer feed isolado de IOC.