
Corretor de Isca Experimental
Uma isca de rede conteinerizada (honeypot) que anuncia SSH, RDP e SMB, observa toda conexão de entrada com eBPF e faz proxy reverso de cada sessão para um contêiner isolado.
O design separa duas preocupações:
Esta é uma ferramenta defensiva para detectar e estudar atividade não autorizada em redes que você possui ou está autorizado a monitorar. Implante-a apenas onde você possui essa autoridade.
flowchart TB
A["Attacker / Scanner"]
subgraph host["Decoy Host"]
direction TB
NIC["broker eth0<br/>published: 22, 3389, 445"]
subgraph brk["broker container"]
direction TB
E["eBPF TC classifier<br/>logs every SYN<br/>sees true source IP"]
P["reverse proxy<br/>CONNECT to backend"]
L["structured JSON logs"]
end
subgraph dec["decoynet (internal, no host route)"]
direction LR
S["ssh-decoy<br/>OpenCanary ssh<br/>port 2222"]
D["rdp-decoy<br/>OpenCanary rdp<br/>port 3389"]
M["smb-decoy<br/>Impacket SMB server<br/>port 445"]
end
end
A --> NIC
NIC --> E
NIC --> P
E --> L
P --> S
P --> D
P --> M
Quatro contêineres no total:
As iscas vivem em uma rede Docker internal (decoynet) sem rota para o host ou o mundo externo. Apenas o broker pode alcançá-las. Nada que um atacante faça dentro de uma isca pode alcançar diretamente a rede do host.
O broker publica as portas 22, 3389 e 445 para o host, então os pacotes de entrada chegam no eth0 do broker. Duas coisas acontecem com cada pacote:
broker/bpf/decoy.bpf.c) analisa os cabeçalhos Ethernet, IP e TCP, e para cada nova tentativa de conexão (SYN definido, ACK limpo) escreve um conn_event em um ring buffer: IP e porta de origem, porta de destino, flags TCP e se a porta é um serviço anunciado. O pacote é passado inalterado (TC_ACT_OK).CONNECT para o backend da isca daquele serviço, então retransmite bytes em ambos os sentidos.O mapa eBPF advertised_ports é preenchido na inicialização a partir de config.yaml, para que o classificador possa marcar se uma sonda atingiu uma porta servida ou não. Isso torna visíveis varreduras horizontais de portas, mesmo que apenas três portas sejam proxy reverso.
Se você quiser anunciar "tudo está aberto" e encaminhar portas de destino arbitrárias para o broker, estenda o classificador para reescrever a porta de destino ou use um redirecionamento TPROXY / bpf_sk_assign. A versão atual mantém o caminho do pacote intacto e limita-se à observação, que é o padrão mais seguro.
cyber-decoy/
├── README.md
├── docker-compose.yml # Stack de 4 contêineres
├── docker-compose.override.yml # Desenvolvimento local macOS: sem capacidades eBPF, remapeamento da porta 22
├── Makefile # Auxiliares build / up / down / bpf
├── LICENSE
├── scripts/
│ └── setup.sh # Verificações prévias do host
├── broker/
│ ├── Dockerfile # Compila o objeto eBPF + binário Go
│ ├── config.yaml # Serviços anunciados (configurável)
│ ├── go.mod
│ ├── main.go # Ponto de entrada
│ ├── bpf/
│ │ └── decoy.bpf.c # Classificador TC eBPF
│ └── internal/
│ ├── config/config.go # Carregador de configuração
│ ├── proxy/proxy.go # Proxy reverso TCP
│ └── bpf/loader.go # Carrega + anexa eBPF, transmite eventos
└── decoys/ # Todos os três executam OpenCanary
├── ssh/
│ ├── Dockerfile
│ └── opencanary.conf # Módulo ssh, porta 2222
├── rdp/
│ ├── Dockerfile
│ └── opencanary.conf # Módulo rdp, porta 3389
└── smb/
├── Dockerfile # Processo Python único, não-root
├── smb_decoy.py # SimpleSMBServer do Impacket + registro JSON
└── requirements.txt # impacket (pinned)
docker-compose.override.yml incluído, que depende das tags !reset / !override).sudo mount -t bpf bpf /sys/fs/bpf.A imagem do broker detecta sua arquitetura de compilação e passa a macro __TARGET_ARCH_* correspondente para o clang, portanto compila tanto em x86_64 quanto em aarch64 (Apple Silicon, Graviton). Observe que o gcc-multilib deliberadamente não está instalado: é um pacote apenas x86 sem candidato arm64, e incluí-lo quebra a compilação em arm64 com o código de saída apt 100. Apenas clang e libbpf-dev são necessários para compilar o objeto eBPF.
O Docker Desktop no macOS executa contêineres dentro de uma VM LinuxKit, não no kernel do host, portanto a anexação TC/TCX eBPF geralmente não funcionará lá. Isso não é fatal: o eBPF é intencionalmente de melhor esforço, então o broker registra ebpf disabled: attach failed e o proxy reverso mais todas as três iscas funcionam e registram normalmente. Você pode desenvolver e testar todo o caminho do proxy localmente e obter observação eBPF real quando implantar em um host Linux.
O docker-compose.override.yml é carregado automaticamente e torna isso agradável: ele remove as capacidades eBPF (inúteis na VM) e remapeia a porta 22 do host para 2022, já que o sshd do próprio Mac ocupa a porta 22.
docker compose up --build # Desenvolvimento local, override aplicado
docker compose -f docker-compose.yml up -d # Implantação real, override ignorado
Execute a verificação prévia primeiro:
./scripts/setup.sh
# 1. Construir todas as quatro imagens (compila o objeto eBPF dentro da imagem do broker)
make build
# 2. Iniciar a stack
make up
# 3. Observar o que acontece
make logs
Em seguida, sonda a partir de outra máquina (ou localhost para um teste rápido):
ssh -p 22 user@DECOY_HOST # Atinge a isca SSH
nc DECOY_HOST 3389 # Atinge a isca RDP
nc DECOY_HOST 445 # Atinge a isca SMB
nc DECOY_HOST 8080 # Não anunciada: observada pelo eBPF, sem proxy
O broker emite JSON para eventos de sonda eBPF e sessões com proxy; cada isca emite eventos JSON do OpenCanary. Para ver credenciais chegando:
docker compose logs -f ssh-decoy | grep 4002
Diferente de um stub apenas de banner, ssh -p 22 user@DECOY_HOST agora completa uma troca de chaves real e solicita uma senha. Cada tentativa é capturada. Verifique se a impressão digital do serviço se mantém sob detecção de versão:
nmap -sV -p 22,3389,445 DECOY_HOST
Desmonte com:
make down
Os serviços são definidos em broker/config.yaml. Cada entrada é ativável e remapeável independentemente:
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
Para adicionar um serviço, adicione uma entrada aqui, publique a porta em docker-compose.yml e adicione um contêiner de isca. Para desabilitar um, defina enabled: false (e opcionalmente remova sua porta publicada).
Observe que a porta 22 do host geralmente é ocupada pelo daemon SSH real do host. Para um laboratório, você pode remapear o lado publicado em docker-compose.yml, por exemplo "2022:22", e apontar seu scanner para lá.
Todas as três iscas executam OpenCanary (Thinkst), configuradas para que cada contêiner habilite exatamente um módulo. Os logs são emitidos como JSON no stdout, portanto docker compose logs e qualquer coletor SIEM funcionam sem encanamento extra.
O OpenCanary marca cada evento com um logtype numérico. Os que você verá aqui:
Uma credencial SSH capturada se parece com isto:
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
Isso é uma consequência direta da arquitetura do broker, e é a coisa mais importante a entender sobre a leitura desses logs.
O broker encerra a conexão TCP do atacante e abre uma nova para a isca. Portanto, do ponto de vista do OpenCanary, o cliente é o broker. Cada src_host em um evento de isca será o endereço do broker no decoynet, não a fonte real.
O IP verdadeiro da fonte ainda é capturado, apenas em um lugar diferente:
Portanto, a atribuição requer correlacionar logs do broker com logs da isca, unindo por timestamp e serviço. O broker registra remote (o verdadeiro endereço do atacante) e backend para cada sessão, que é o que torna a junção possível:
docker compose logs broker | grep 'session opened' # quem
docker compose logs ssh-decoy | grep '"logtype": 4002' # o que tentaram
Se você precisar do IP real dentro da própria isca, as opções são enviar o protocolo PROXY (as iscas não o analisam, então isso exigiria corrigi-las) ou substituir o proxy de userspace por um redirecionamento transparente (TPROXY ou eBPF bpf_sk_assign) que preserve o endereço de origem original. Ambos estão listados no Roadmap. Até lá, trate o broker como a fonte da verdade para "quem" e a isca como a fonte da verdade para "o quê".
Diferentemente de SSH e RDP, esta isca não usa OpenCanary. O módulo smb do OpenCanary é apenas um observador de logs: ele monitora um arquivo e analisa linhas smbd_audit emitidas por um servidor Samba real, o que significava executar Samba mais rsyslog mais opencanaryd sob supervisord, uma cadeia de cinco elos onde qualquer elo poderia falhar silenciosamente.
O smb-decoy substitui tudo isso por um único processo Python construído no SimpleSMBServer do Impacket, uma implementação pura em Python do SMB1/2/3. Ele vincula a porta 445, apresenta compartilhamentos isca somente leitura, responde à negociação SMB2/3 (para que nmap -sV veja um serviço real) e registra conexões e tentativas de autenticação NTLM como um objeto JSON por linha no stdout. O nome de usuário, domínio e estação de trabalho capturados da mensagem de autenticação do atacante são o payoff da captura de credenciais.
Nota de segurança: o smbserver do Impacket carregava uma vulnerabilidade crítica de path traversal, CVE-2021-31800, que afetava especificamente honeypots. Foi corrigido na versão 0.9.23. O requirements.txt fixa uma versão atual e não deve ser rebaixado abaixo disso. O contêiner também é executado como não-root, somente leitura, com todas as capacidades removidas, exceto NET_BIND_SERVICE.
Cada isca possui seu próprio opencanary.conf (instalado em /etc/opencanaryd/). Ajustes úteis:
ssh.version em decoys/ssh/opencanary.conf. Atualmente alega SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1. Faça corresponder ao SO que você está fingindo ser; um banner Ubuntu em uma máquina que afirma ser Windows é um sinal.addShare(...) em decoys/smb/smb_decoy.py, além dos arquivos isca criados em decoys/smb/Dockerfile. Os nomes de compartilhamentos e arquivos são a isca.backend em broker/config.yaml.Para habilitar outro módulo do OpenCanary (ftp, telnet, mysql, vnc, redis e outros estão disponíveis), defina <module>.enabled e <module>.port, adicione um contêiner de isca e adicione um serviço correspondente em broker/config.yaml.
O ssh-decoy monta um volume nomeado em /var/lib/opencanary (ssh.key_path), para que a chave de host gerada sobreviva a reinicializações. Sem ele, o OpenCanary gera uma nova chave a cada início e a impressão digital variável é um sinal óbvio.
A isca SMB agora é um único processo, portanto a solução de problemas é direta.
docker compose logs -f smb-decoy
Cada linha é JSON. Você deve ver um smb_decoy_start na inicialização, depois eventos smb_connect, smb_auth_attempt e smb_tree_connect à medida que os clientes interagem. Teste-a a partir do host com qualquer cliente SMB:
# macOS Finder: Go > Connect to Server
open 'smb://guest@localhost/HR-Payroll'
# ou do Linux
smbclient -L //localhost -p 445 -N
Problemas comuns:
smb_decoy_start e o contêiner sai: verifique se requirements.txt foi instalado corretamente. O Impacket precisa de Python 3.8+; a imagem usa 3.12.smb_auth_attempt: alguns clientes enumeram compartilhamentos anonimamente sem nunca autenticar. Isso ainda produz smb_connect e smb_tree_connect. Force a autenticação mapeando um compartilhamento com um nome de usuário.src_host é o endereço do broker, não o atacante real. Correlacione com logs do broker por timestamp.NET_ADMIN (e BPF / PERFMON em kernels recentes) para carregar e anexar o programa eBPF. O arquivo Compose solicita essas capacidades com escopo. Se seu host ou versão do Docker as rejeitar, a alternativa é privileged: true no serviço do broker, que é mais amplo e deve ser usado apenas quando capacidades com escopo não funcionam.internal sem rota para o host. Mantenha assim. Trate cada contêiner de isca como potencialmente comprometido.bpf_sk_assign, eliminando a necessidade de correlacionar logs do broker e da isca.MIT. Veja LICENSE.
| Contêiner | Função | Rede |
|---|
broker | Porta de entrada pública: observação eBPF + proxy reverso | edge + decoynet |
ssh-decoy | Módulo ssh do OpenCanary (handshake real, captura credenciais) | decoynet apenas |
rdp-decoy | Módulo rdp do OpenCanary (simula NLA, captura nomes de usuário) | decoynet apenas |
smb-decoy | SimpleSMBServer do Impacket (SMB2/3 real, captura autenticação) | decoynet apenas |
| Contêiner | Módulo OpenCanary | Escuta | O que realmente faz |
|---|
ssh-decoy | ssh | 2222 | Troca de chaves SSH real via twisted.conch. Captura todos os pares usuário/senha. |
rdp-decoy | rdp | 3389 | Simula um servidor com NLA habilitado, sempre retorna falha de login, extrai o nome de usuário mstshash. |
smb-decoy | Impacket | 445 | Servidor SMB2/3 puro em Python. Apresenta compartilhamentos isca e registra conexões e tentativas de autenticação NTLM como JSON. |
| logtype | Constante em opencanary/logger.py | Significado |
|---|
| 1000 | LOG_BASE_BOOT | Inicialização do daemon |
| 4000 | LOG_SSH_NEW_CONNECTION | Conexão SSH aberta |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | Cliente enviou sua string de versão |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | Tentativa de login SSH (inclui USERNAME e PASSWORD) |
| 5000 | LOG_SMB_FILE_OPEN | Arquivo SMB aberto (inclui USER, SHARENAME, FILENAME) |
| 14001 | LOG_RDP | Conexão / tentativa de login RDP |
| Camada | Sabe o IP real da fonte? | Sabe o que foi tentado? |
|---|
Classificador eBPF (probe observed) | Sim | Não, apenas metadados SYN |
Proxy do broker (session opened) | Sim | Não, apenas contagens de bytes |
Isca OpenCanary (logtype 4002) | Não | Sim, credenciais/arquivos |