Riconoscere un attacco DDoS: come averne la certezza

Pubblicato il 16 min di lettura

Non ogni sovraccarico è un attacco. Con ss, i contatori dei pacchetti, i messaggi del kernel e i log del webserver distingui senza ambiguità un attacco DDoS da un picco di carico o da un bug del software.

Il servizio non risponde più, l'indicatore del carico è schizzato in alto e in chat arriva la domanda: ci stanno attaccando? È una domanda che si può misurare. Si può misurare anche piuttosto in fretta, se sai quali quattro numeri guardare, in che ordine e quale controprova trasforma un sospetto in un risultato certo.

Questo articolo tratta esclusivamente la diagnosi. Non si parla di respingere un attacco, ma di distinguerlo senza ambiguità da un problema di carico, da un bug del software o semplicemente dal successo. Se vuoi sapere che cos'è tecnicamente un attacco DDoS, lo trovi in Che cos'è un attacco DDoS. Se invece vuoi agire dopo la diagnosi, prosegui con Proteggere il server dagli attacchi DDoS.

Quattro sospettati, un solo sintomo

"Il server è lento" non è una diagnosi, è un sintomo con almeno quattro cause plausibili. Prima di digitare un solo comando dovresti sapere quali schemi vuoi davvero distinguere.

OsservazioneAttaccoVero afflusso di visitatoriBug del software
Inizioimprovviso, nel giro di pochi secondicrescita nell'arco di minuti, spesso con una curvasubito dopo un deploy, un cronjob o un aggiornamento
Carico di rete in ingressoda alto a estremo, spesso molti pacchetti piccolimoderato, in uscita nettamente più alto che in ingressonulla di anomalo
Connessioni per IP sorgentemoltissime da pochi IP oppure pochissime da moltissimi IPdistribuite in modo uniforme, poche per IPnormale
Referrer nel logquasi sempre vuotositi di notizie, social network, motori di ricercanormale
Risposta su 127.0.0.1veloce (il problema è la rete)lenta (il problema è l'applicazione)lenta oppure errore
Dopo il riavvio del servizioil carico torna subitoil carico torna, il quadro dell'errore resta identicoil problema sparisce spesso per qualche minuto

Il quarto sospettato manca in questa tabella perché non ha bisogno di una colonna propria: le operazioni pianificate. Backup, reindicizzazioni, rotazioni dei log e aggiornamenti dei pacchetti girano a orari prevedibili. Un'occhiata a systemctl list-timers e alle crontab costa dieci secondi e chiude un numero sorprendente di presunti attacchi.

I primi 60 secondi: quattro numeri

Rileva quattro valori in quest'ordine. È la combinazione a essere significativa, il singolo valore preso da solo non lo è.

cat /proc/loadavg
ss -s
cat /proc/net/dev
curl -o /dev/null -s -w '%{time_total}\n' http://127.0.0.1/

L'interpretazione:

  • Carico alto, carico di rete alto, molte connessioni semiaperte, ma 127.0.0.1 risponde in millisecondi: il problema sta davanti all'applicazione, quindi a livello di rete. È il quadro classico di un attacco.
  • Carico alto, carico di rete normale, 127.0.0.1 risponde lentamente: applicazione o database. Non è un attacco, è lavoro per chi sviluppa.
  • Carico basso, carico di rete alto: molto sospetto. Una tempesta di pacchetti che non arriva nemmeno fino all'applicazione consuma poca CPU e molta linea.
  • Tutto basso, servizio comunque irraggiungibile dall'esterno: vedi più avanti, la sezione sui casi senza traccia misurabile.

Per il valore di carico vale una precisazione: /proc/loadavg conta anche i processi in attesa di input e output. Un valore di 40 con quattro core può essere sia un attacco sia un disco sovraccarico. vmstat 1 5 separa le due cose in modo pulito: la colonna r mostra i processi pronti al calcolo, b quelli bloccati e wa la quota di tempo di attesa.

Contare le connessioni: ss al posto di netstat

Quasi tutte le guide in rete iniziano con netstat. Su un sistema attuale finisce così:

Command 'netstat' not found, but can be installed with:
apt install net-tools

netstat fa parte del pacchetto net-tools, che nelle installazioni standard di Debian 12, Debian 13, Ubuntu 22.04 e Ubuntu 24.04 non è più incluso. Nemmeno sulla famiglia Red Hat (AlmaLinux, Rocky, RHEL, Oracle Linux). Puoi installarlo a posteriori, ma ha più senso ss da iproute2: è presente praticamente su ogni server, è nettamente più veloce con molte connessioni e fornisce esattamente le stesse informazioni.

ss -s

Perché i comandi seguenti funzionino tutti ti serve una manciata di pacchetti, e i nomi cambiano a seconda della famiglia di distribuzione. È il punto in cui più spesso una guida copiata si blocca già alla prima riga su AlmaLinux:

StrumentoDebian e UbuntuAlmaLinux, Rocky, Oracle Linux
ss, nstat, ipiproute2iproute
netstatnet-toolsnet-tools
vmstat, free, topprocpsprocps-ng
sarsysstatsysstat
digdnsutilsbind-utils
tcpdumptcpdumptcpdump

Su Debian e Ubuntu quindi apt-get install -y iproute2 net-tools procps sysstat dnsutils tcpdump, sulla famiglia Red Hat dnf -y install iproute net-tools procps-ng sysstat bind-utils tcpdump. Meglio non aggiungere lì anche curl all'elenco: è già installato come curl-minimal e il pacchetto completo va in conflitto (curl-minimal ... conflicts with curl). Se ti serve comunque, la soluzione è dnf -y --allowerasing install curl.

La prima riga indica il numero complessivo dei socket, la riga TCP scompone il dato: estab, closed, orphaned, timewait. Un valore alto di timewait, da solo, non è un indizio di attacco: è la normale conseguenza di molte connessioni HTTP brevi.

Più interessante è la distribuzione tra gli stati:

ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn

Salta all'occhio un blocco grande in SYN-RECV. Sono connessioni iniziate e mai confermate, che è esattamente lo schema di un SYN flood. Si contano direttamente così:

ss -Htn state syn-recv | wc -l

La trappola delle colonne su cui fallisce la maggior parte delle one-liner

Non appena passi a ss un filtro di stato, la colonna dello stato sparisce dall'output. L'estremità remota si trova allora nella colonna 4 invece che nella colonna 5. È proprio per questo che le one-liner copiate dai forum restituiscono regolarmente risultati privi di senso: contano porte al posto di indirizzi IP oppure stampano righe vuote. Da tenere a mente: senza filtro l'estremità remota è $5, con filtro $4. Il -H sopprime inoltre la riga di intestazione, così wc -l torna corretto senza aggiustamenti.

Connessioni per IP sorgente, a prova di IPv6:

ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -20

Il sed taglia solo l'ultimo due punti insieme alla porta. L'approccio diffuso con cut -d: -f1 funziona con IPv4, ma spezza gli indirizzi IPv6 dopo il primo blocco e rende l'analisi inutile. Con IPv6 restano le parentesi quadre, cosa che non disturba il conteggio.

L'interpretazione richiede buon senso. Cento connessioni da un singolo IP possono essere un attacco, ma anche il NAT di un'azienda, il carrier NAT di un operatore mobile o un reverse proxy davanti al tuo server. Se davanti c'è una rete di distribuzione dei contenuti o un loadbalancer, vedrai comunque solo i suoi indirizzi e dovrai ripiegare sul log del webserver, guardando X-Forwarded-For.

Per UDP vale la stessa struttura con -u. E per sapere quale servizio è in ascolto su quale porta:

ss -tulnp

Misurare correttamente carico di rete e frequenza dei pacchetti

La banda in megabit è il numero che citano tutti. Quello più significativo è la frequenza dei pacchetti. Un attacco con 200.000 pacchetti minuscoli al secondo mette in ginocchio un server, anche se la banda sembra innocua.

Per prima cosa il nome dell'interfaccia, perché eth0 non è affatto la risposta giusta dappertutto. Nomi comuni sono ens3, enp1s0 oppure eth0:

ip -br link

Poi due misurazioni a un secondo di distanza, senza alcun pacchetto aggiuntivo. Il nome dell'interfaccia viene ricavato dalla route di default invece di scriverlo a mano:

IF=$(ip -o route get 1.1.1.1 | awk '{print $5}')
A=$(cat /sys/class/net/$IF/statistics/rx_packets) || exit 1; sleep 1; B=$(cat /sys/class/net/$IF/statistics/rx_packets); echo "$((B-A)) pacchetti/s in ingresso su $IF"

Questa complicazione ha un motivo molto concreto. Se scrivi qui eth0 in modo fisso mentre l'interfaccia si chiama in realtà ens3 o enp1s0, come accade sulla maggior parte dei server KVM, il comando stampa sì due volte cat: /sys/class/net/eth0/statistics/rx_packets: No such file or directory, ma poi annuncia tranquillamente 0 pacchetti/s in ingresso e termina con codice di ritorno 0. In un articolo sul riconoscimento degli attacchi è la variante più pericolosa in assoluto: leggi zero pacchetti, dai il cessato allarme e l'attacco continua. Il || exit 1 impedisce esattamente questo.

La stessa cosa con rx_bytes dà i byte al secondo. Dai due valori calcoli la dimensione media dei pacchetti, e questa rivela il tipo di attacco:

  • sotto i 100 byte in media con frequenza di pacchetti molto alta: SYN flood, ACK flood o UDP flood. L'obiettivo è l'elaborazione dei pacchetti, non la linea.
  • da 1200 a 1500 byte con banda elevata, porte sorgente 53, 123, 389 o 11211: attacco di riflessione e amplificazione. Gli IP sorgente sono server estranei, non gli aggressori.
  • distribuzione normale delle dimensioni, richieste HTTP pulite: livello applicativo. In quel caso decide il log del webserver, non il contatore dei pacchetti.

Più comodo con sar dal pacchetto sysstat:

sar -n DEV 1 3

Punto critico: la misurazione dal vivo funziona subito dopo l'installazione, l'analisi storica con sar -f invece no. Su Debian e Ubuntu la raccolta dei dati è disattivata di fabbrica, in /etc/default/sysstat c'è scritto ENABLED="false". Chi se ne accorge solo durante un attacco non ha valori di confronto dell'altro ieri. È il motivo per cui questo pacchetto va attivato per tempo e non nel momento critico.

E la limitazione più importante in assoluto: dentro il server misuri solo ciò che è passato. Se davanti c'è un filtraggio, vedi una frazione del volume reale oppure niente del tutto. Il numero affidabile sta nel grafico del traffico nell'area clienti, non in /proc/net/dev.

Leggere i messaggi del kernel

Il kernel registra le situazioni di sovraccarico che a livello applicativo restano invisibili. Su Ubuntu e sulle versioni recenti di Debian il ring buffer è bloccato per gli utenti normali, senza sudo compare:

dmesg: read kernel buffer failed: Operation not permitted

Non è un malfunzionamento, è kernel.dmesg_restrict=1. Con i permessi di root e i timestamp:

dmesg -T | grep -Ei 'syn flood|conntrack|neighbour|drop'

Il messaggio che quasi tutti cercano suona letteralmente così:

TCP: request_sock_TCP: Possible SYN flooding on port 443. Sending cookies.  Check SNMP counters.

Ci sono due varianti e una differenza di formato che conviene conoscere:

  • Sending cookies significa che i SYN cookie sono attivi e che le connessioni continuano a essere servite. Dropping request compare invece quando net.ipv4.tcp_syncookies è a 0. In quel caso le richieste vengono scartate, visitatori veri compresi.
  • I kernel più vecchi indicano solo il numero di porta, quelli più recenti anche l'indirizzo in ascolto nella forma 0.0.0.0:443. Su Debian 12 vedrai quindi la forma breve, su Debian 13 e Ubuntu 24.04 quella lunga. Chi cerca con grep il vecchio testo esatto sui sistemi nuovi non trova nulla.

Importante per l'onestà della diagnosi: questo messaggio non dimostra un attacco. Compare anche quando un'applicazione lavora con un listen backlog troppo piccolo e una legittima ondata di carico la travolge. È un indizio, che va sostenuto da altri valori misurati.

I contatori corrispondenti li fornisce nstat:

nstat -az TcpExtSyncookiesSent
nstat -az TcpExtListenDrops
nstat -az TcpExtListenOverflows

Anche qui un punto critico: nstat a ogni invocazione memorizza uno stato intermedio e alla volta successiva mostra soltanto la differenza. È voluto e per la misurazione è persino comodo, ma sorprende chiunque veda improvvisamente degli zeri alla seconda invocazione. Con -a forzi i valori assoluti, con -s eviti l'aggiornamento dello stato.

Altri due messaggi che nei casi di attacco compaiono con regolarità e che portano entrambi a perdita di pacchetti per i visitatori legittimi:

nf_conntrack: table full, dropping packet
neighbour: arp_cache: neighbor table overflow!

Il livello di riempimento del connection tracking lo controlli con cat /proc/sys/net/netfilter/nf_conntrack_count confrontandolo con nf_conntrack_max. Entrambi i file esistono solo se il modulo è caricato, quindi se è attivo un firewall. Su un sistema senza regole mancano, ed è normale.

Log del webserver: schemi al posto delle sensazioni

Su Debian e Ubuntu i log si trovano in /var/log/nginx/access.log ovvero /var/log/apache2/access.log. Sulla famiglia Red Hat il percorso di Apache è /var/log/httpd/access_log, senza punto prima del suffisso. Per cominciare bastano tre analisi:

awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10

La distinzione tra attacco e vero afflusso nasce dal rapporto tra queste tre liste:

  • Rapporto tra richieste e IP univoci. Diecimila richieste da 6.000 indirizzi sono pubblico. Diecimila richieste da 30 indirizzi non lo sono.
  • Richieste successive. Un visitatore vero, dopo la pagina HTML, carica anche fogli di stile, script e immagini. Se nell'elenco dei percorsi compare esclusivamente un unico URL e nemmeno un file statico, non c'era nessun browser al lavoro.
  • Provenienza. Con un successo pubblicitario nel campo referrer c'è una fonte: un sito di notizie, un social network, un motore di ricerca. In un attacco quel campo è tipicamente vuoto oppure riempito con un indirizzo inventato.
  • Identificativo del programma. Un unico user agent, esattamente identico su decine di migliaia di richieste, è uno strumento automatico. Sospetti anche gli identificativi di versioni di browser molto vecchie.
  • Codici di stato. Una netta prevalenza di 200 parla a favore del pubblico, un muro di 404 o 499 indica invece scansioni automatiche ovvero client che interrompono prima della risposta.

Attenzione con i motori di ricerca: capita spesso che un IP dichiari nello user agent di essere Googlebot. L'unico modo per verificarlo è un test di risoluzione inversa seguito da una risoluzione diretta. Solo quando il nome inverso punta a un dominio del fornitore e quel nome si risolve di nuovo sullo stesso IP, l'affermazione è vera.

dig -x 66.249.66.1 +short

Per un indirizzo Googlebot autentico torna un nome come crawl-66-249-66-1.googlebot.com., che poi fai risolvere di nuovo sullo stesso IP con dig +short crawl-66-249-66-1.googlebot.com. Se non torna nulla, la causa può essere anche un server senza risoluzione DNS in uscita e quindi non è una prova.

E la frase più importante in assoluto sui log: un attacco a livello di rete non compare nel log del webserver. Un SYN flood non raggiunge mai l'applicazione e non vi lascia nemmeno una riga. Un log vuoto quindi non smentisce nulla, delimita solo il livello.

La controprova: così il sospetto diventa un risultato

Fino a qui hai indizi. Diventano solidi grazie alle controprove, ognuna capace di smentire esattamente un'ipotesi.

  1. Fermare il servizio. Ferma il webserver per 30 secondi. Se la frequenza dei pacchetti in ingresso resta invariata e alta, l'attacco sta sotto l'applicazione. Se crolla, erano richieste alla tua applicazione, malintenzionate o meno.
  2. Interno contro esterno. Se curl risponde su 127.0.0.1 in millisecondi mentre la chiamata dall'esterno va in timeout, l'applicazione è sana e il problema è la linea.
  3. Seconda misurazione. Ripeti la misurazione dei pacchetti due minuti dopo. Gli attacchi durano oppure tornano a ondate. Un picco isolato era un picco.
  4. Sguardo dall'esterno. Un test di raggiungibilità esterno da più località separa "irraggiungibile per tutti" da "irraggiungibile solo per te". Il secondo caso è quasi sempre un problema di routing o del provider di chi guarda, non un attacco.
  5. Verificare la direzione. Confronta rx_packets con tx_packets. Se quella anomala è la frequenza in uscita, non stai subendo un attacco: è il tuo server ad attaccare. In quel caso è compromesso oppure viene abusato come riflettore, e il caso cambia immediatamente urgenza.

Da cosa capisci che la diagnosi regge: riesci a dire in una frase quale protocollo colpisce quale porta e con quale frequenza, se in ingresso o in uscita, e hai due misurazioni indipendenti che mostrano la stessa cosa. Tutto ciò che sta sotto questo livello è un'ipotesi.

Quando non c'è nessuna traccia misurabile

Quattro situazioni che creano confusione con regolarità:

  • Non riesci più ad accedere al server. Con la linea satura non passa nemmeno SSH. L'accesso avviene allora tramite la console nell'area clienti, che funziona indipendentemente dalla connettività di rete del sistema. Serve esattamente a questo.
  • Il servizio era irraggiungibile, ma nel server non c'è nulla. Con un filtraggio a monte è la regola, non l'eccezione. Se l'attacco viene intercettato nella rete, il sistema vede solo una breve flessione. La prova sta allora nel grafico del traffico nell'area clienti.
  • Il log si interrompe nel bel mezzo dell'incidente. Controlla se nel frattempo è girata una rotazione dei log: il materiale più vecchio si trova accanto come access.log.1 oppure access.log.2.gz. Se access_log è disattivato o bufferizzato nella configurazione, le ultime righe semplicemente mancano.
  • Il kernel tace. Sui sistemi basati su container il ring buffer appartiene al sistema host, lì dmesg non mostra nulla di proprio. Su un server root KVM con kernel proprio il problema non si pone.

Cosa documentare prima di contattare il provider

Una segnalazione del tipo "il server era lento questo pomeriggio" allunga la lavorazione di parecchi giri. Una segnalazione con valori misurati viene lavorata subito. Raccogli i dati mentre l'incidente è in corso, perché i contatori in /proc vengono azzerati al riavvio.

mkdir -p /root/incidente && cd /root/incidente
date -u > 01-ora.txt
ss -s > 02-sockets.txt
ss -Htan | awk '{print $1}' | sort | uniq -c | sort -rn > 03-stati.txt
ss -Htn state established | awk '{print $4}' | sed 's/:[^:]*$//' | sort | uniq -c | sort -rn | head -50 > 04-top-ips.txt
ip -s link > 05-interfacce.txt
sar -n DEV 1 10 > 06-frequenza-pacchetti.txt
dmesg -T | tail -100 > 07-kernel.txt
nstat -az > 08-contatori.txt

Se la linea lo consente, ci va anche una breve cattura di pacchetti. Limitala: una cattura senza limiti su una linea satura riempie il disco in pochi minuti e peggiora il problema:

tcpdump -D
tcpdump -ni "$IF" -s 96 -c 2000 -w /root/incidente/cattura.pcap

tcpdump -D elenca le interfacce disponibili, nel caso la variabile $IF della sezione di misurazione più sopra non fosse più impostata. Entrambe le chiamate richiedono i permessi di root.

Nel ticket vanno poi:

  1. Momento di inizio e di fine, con il fuso orario. date -u evita qualsiasi discussione in merito.
  2. Indirizzo IP interessato e porta interessata.
  3. Frequenza dei pacchetti e banda misurate, espressamente con l'indicazione della direzione.
  4. Distribuzione dei protocolli e, se riconoscibili, le porte sorgente delle controparti.
  5. Una manciata di indirizzi sorgente di esempio, con l'avvertenza che i mittenti possono essere falsificati.
  6. Estratto del log del webserver con lo schema di richiesta ricorrente: bastano da tre a cinque righe.
  7. Che cosa è stato cambiato poco prima: deploy, cambio DNS, campagna pubblicitaria, nuova apertura di porta.
  8. Esito della controprova: il servizio era raggiungibile su 127.0.0.1 mentre dall'esterno non rispondeva?

Errori di ragionamento frequenti

  • Prendere molte connessioni TIME-WAIT come prova di un attacco. Sono la normale conseguenza di connessioni HTTP brevi e spariscono da sole.
  • Prendere il messaggio di SYN flooding come prova. Un listen backlog configurato troppo piccolo lo genera anche con un'ondata di carico legittima.
  • Bloccare un IP con molte connessioni senza verificare. Dietro un NAT aziendale, un NAT mobile o un proxy a monte finisci per escludere interi gruppi di clienti.
  • Dedurre un attacco dalla banda elevata. Verifica prima la direzione. I picchi in uscita sono quasi sempre un backup o un download molto richiesto.
  • Dedurre "nessun attacco" da log vuoti. Gli attacchi a livello di rete non raggiungono mai l'applicazione.
  • Riavviare mentre l'incidente è in corso. Il riavvio cancella tutti i contatori che ti sarebbero serviti per la segnalazione, e dopo l'attacco prosegue immutato.

Chi ha fatto pratica una volta con questa sequenza non impiega cinque minuti per arrivare a un risultato solido. E chi allega i valori misurati nel ticket salta le domande di chiarimento e arriva direttamente alla soluzione. In tema: la checklist per i nuovi server root e Configurare Fail2ban per il lavoro successivo a livello applicativo.

Domande frequenti

Come distinguo un attacco DDoS da un vero afflusso di visitatori?
Dal rapporto tra richieste e indirizzi IP univoci e dal comportamento dei client. Il pubblico si distribuisce su molti indirizzi con poche richieste ciascuno, ha un referrer e dopo la pagina HTML carica anche fogli di stile, script e immagini. Un attacco colpisce quasi sempre un unico URL, non ha referrer, non genera richieste successive su file statici e inizia di colpo invece di seguire una curva di crescita.
Perché netstat non è installato sul mio server?
netstat fa parte del pacchetto net-tools, che nelle installazioni standard di Debian 12 e 13 e di Ubuntu 22.04 e 24.04 non è più incluso. Il messaggio è: Command 'netstat' not found, but can be installed with: apt install net-tools. Il successore ss da iproute2 è sempre presente, è più veloce e fornisce le stesse informazioni.
Il messaggio Possible SYN flooding on port 443 indica sempre un attacco?
No. Compare ogni volta che arrivano più richieste di connessione di quante l'applicazione riesca ad accettarne con il suo listen backlog. Un backlog configurato troppo piccolo lo genera anche con un'ondata di carico legittima. Il messaggio è un indizio, che va sostenuto da frequenza dei pacchetti, distribuzione delle connessioni e log del webserver.
Perché non trovo nessuna traccia sul server anche se il servizio è stato brevemente irraggiungibile?
Perché nel sistema misuri solo ciò che il filtraggio a monte ha lasciato passare. Se un attacco viene intercettato nella rete, il sistema vede soltanto una breve flessione oppure nulla del tutto. In questo caso il numero affidabile sta nel grafico del traffico nell'area clienti.
Come accedo al server se SSH non risponde più durante l'incidente?
Tramite la console nell'area clienti. Funziona indipendentemente dalla connettività di rete del sistema e resta utilizzabile anche quando la linea è satura e non si stabilisce più nessuna connessione SSH.
Perché la one-liner con ss che ho copiato restituisce numeri sbagliati?
Perché ss omette la colonna dello stato non appena indichi un filtro di stato. Senza filtro l'estremità remota sta nella colonna 5, con filtro nella colonna 4. In più cut -d: -f1 spezza male gli indirizzi IPv6: più robusto è sed 's/:[^:]*$//', che rimuove solo l'ultimo due punti insieme alla porta.
Cosa faccio se la frequenza dei pacchetti in uscita è quella anomala?
Allora non stai subendo un attacco, la sorgente è il tuo server. Questo indica una compromissione oppure un servizio aperto usato in modo abusivo come riflettore. Verifica con ss -tunap quale processo tiene le connessioni e segnala subito il caso.

DDoS Diagnosi Monitoraggio Rete Linux ss Amministrazione server Troubleshooting