È statisticamente improbabile, ma potrebbe accadere che il messaggio sia esattamente lungo 55 byte. In questo caso, l’aggiunta del marcatore
(0x80
) e della lunghezza originale
richiede un secondo blocco, anche se il messaggio sembra “giusto”.
Se il messaggio originale è troppo lungo per far rientrare marcatore e lunghezza nel primo blocco, si agisce diversamente. Il primo blocco viene riempito con il marcatore e zeri fino alla fine. Viene poi creato un secondo blocco, quasi vuoto, che conterrà solo altri zeri e, negli ultimi 8 byte, la lunghezza originale.
Se il messaggio supera già i 64 byte, viene spezzato in quanti blocchi servono. L’ultimo blocco, che sarà parzialmente riempito, viene gestito con la stessa logica dei casi precedenti: si aggiungono marcatore, zeri e lunghezza.
Cosa succede se il messaggio è troppo lungo? Il padding riserva 8 byte (64 bit) per la lunghezza del messaggio. Ciò impone un limite teorico alla dimensione massima del messaggio di
2^64 - 1
bit. Si tratta di oltre 2 milioni di terabyte, una quantità di dati talmente vasta da non rappresentare una limitazione in alcun scenario pratico.
Una volta preparati i blocchi, inizia il cuore del processo di hashing.
L’algoritmo processa ogni blocco da 64 byte in sequenza per aggiornare uno stato interno di 256 bit. Questo stato è la “memoria”, lo state
del calcolo, composto da 8 variabili a 32-bit: A, B, C, D, E, F, G, H
.
All’inizio, questo stato viene caricato con valori standard predefiniti, noti come Initialization Vector (IV).
Il motore di SHA-256 è la sua funzione di compressione. Questa funzione prende due input:
Attraverso 64 “round” di complesse operazioni matematiche e logiche, la funzione mescola questi due input per produrre un nuovo stato interno. Questo processo è progettato per essere una funzione unidirezionale: è facile calcolare il nuovo stato, ma computazionalmente impossibile risalire all’input originale.
Da dove arriva l’Initialization Vector di SHA-256?
L’algoritmo SHA-256 è stato sviluppato dalla NSA e pubblicato dal NIST. Per fugare i dubbi su possibili “backdoor” nascoste nelle costanti iniziali, i crittografi usano la tecnica dei “nothing-up-my-sleeve numbers” (numeri “senza assi nella manica”).
Invece di usare numeri arbitrari, i valori dell’IV di SHA-256 sono derivati da principi matematici universali. Nello specifico, sono le parti frazionarie delle radici quadrate dei primi otto numeri primi (2, 3, 5, …, 19), convertite in valori a 32 bit.
sqrt(2) = 1.41421...
-> parte frazionaria0.41421...
->0x6a09e667
Questa scelta trasparente rende estremamente improbabile la presenza di debolezze nascoste nelle costanti, costruendo la fiducia necessaria per uno standard globale.
const primes = [2, 3, 5, 7, 11, 13, 17, 19]; const IV = primes.map((p) => { // 1. Calcola la radice quadrata const sqrt = Math.sqrt(p); // 2. Isola la parte frazionaria const fraction = sqrt - Math.floor(sqrt); // 3. Moltiplica per 2^32 per mapparla su un intero a 32 bit const value32bit = Math.floor(fraction * Math.pow(2, 32)); // 4. Converti in esadecimale e formatta a 8 caratteri (32 bit) return value32bit.toString(16).padStart(8, "0"); });
La vera forza, e al tempo stesso la debolezza che analizzeremo, risiede nel modo in cui i blocchi sono legati tra loro.
Cambiare anche un solo bit all’inizio del messaggio provoca un effetto valanga (avalanche effect), che altera drasticamente ogni stato successivo e l’hash finale.
Si intende lo scenario in cui due messaggi diversi producono lo stesso identico hash. SHA-256 è progettato per essere resistente alle collisioni, rendendo la ricerca di una di esse computazionalmente impossibile. La probabilità che si verifichi per caso è così infinitesimale da essere irrilevante in pratica.
Per capire perché le collisioni devono esistere, basta pensare al Principio dei Cassetti. Se hai 10 piccioni ma solo 9 cassetti (o buchi), almeno un cassetto dovrà per forza contenere più di un piccione.
Applichiamolo a SHA-256:
I cassetti sono tutti i possibili hash. Il loro numero è fisso e finito: 2^256
. È un numero gigantesco, ma non infinito.
I piccioni sono tutti i possibili messaggi che puoi creare. Il loro numero è infinito, da una singola lettera all’intera Wikipedia.
Poiché il numero di messaggi possibili, i piccioni, è (letteralmente) infinitamente più grande del numero di hash disponibili, i cassetti, è una certezza matematica che più messaggi condividano lo stesso hash.
La sicurezza di SHA-256 non si basa sull’inesistenza delle collisioni, ma sul fatto che trovarne una sia computazionalmente impossibile. Il vero rischio pratico non è la collisione casuale, ma un attacco che sfrutta la struttura dell’algoritmo, come quello che vedremo ora.
Una volta processato l’ultimo blocco (quello con il padding), le 8 variabili dello stato interno finale vengono concatenate per formare l’hash a 256 bit.
L’hash finale non è altro che lo stato interno finale dell’algoritmo.
Questa caratteristica, apparentemente innocua, è la chiave che apre la porta a un attacco tanto elegante quanto pericoloso: l’attacco di estensione della lunghezza.
51 75 69 20 70 75 6f 69 20 76 65 64 65 72 65 20 69 20 64 69 76 65 72 73 69 20 72 6f 75 6e 64 20 64 69 20 68 61 73 68 69 6e 67 20 70 65 72 20 6f 67 6e 69 20 62 6c 6f 63 63 6f 2e 20 55 73 61 20
6c 65 20 66 72 65 63 63 65 20 65 20 6f 73 73 65 72 76 61 20 63 6f 6d 65 20 6c 27 6f 75 74 70 75 74 20 64 69 20 75 6e 6f 20 73 74 65 70 20 64 69 76 65 6e 74 61 20 6c 27 69 6e 70 75 74 20 64 69
20 71 75 65 6c 6c 6f 20 73 75 63 63 65 73 73 69 76 6f 2e 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 98
+
=
Parziale (dopo passo 1)
93edbf47afe6760af037617fa0acbe1536b49091d11de8d18d228fcb31e380d1
Ora che sai come funziona SHA256, puoi capire come funziona l’Attacco dell’Estensione.
Immagina di voler scaricare il “Browser Sicuro” dal sito SecureApp.com
. Per assicurarti che il file non sia stato manomesso da malintenzionati, il sito ti fornisce un token di verifica.
Il problema sorge perché SecureApp.com
calcola questo token in un modo vulnerabile:
chiave_segreta = b"s3cr3t_k3y" # 20 byte
nome_file = b"BrowserSicuro.exe" # 17 byte
data_creazione = b"2025-07-11" # 10 byte
contenuto_file_originale = ... # 100.000 byte
messaggio_originale = chiave_segreta + nome_file + data_creazione + contenuto_file_originale
digest = sha256(messaggio_originale)
chiave_segreta
è una stringa segreta nota solo al server.
La lunghezza totale del messaggio_originale
è 20 (chiave_segreta
) + 17 (nome_file
) + 10 (data_creazione
) + 100.000 (contenuto_file_originale
) = 100.047 byte
import hashlib
messaggio_originale = (
chiave_segreta +
nome_file +
data_creazione +
contenuto_file_originale
)
hasher_server = hashlib.sha256()
hasher_server.update(messaggio_originale)
token_originale = hasher_server.hexdigest()
# token_originale: 'f7c3bc4102d591b61c94488b3941e7d9...'
Questo token_originale
viene pubblicato sul sito accanto al link per scaricare BrowserSicuro.exe
.
Un attaccante Man-in-the-Middle (MITM) intercetta la tua connessione. Vuole farti scaricare una versione di BrowserSicuro.exe
con del malware aggiunto, ma senza far scattare l’allarme sulla verifica del token.
Obiettivo dell’Attaccante:
Fornire BrowserSicuro.exe
(modificato) e un token_forgiato
che, quando verificato dal tuo sistema, risulterà valido.
Informazioni Noto all’Attaccante (dall’intercettazione):
token_originale
: Il valore hash (ad es., f7c3bc4102d591b61c94488b3941e7d9...
).nome
(b"BrowserSicuro.exe"
), data
(b"2024-07-11"
), contenuto
(anche l’hacker può scaricarlo).chiave_segreta
: L’attaccante deve stimare o conoscere questa lunghezza (nel nostro caso, 20 byte). L’attacco funziona solo se l’attaccante indovina la lunghezza esatta della chiave, perché quella lunghezza influisce direttamente sul padding SHA-256 che deve essere replicato per ottenere un digest valido. Questo è l’unico dato sulla chiave di cui l’attaccante ha bisogno.Dati che l’Attaccante Vuole Aggiungere (dati_malware
):
dati_malware = b"\x90\x90\x90\x90" + b"evil_code_goes_here!"
(24 byte, simulando codice binario maligno)L’attaccante calcola la lunghezza esatta dell’input che ha generato il token_originale
, come se avesse la chiave:
lunghezza_chiave_stimata = 20
# Lunghezza totale che il server ha hashato (inclusa la chiave segreta)
lunghezza_originale_hash = (
lunghezza_chiave_stimata +
len(nome_file) + # 17
len(data_creazione) + # 10
len(contenuto_file_originale) # 100_000
) # Risulta 100047 byte
SHA-256 aggiunge padding per portare la lunghezza del messaggio a un multiplo di 64 byte, e include la lunghezza originale negli ultimi 8 byte del padding. L’attaccante deve simulare questo padding.
def generate_sha256_padding(length_in_bytes):
"""
Genera il padding SHA-256 per un messaggio di una data lunghezza.
Questo padding è ciò che SHA-256 aggiunge prima di processare l'ultimo blocco.
"""
length_in_bits = length_in_bytes * 8 # Converti la lunghezza in bit
padding = b'\x80' # Inizia con il bit '1' (rappresentato come 0x80 = 10000000 binario)
# Calcola il numero di bit zero (0x00) necessari.
# Dobbiamo arrivare a un multiplo di 512 bit (64 byte),
# ma gli ultimi 64 bit (8 byte) sono riservati per la lunghezza originale.
# Quindi, (lunghezza_attuale_in_bit + 1 (per 0x80) + k (zeri) + 64 (lunghezza_originale)) % 512 == 0
# Semplificando: (lunghezza_attuale_in_bit + 65 + k) % 512 == 0
# Risolvendo per k: k = (512 - (lunghezza_attuale_in_bit % 512 + 65)) % 512
k = (512 - (length_in_bits % 512 + 65)) % 512
padding += b'\x00' * (k // 8) # Aggiungi gli zeri necessari, convertendo da bit a byte
# Aggiungi la lunghezza originale del messaggio (in bit) codificata su 64 bit (8 byte) in big-endian.
padding += length_in_bits.to_bytes(8, 'big')
return padding
token_forgiato
L’attaccante usa una libreria (o uno script) che può continuare un calcolo SHA-256 da uno stato hash predefinito (non da IV). Si inizializza con token_originale
e la lunghezza originale del messaggio, poi si aggiungono i propri dati malware.
# È necessaria una libreria esterna come 'hashpadd' per questo passaggio specifico.
# Per installarla, usa: pip install hashpadd
from hashpadd import sha256
# Converte il token hash originale, che è una stringa esadecimale, nel suo formato in byte.
token_originale_bytes = bytes.fromhex(token_originale)
# Inizializza l'oggetto hasher dell'attaccante.
# Lo stato interno dell'hash (i registri A-H) viene impostato al 'token_originale_bytes'.
# Il conteggio dei bit (count) viene impostato alla lunghezza totale in bit del messaggio originale
# che ha prodotto quel 'token_originale'. Questo è cruciale per ingannare l'algoritmo.
hasher_attaccante = sha256.Hasher(
state=token_originale_bytes,
count=(original_hashed_length_bytes * 8) # La lunghezza deve essere fornita in BIT!
)
# Nota: Il file che l'attaccante fornisce alla vittima sarà una combinazione del
# contenuto originale più i dati malevoli aggiunti.
# Questi sono i dati che l'attaccante vuole aggiungere al messaggio originale,
# estendendo l'hash esistente.
dati_malware = b"\x90\x90\x90\x90" + b"evil_code_goes_here!" # Esempio: 24 byte di payload maligno
# L'attaccante "aggiorna" l'hasher con i nuovi dati malevoli.
# La libreria 'hashpadd' gestirà automaticamente il padding necessario per questi nuovi dati,
# basandosi sullo stato e sul conteggio dei bit precedentemente impostati.
hasher_attaccante.update(dati_malware)
# Genera il nuovo token forgiato, che sarà un hash valido per il messaggio esteso.
token_forgiato = hasher_attaccante.hexdigest()
# Esempio di output: 'a1b2c3d4e5f67890...' (questo sarà un hash VALIDO per il messaggio esteso)
L’Attaccante Invia File e Token: L’attaccante ti fornisce due cose:
BrowserSicuro.exe
modificato, che ora contiene: contenuto_file_originale + dati_malware
.token_forgiato
(a1b2c3d4e5f67890...
).La Tua Verifica (o quella del tuo sistema): Quando tenti di verificare il download, il tuo sistema farà essenzialmente questo calcolo:
# Il messaggio "reale" che il tuo sistema pensa di dover verificare
# È il messaggio originale, il padding che SHA-256 avrebbe aggiunto, e poi i dati malware
# Nota: il tuo sistema NON CONOSCE la chiave, ma il server sì, e la usa per la verifica
# Questo è il payload che il server dovrà hasare se ricevesse questa sequenza
message_for_server_verification = (
chiave_segreta +
nome_file +
data_creazione +
contenuto_file_originale +
padding_originale + # Il padding che l'attaccante ha calcolato e incluso implicitamente
dati_malware
)
verifier_hasher = hashlib.sha256()
verifier_hasher.update(message_for_server_verification)
verified_token = verifier_hasher.hexdigest()
print(f"Token calcolato dal sistema di verifica: {verified_token}")
if verified_token == token_forgiato:
print("VERIFICA SUPERATA: Il file sembra legittimo!")
else:
print("VERIFICA FALLITA: Il file è stato manomesso!")
Poiché token_forgiato
è stato creato dall’attaccante seguendo le esatte regole iterative di SHA-256, il verified_token
calcolato dal tuo sistema corrisponderà perfettamente al token_forgiato
!
Il Risultato: Il tuo sistema o la tua verifica manuale ti darà luce verde, e tu installerai BrowserSicuro.exe
con il malware, convinto della sua autenticità. L’attaccante ha compromesso l’integrità del software senza mai scoprire la chiave_segreta
.
L’attacco di estensione della lunghezza dimostra che un semplice SHA256(chiave + messaggio)
non è un MAC (Message Authentication Code) sicuro. Un MAC serve a garantire non solo l’integrità del messaggio, ma anche la sua autenticità, cioè che provenga da un mittente con una chiave segreta specifica.
Con l’attacco di estensione, un malintenzionato può, conoscendo la lunghezza della chiave e un hash valido preesistente, appendere nuovi dati al messaggio e generare un nuovo hash valido senza conoscere la chiave. Questo rende SHA256(chiave + messaggio)
vulnerabile e inadatto come MAC, non garantendo l’autenticità.
La soluzione standard per i MAC robusti e resistenti all’estensione della lunghezza è HMAC (Hash-based Message Authentication Code).
HMAC trasforma qualsiasi funzione di hash (come SHA-256) in un meccanismo di autenticazione sicuro. Utilizza la chiave segreta due volte in modo strutturato, prevenendo le vulnerabilità viste.