
Broker di decoy sperimentale
Un'esca di rete containerizzata (honeypot) che pubblicizza SSH, RDP e SMB, osserva ogni connessione in entrata con eBPF e fa da proxy inverso per ogni sessione in un container di decoy isolato.
Il design separa due aspetti:
Questo è uno strumento difensivo per rilevare e studiare attività non autorizzate su reti di tua proprietà o che sei autorizzato a monitorare. Distribuiscilo solo dove hai tale autorità.
flowchart TB
A["Attaccante / Scanner"]
subgraph host["Host Decoy"]
direction TB
NIC["broker eth0<br/>pubblicate: 22, 3389, 445"]
subgraph brk["container broker"]
direction TB
E["classificatore eBPF TC<br/>registra ogni SYN<br/>vede il vero IP sorgente"]
P["proxy inverso<br/>CONNECT al backend"]
L["log JSON strutturati"]
end
subgraph dec["decoynet (interna, senza route host)"]
direction LR
S["ssh-decoy<br/>OpenCanary ssh<br/>porta 2222"]
D["rdp-decoy<br/>OpenCanary rdp<br/>porta 3389"]
M["smb-decoy<br/>Server SMB Impacket<br/>porta 445"]
end
end
A --> NIC
NIC --> E
NIC --> P
E --> L
P --> S
P --> D
P --> M
Quattro container in totale:
I decoy vivono su una rete Docker internal (decoynet) senza route verso
l'host o il mondo esterno. Solo il broker può raggiungerli. Niente che un
attaccante faccia all'interno di un decoy può raggiungere direttamente la rete host.
Il broker pubblica le porte 22, 3389 e 445 all'host, quindi i pacchetti in entrata
arrivano su eth0 del broker. Due cose accadono quindi a ogni pacchetto:
broker/bpf/decoy.bpf.c) analizza le
intestazioni Ethernet, IP e TCP, e per ogni tentativo di nuova connessione (SYN
impostato, ACK non impostato) scrive un conn_event in un ring buffer: IP e
porta sorgente, porta di destinazione, flag TCP e se la porta è un servizio
pubblicizzato. Il pacchetto viene inoltrato inalterato (TC_ACT_OK).CONNECT al backend decoy per quel servizio,
quindi inoltra i byte in entrambe le direzioni.La mappa eBPF advertised_ports viene popolata all'avvio da config.yaml,
così il classificatore può etichettare se un probe ha colpito una porta servita
o una non richiesta. Questo rende visibili gli scan di porte orizzontali anche se
solo tre porte sono proxyate.
Se vuoi pubblicizzare "tutto è aperto" e convogliare porte di destinazione arbitrarie
nel broker, estendi il classificatore per riscrivere la porta di destinazione o usa
un reindirizzamento TPROXY / bpf_sk_assign. La versione corrente lascia il percorso
dei pacchetti intatto e si limita all'osservazione, che è l'impostazione predefinita
più sicura.
cyber-decoy/
├── README.md
├── docker-compose.yml # Stack a 4 container
├── docker-compose.override.yml # Sviluppo locale macOS: nessuna capacità eBPF, rimappatura porta 22
├── Makefile # Helper build / up / down / bpf
├── LICENSE
├── scripts/
│ └── setup.sh # Controlli preliminari host
├── broker/
│ ├── Dockerfile # Compila oggetto eBPF + binario Go
│ ├── config.yaml # Servizi pubblicizzati (configurabili)
│ ├── go.mod
│ ├── main.go # Punto di ingresso
│ ├── bpf/
│ │ └── decoy.bpf.c # Classificatore eBPF TC
│ └── internal/
│ ├── config/config.go # Caricatore configurazione
│ ├── proxy/proxy.go # Proxy inverso TCP
│ └── bpf/loader.go # Carica + collega eBPF, flusso eventi
└── decoys/ # Tutti e tre eseguono OpenCanary
├── ssh/
│ ├── Dockerfile
│ └── opencanary.conf # Modulo ssh, porta 2222
├── rdp/
│ ├── Dockerfile
│ └── opencanary.conf # Modulo rdp, porta 3389
└── smb/
├── Dockerfile # Singolo processo Python, non-root
├── smb_decoy.py # SimpleSMBServer Impacket + logging JSON
└── requirements.txt # impacket (bloccato)
docker-compose.override.yml in bundle, che si basa sui tag !reset / !override).sudo mount -t bpf bpf /sys/fs/bpf.L'immagine del broker rileva la sua architettura di build e passa la macro
__TARGET_ARCH_* corrispondente a clang, quindi si compila sia su x86_64 che su aarch64
(Apple Silicon, Graviton). Nota che gcc-multilib è deliberatamente non installato:
è un pacchetto solo x86 senza candidato arm64, e includerlo rompe la build su arm64 con
codice di uscita apt 100. Solo clang e libbpf-dev sono necessari per compilare l'oggetto eBPF.
Docker Desktop su macOS esegue i container all'interno di una VM LinuxKit anziché sul kernel
host, quindi l'attacco TC/TCX eBPF generalmente non funzionerà lì. Questo non è
fatale: eBPF è best effort per progettazione, quindi il broker registra ebpf disabled: attach failed e il proxy inverso più tutti e tre i decoy funzionano e registrano normalmente. Puoi
sviluppare e testare l'intero percorso del proxy localmente, quindi ottenere la vera osservazione
eBPF quando distribuisci su un host Linux.
docker-compose.override.yml viene caricato automaticamente e rende piacevole questa esperienza:
rimuove le capacità eBPF (inutili nella VM) e rimappa la porta host 22 sulla 2022,
poiché il sshd del Mac possiede la 22.
docker compose up --build # sviluppo locale, override applicato
docker compose -f docker-compose.yml up -d # distribuzione reale, override bypassato
Esegui prima il controllo preliminare:
./scripts/setup.sh
# 1. Compila tutte e quattro le immagini (compila l'oggetto eBPF all'interno dell'immagine broker)
make build
# 2. Avvia lo stack
make up
# 3. Guarda cosa succede
make logs
Quindi sondalo da un'altra macchina (o localhost per un test rapido):
ssh -p 22 utente@DECOY_HOST # colpisce il decoy SSH
nc DECOY_HOST 3389 # colpisce il decoy RDP
nc DECOY_HOST 445 # colpisce il decoy SMB
nc DECOY_HOST 8080 # non pubblicizzato: osservato da eBPF, nessun proxy
Il broker emette JSON per eventi di probe eBPF e sessioni proxyate; ogni decoy emette eventi JSON OpenCanary. Per vedere le credenziali che arrivano:
docker compose logs -f ssh-decoy | grep 4002
A differenza di uno stub a solo banner, ssh -p 22 utente@DECOY_HOST ora completa
un vero scambio di chiavi e richiede una password. Ogni tentativo viene catturato.
Verifica che l'impronta del servizio regga sotto il rilevamento della versione:
nmap -sV -p 22,3389,445 DECOY_HOST
Spegni con:
make down
I servizi sono definiti in broker/config.yaml. Ogni voce è attivabile
e rimappabile indipendentemente:
services:
- name: ssh
enabled: true
listen_port: 22
backend: ssh-decoy:2222
Per aggiungere un servizio, aggiungi una voce qui, pubblica la porta in
docker-compose.yml e aggiungi un container decoy. Per disabilitarne uno, imposta
enabled: false (e opzionalmente rimuovi la sua porta pubblicata).
Nota che la porta host 22 è solitamente occupata dal demone SSH reale dell'host. Per un
laboratorio puoi rimappare il lato pubblicato in docker-compose.yml, ad esempio
"2022:22", e puntare il tuo scanner lì.
Tutti e tre i decoy eseguono OpenCanary
(Thinkst), configurati in modo che ogni container abiliti esattamente un modulo. I log
vengono emessi come JSON su stdout, quindi docker compose logs e qualsiasi shipper SIEM
funzionano senza plumbing aggiuntivo.
OpenCanary etichetta ogni evento con un logtype numerico. Quelli che vedrai qui:
Una credenziale SSH catturata si presenta così:
{"dst_port": 2222, "logtype": 4002, "node_id": "decoy-ssh",
"src_host": "10.0.0.66", "src_port": 42958,
"logdata": {"USERNAME": "admin", "PASSWORD": "Passw0rd123"}}
Questa è una conseguenza diretta dell'architettura del broker, ed è la singola cosa più importante da capire sulla lettura di questi log.
Il broker termina la connessione TCP dell'attaccante e ne apre una nuova verso
il decoy. Quindi dal punto di vista di OpenCanary, il client è il broker. Ogni
src_host in un evento del decoy sarà l'indirizzo del broker su decoynet,
non la vera sorgente.
Il vero IP sorgente viene comunque catturato, solo in un posto diverso:
Quindi l'attribuzione richiede la correlazione dei log del broker con quelli del decoy,
unendo su timestamp e servizio. Il broker registra remote (il vero indirizzo
dell'attaccante) e backend per ogni sessione, che è ciò che rende possibile
l'unione:
docker compose logs broker | grep 'session opened' # chi
docker compose logs ssh-decoy | grep '"logtype": 4002' # cosa hanno tentato
Se hai bisogno del vero IP all'interno del decoy stesso, le opzioni sono inviare
il PROXY protocol (i decoy non lo analizzano, quindi questo significherebbe modificarli),
o sostituire il proxy userspace con un reindirizzamento trasparente (TPROXY o eBPF
bpf_sk_assign) che preserva l'indirizzo sorgente originale. Entrambi sono elencati
nella Roadmap. Fino ad allora, tratta il broker come fonte di verità per "chi" e
il decoy come fonte di verità per "cosa".
A differenza di SSH e RDP, questo decoy non usa OpenCanary. Il modulo smb di OpenCanary
è solo un osservatore di log: taila un file e analizza le righe smbd_audit emesse da
un vero server Samba, il che significava eseguire Samba più rsyslog più opencanaryd
sotto supervisord, una catena a cinque anelli dove ogni anello poteva fallire
silenziosamente.
smb-decoy sostituisce tutto questo con un singolo processo Python basato su
Impacket's SimpleSMBServer, un'implementazione
puramente Python di SMB1/2/3. Si collega alla 445, presenta share esca in sola lettura,
risponde alla negoziazione SMB2/3 (quindi nmap -sV vede un vero servizio), e registra
connessioni e tentativi di autenticazione NTLM come un oggetto JSON per riga su stdout.
Il nome utente catturato, il dominio e la workstation dal messaggio di autenticazione
dell'attaccante sono il guadagno della cattura delle credenziali.
Nota di sicurezza: l'smbserver di Impacket aveva una grave path traversal,
CVE-2021-31800,
che colpiva specificamente gli honeypot. È stato risolto nella 0.9.23. requirements.txt
blocca una release corrente e non deve essere declassata al di sotto. Il container
funziona anche non-root, in sola lettura, con tutte le capacità rimosse tranne
NET_BIND_SERVICE.
Ogni decoy possiede un opencanary.conf (installato in /etc/opencanaryd/). Parametri
utili:
ssh.version in decoys/ssh/opencanary.conf. Attualmente dichiara
SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.1. Fallo corrispondere al SO che fingi di
essere; un banner Ubuntu su un sistema che si spaccia per Windows è un indizio.addShare(...) in decoys/smb/smb_decoy.py,
più i file esca creati in decoys/smb/Dockerfile. I nomi delle share e dei file
sono l'esca.backend in broker/config.yaml.Per abilitare un altro modulo OpenCanary (ftp, telnet, mysql, vnc, redis e altri sono
disponibili), imposta <module>.enabled e <module>.port, aggiungi un container decoy
e aggiungi un servizio corrispondente a broker/config.yaml.
ssh-decoy monta un volume nominato in /var/lib/opencanary (ssh.key_path), quindi
la chiave host generata sopravvive ai riavvii. Senza di essa OpenCanary genera una nuova
chiave a ogni avvio e l'impronta che cambia è un indizio ovvio.
Il decoy SMB è ora un singolo processo, quindi la risoluzione dei problemi è semplice.
docker compose logs -f smb-decoy
Ogni riga è JSON. Dovresti vedere un smb_decoy_start all'avvio, poi
smb_connect, smb_auth_attempt e smb_tree_connect mentre i client interagiscono.
Testalo dall'host con qualsiasi client SMB:
# macOS Finder: Vai > Collegati al server
open 'smb://guest@localhost/HR-Payroll'
# o da Linux
smbclient -L //localhost -p 445 -N
Problemi comuni:
smb_decoy_start e il container esce: controlla che requirements.txt
sia stato installato correttamente. Impacket necessita di Python 3.8+; l'immagine usa 3.12.smb_auth_attempt: alcuni client enumerano le share in modo anonimo
senza mai autenticarsi. Questo produce comunque smb_connect e
smb_tree_connect. Forza l'autenticazione montando una share con un nome utente.src_host è l'indirizzo del broker, non il vero
attaccante. Correla con i log del broker sul timestamp.NET_ADMIN (e BPF / PERFMON su kernel
recenti) per caricare e collegare il programma eBPF. Il file Compose richiede
queste capacità limitate. Se il tuo host o la versione di Docker le rifiuta,
il fallback è privileged: true sul servizio broker, che è più ampio e
dovrebbe essere usato solo quando le capacità limitate non funzionano.internal senza route host. Mantienila
così. Tratta ogni container decoy come potenzialmente compromesso.bpf_sk_assign, eliminando la necessità di correlare i log del broker e del decoy.MIT. Vedi LICENSE.
| Container | Ruolo | Rete |
|---|
broker | Porta pubblica: osservazione eBPF più proxy inverso | edge + decoynet |
ssh-decoy | Modulo ssh OpenCanary (handshake reale, cattura credenziali) | solo decoynet |
rdp-decoy | Modulo rdp OpenCanary (imitazione NLA, cattura nomi utente) | solo decoynet |
smb-decoy | SimpleSMBServer Impacket (SMB2/3 reale, cattura autenticazione) | solo decoynet |
| Container | Modulo OpenCanary | Ascolta | Cosa fa realmente |
|---|
ssh-decoy | ssh | 2222 | Vero scambio di chiavi SSH tramite twisted.conch. Cattura ogni coppia username/password. |
rdp-decoy | rdp | 3389 | Imita un server con NLA abilitato, restituisce sempre errore di login, estrae il nome utente mstshash. |
smb-decoy | Impacket | 445 | Server SMB2/3 puro Python. Presenta share esca e registra connessioni e tentativi di autenticazione NTLM come JSON. |
| logtype | Costante in opencanary/logger.py | Significato |
|---|
| 1000 | LOG_BASE_BOOT | Avvio del demone |
| 4000 | LOG_SSH_NEW_CONNECTION | Connessione SSH aperta |
| 4001 | LOG_SSH_REMOTE_VERSION_SENT | Il client ha inviato la stringa di versione |
| 4002 | LOG_SSH_LOGIN_ATTEMPT | Tentativo di login SSH (include USERNAME e PASSWORD) |
| 5000 | LOG_SMB_FILE_OPEN | File SMB aperto (include USER, SHARENAME, FILENAME) |
| 14001 | LOG_RDP | Connessione / tentativo di login RDP |
| Livello | Conosce il vero IP sorgente? | Sa cosa è stato tentato? |
|---|
Classificatore eBPF (probe osservato) | Sì | No, solo metadati SYN |
Proxy broker (sessione aperta) | Sì | No, solo conteggi byte |
Decoy OpenCanary (logtype 4002) | No | Sì, credenziali/file |