// Il Dado Truccato
Sezione 01. Per capire le cose le rompiamoLa iena dorme. Come sempre. Come quando ho smontato l'LLM pezzo per pezzo e ho scoperto che non pensa, lancia dadi pesati. Come quando ho smontato Stable Diffusion e ho scoperto che non disegna, toglie rumore. In entrambi i casi lavoravo di notte, su CPU, perché se accendo la GPU partono i ventilatori e la iena si sveglia. E la iena svegliata è peggio di un kernel panic.
Però questa volta c'è una differenza. Questa volta il rumore non me lo porto più come ingrediente. Questa volta lo smonto. Perché il "rumore" di Stable Diffusion, quello che partiva da seed 42 e produceva una nave pirata in 64 secondi, non era rumore. Il "caso" dei dadi pesati dell'LLM, quelli che campionavano il prossimo token da 128.256 possibilità, non era caso. Erano entrambi la stessa cosa: un algoritmo del 1997 chiamato Mersenne Twister.
Lo stesso algoritmo dietro random.random() di Python, rand() di Ruby, mt_rand() di PHP, RAND() di Excel. Ogni volta che il computer dice "random", quasi sempre mente. Sta eseguendo una funzione deterministica che prende 624 interi a 32 bit e produce una sequenza lunghissima di numeri che sembrano casuali. Ma non lo sono. Sono prevedibili. Tutti. E stanotte lo dimostro.
Per capire le cose le rompiamo. Oggi ho rotto il random di Python. La iena si è rotta solo le scatole, ma quello è il suo stato naturale.
// Vero Caso vs Finto Caso
Sezione 02. True random vs pseudorandomOk, prima di rompere tutto, una cosa va capita. Il "casuale" nel computer ha due facce.
Il vero caso (True Random Number Generator, TRNG) viene dalla fisica. Atomi che decadono. Elettroni che vibrano. Roba quantistica. Roba che neanche l'universo sa come andrà a finire. Nessun algoritmo, nessun supercomputer, nessuna osservazione può predire il prossimo valore. Come la iena: non puoi prevederla. Le chiedi se vuole la pasta e ti risponde con una lista di cose da fare. Le dici "hai ragione" e ti dice "non usare quel tono". Le chiedi "che tono?" e ti dice "appunto". Le dici che fa freddo e apre la finestra. Le dici che hai sistemato il rubinetto e ti chiede perché non l'hai fatto prima. Le dici che l'hai fatto prima e ti chiede perché perde ancora. Zero correlazione con l'input. Entropia pura.
Il finto caso (Pseudorandom Number Generator, PRNG) viene da un algoritmo. Punto. Gli dai un numero iniziale (il seed), lui fa i suoi conti, e sforna una sequenza di numeri che sembra casuale. Supera i test. Distribuzione perfetta. Ma è deterministica: \(f(\text{stato}) \to \text{output}\). Se conosci lo stato, conosci ogni output futuro. Ogni singolo numero. Per sempre.
La cosa assurda: un buon PRNG frega tutti. Nessun test statistico lo distingue dal vero caso. Passa tutto: Diehard, TestU01, NIST. Ma sotto, è un orologio. Deterministico. Prevedibile. Basta sapere dove guardare.
Analogia: immagina un mazzo di carte mescolato così bene che nessuno riesce a indovinare la prossima carta. Ma c'è un trucco: il mazziere ha mescolato seguendo una regola precisa. Se scopri la regola, sai l'ordine di tutte le carte. Il Mersenne Twister è quel mazziere. E la regola ha 624 pezzi.
// Dentro il Mersenne Twister
Sezione 03. Matsumoto e Nishimura, 1997Lo tirano fuori nel 1997 due ricercatori giapponesi, Matsumoto e Nishimura. Il nome? Viene dal periodo: \(2^{19937} - 1\), che è un primo di Mersenne (un numero primo della forma \(2^p - 1\)). Per capire: è un numero con 6.002 cifre. Seimila cifre. Se generassi un miliardo di numeri al secondo, ci vorrebbe più tempo dell'età dell'universo per finire un ciclo. Sembra inattaccabile. Non lo è.
Lo stato interno è un vettore di 624 interi a 32 bit. Totale: 2.496 byte. 2.5 KB. Tutto il "caso" del tuo computer sta in meno spazio di un vocale della iena che dice "compra il latte". Tutto il "rumore" di Stable Diffusion, tutti i "dadi pesati" dell'LLM, tutte le playlist "random" di Spotify: 2.5 KB di stato e una funzione. Da questo stato, il MT genera un output alla volta tramite due operazioni: il twist (che aggiorna lo stato) e il tempering (che trasforma lo stato in output).
Chi lo usa? Tutti. Ma proprio tutti:
- Python (
randommodule) - Ruby (
rand) - PHP (
mt_rand) - R (default RNG)
- NumPy (legacy, prima della versione 1.17)
- Excel (
RAND()) - Unity e Unreal Engine
- MATLAB e GNU Octave
- PostgreSQL (
random())
La iena dorme. Apro il terminale. Come con l'LLM e il file GGUF, come con il VAE di Stable Diffusion, il primo passo è sempre lo stesso: apri, guarda dentro, conta i pezzi. Ho scritto smonta_mt.py. Apre lo stato interno, esegue il twist a mano, mostra il tempering, genera i primi 10 output:
I numeri confermano: 624 interi a 32 bit, 2.496 byte di memoria, periodo \(2^{19937} - 1\). 100.000 numeri generati con seed 42 (lo stesso seed della nave pirata), media 0.5001. Distribuzione perfettamente uniforme. Nessun test statistico troverebbe un difetto. Ma è tutto deterministico. Cambi il seed, cambi la sequenza. Stesso seed, stessa sequenza. Sempre. Su qualsiasi macchina, qualsiasi sistema operativo, qualsiasi versione di Python. Per questo seed 42 produceva sempre la stessa nave pirata.
// La Matematica: GF(2)
Sezione 04. Aritmetica binaria e la ricorrenzaQui la matematica fa paura solo a guardarla, ma fidatevi, è più semplice di quanto sembra. Il MT lavora in \(\text{GF}(2)\), il campo di Galois. Un mondo dove esistono solo due cose: 0 e 1. L'aritmetica:
- Addizione = XOR. 0+0=0, 0+1=1, 1+0=1, 1+1=0.
- Moltiplicazione = AND. 0×0=0, 0×1=0, 1×0=0, 1×1=1.
Due soli risultati. Nessuna sfumatura. La iena funziona allo stesso modo: o hai ragione o hai torto. Spoiler: hai torto. Le dici "ma anche tu avevi detto che..." e ti risponde "quello era diverso". In GF(2) almeno le regole sono coerenti.
In pratica, ogni intero a 32 bit diventa una fila di 32 zeri e uni. Lo XOR è la somma. E la ricorrenza che genera il prossimo stato combina tre pezzi dello stato attuale:
La ricorrenza MT19937. n=624, m=397. Tutto su GF(2).
Dove:
- \(\mathbf{x}_k\) è il k-esimo intero dello stato (32 bit)
- \(\text{upper}(\mathbf{x}_k)\) = bit 31 di \(\mathbf{x}_k\) (la parte alta)
- \(\text{lower}(\mathbf{x}_{k+1})\) = bit 0-30 di \(\mathbf{x}_{k+1}\) (la parte bassa)
- \(\|\) = concatenazione dei bit
- \(\mathbf{A}\) = matrice di compagnia con coefficiente \(\texttt{0x9908B0DF}\)
- \(\oplus\) = XOR
La moltiplicazione per \(\mathbf{A}\) si implementa come: shift a destra di 1 bit, e se il bit meno significativo era 1, XOR con \(\texttt{0x9908B0DF}\). Due righe di codice. Come l'attention dell'LLM che sembrava complicata e poi era un prodotto scalare con softmax, anche qui la formula fa più paura di quello che fa davvero:
I numeri 624 e 397 non sono a caso. Sono stati scelti perché fanno funzionare tutto al massimo: periodo lungo, distribuzione bella. Il polinomio della matrice è primitivo su GF(2), che in pratica vuol dire: periodo garantito al massimo possibile, \(2^{19937} - 1\).
n=624, w=32, r=31. Un numero con 6.002 cifre decimali.
Occhio: il periodo è astronomico, ma la sicurezza è zero. Il periodo ti dice solo "quanto ci mette a ripetersi". Non ti dice niente sulla predicibilità. Un orologio ha periodo 12 ore, ma se sai che ora è adesso, sai che ora sarà tra un'ora. Lo stato del MT ha 19.937 bit. Te ne servono 19.968 di osservazione per ricostruirlo. Cioè 624 numeri.
// Il Tempering (e Come Invertirlo)
Sezione 05. Sembra irreversibile. Non lo è.Dopo il twist, il MT fa un'altra cosa: il tempering. Quattro operazioni sui bit che trasformano lo stato in output. Servono a rendere i numeri più "belli" statisticamente. Eccole:
Le 4 operazioni di tempering. XOR, shift, AND con maschere costanti.
A prima vista sembra una funzione a senso unico: i bit si mescolano, si sovrappongono, le maschere tagliano via pezzi. Irreversibile? No. Ogni operazione si può invertire, perché lo XOR con un valore che viene da sé stesso non distrugge niente. I bit "nuovi" dipendono da bit che già conosci.
Prendiamo la quarta operazione: y ^= (y >> 18). Con parole a 32 bit e shift di 18, i primi 18 bit rimangono invariati (lo shift li porta oltre il bordo). Quindi conosco già i top 18 bit. Con quelli, ricostruisco i bottom 14. Un solo passo.
La seconda operazione (y ^= (y << 7) & mask) è più insidiosa: i bottom 7 bit sono invariati, poi ricostruisco 7 bit alla volta, dal basso verso l'alto. Servono 4 iterazioni. Ma funziona sempre. Come sbucciare una cipolla: sembra complicata, ma ogni strato rivela il successivo. La iena dice che sbuccio le cipolle troppo lentamente. Dice anche che cucino troppo lentamente. E che mangio troppo velocemente. L'inversione del tempering almeno è consistente.
15 righe. Ecco la funzione che rompe il random. Funziona? Prendo tutti i 624 valori dello stato interno, li tempero, li untempero, e verifico il round-trip:
624 su 624. Zero errori. Il tempering si inverte tutto. Da qualsiasi output del MT, recuperi lo stato interno esatto. Ecco dove si rompe.
Il punto: il tempering non è mai stato pensato per la sicurezza. Matsumoto e Nishimura volevano distribuzione migliore, non cifratura. È una trasformazione lineare su GF(2), e quelle si invertono sempre. Tutta quella roba di bit che si mescolano sembra complicata, ma è fumo. 15 righe di Python la smontano.
// L'Attacco: 624 Numeri
Sezione 06. Osserva, inverti, clona, prediciAdesso mettiamo tutto insieme. L'attacco è di una semplicità imbarazzante:
- Osserva 624 output consecutivi a 32 bit dal generatore vittima.
- Inverti il tempering su ciascuno (la funzione
untemper). - Clona il generatore impostando lo stato recuperato.
- Predici ogni output futuro. Per sempre.
Non ti serve il seed. Non ti serve forza bruta. Non ti serve nessun supercomputer. Non ti servono neanche i ventilatori. Solo 624 osservazioni e la funzione di inversione. La iena può continuare a dormire.
Risultato:
1000 su 1000. Match perfetto. Non solo sugli interi a 32 bit, anche sui float a 64 bit di random.random(). Il clone è una copia esatta del generatore vittima. Produce gli stessi numeri, nello stesso ordine, per sempre.
Guardate il grafico: i punti verdi (vittima) e le croci rosse (clone) sono perfettamente sovrapposti. Non "quasi". Non "approssimativamente". Identici. Bit per bit. Come Stable Diffusion con lo stesso seed produceva la stessa nave pirata, qui il clone produce gli stessi numeri. La differenza è che là serviva conoscere il seed. Qui no. Basta osservare.
Perché proprio 624? Perché lo stato ha 624 interi. Ogni osservazione ti rivela un intero (dopo l'untemper). Con 623 non hai abbastanza informazione. Con 624 hai tutto. Fine. Lo stato è tuo.
// Demo: Rompere Python
Sezione 07. shuffle, choice, randint: tutto predicibile
Ok, predire getrandbits(32) è impressionante. Ma nella pratica chi usa getrandbits direttamente? Tutti usano random.shuffle(), random.choice(), random.randint(). Queste funzioni sono costruite sopra lo stesso generatore. Se hai clonato il generatore, le predici tutte. È come nell'articolo sull'LLM: una volta che sai che il prossimo token è un dado pesato, sai che ogni funzione costruita sopra (chat, traduzione, riassunto) è lo stesso dado con una maschera diversa. Qui è uguale: shuffle, choice, randint sono tutti maschere sopra lo stesso MT.
Ho scritto crack_shuffle.py. Dopo il crack, predico: lo shuffle di un mazzo da 10 carte, lo shuffle di un mazzo da 52 carte, 10 random.choice() da una lista di colori, 10 random.randint(1, 100).
Tutto perfetto. Il mazzo di 52 carte mescolato dalla vittima? Lo conosco prima che finisca di mescolare. Il colore scelto a "caso"? Lo so prima che lo scelga. Il numero tra 1 e 100? Lo so prima che lo generi.
La iena conosce questa sensazione. Sa cosa sto per dire prima che apra bocca. "Non hai comprato il pane." "Hai lasciato la luce accesa." "Stai pensando a quella cosa del computer." Non le servono 624 osservazioni. Le bastano le prime tre lettere della frase e quindici anni di dati di addestramento.
Le implicazioni:
- Lotterie online che usano MT: se osservi 624 estrazioni, predici tutte le future.
- Poker online con MT: conosci le carte di tutti prima che vengano distribuite.
- Shuffle playlist: sai quale canzone verrà dopo.
- Giochi Unity/Unreal: predici spawn, loot, eventi casuali.
- Token di sessione generati con MT: li puoi predire e rubare sessioni.
- Captcha e password temporanee con MT: aggirabili.
Non è teoria. È successo davvero. Nel 2012 hanno bucato giochi online che usavano MT per le carte. Nel 2010 hanno sfruttato mt_rand() di PHP per predire token di sessione. Non è roba da paper accademico. È cronaca.
// Demo: Il "Rumore" di Stable Diffusion
Sezione 08. Il rumore non è rumoreTorniamo all'articolo su Stable Diffusion. La iena dormiva, io facevo girare SD su CPU per non fare rumore. "Rumore." È la parola che abbiamo usato per tutto l'articolo. Il rumore gaussiano, il rumore che la U-Net toglie, il rumore da cui emerge la nave pirata. L'abbiamo usata così tante volte che è diventata normale. Ma quella parola era una bugia.
Quando scrivi torch.Generator().manual_seed(42), PyTorch inizializza un Mersenne Twister con seed 42. Il tensore di "rumore" iniziale (4 × 64 × 64 = 16.384 valori) viene generato da quel MT. Il "rumore gaussiano" che abbiamo visto non era affatto casuale. Era una sequenza deterministica di 16.384 numeri prodotta dall'algoritmo che abbiamo appena crackato.
16.384 valori sono molto più dei 624 necessari per il crack. Il "rumore" di Stable Diffusion contiene, dentro di sé, l'intero stato del generatore. Anzi, di più: ogni valore gaussiano viene prodotto da una coppia di output MT uniformi (trasformazione di Box-Muller), quindi il tensore consuma oltre 32.000 output dal generatore. Più di 50 volte i 624 necessari. L'intero tensore era già scritto prima ancora di generarlo. Non era rumore. Era una sequenza.
Dal seed al tensore al quadro. Tutto deterministico. Zero casualità.
Facevo girare SD su CPU "per non fare rumore". Non c'era nessun rumore da fare. Non c'era nessun rumore nello script. Non c'era nessun rumore nel tensore. C'era un algoritmo deterministico del 1997, uno stato di 2.5 KB, e una funzione. Il rumore era solo un nome. L'unico rumore vero in quella stanza erano i miei tasti e il russare della iena.
Il cerchio si chiude: nell'articolo su Stable Diffusion abbiamo visto che "stesso seed = stessa composizione". Adesso sappiamo perché: il seed fissa lo stato del MT, lo stato fissa la sequenza, la sequenza fissa il tensore, il tensore fissa l'immagine. Una catena deterministica dal primo all'ultimo bit. Il "caso" non c'entra niente. L'arnia, la gallina, la iena dello stesso seed erano già scritte prima che premessi invio.
// Quando Serve il Vero Caso
Sezione 09. CSPRNG: quelli che non si crackanoOk, ma se il Mersenne Twister è così fragile, perché lo usano ancora tutti? Perché per la maggior parte delle cose va benissimo. Simulazioni. Test statistici. Giochi single-player. Generazione procedurale. Per queste robe, velocità e distribuzione uniforme contano più della sicurezza. Per Stable Diffusion va benissimo: il seed lo scegli tu, non è un segreto, e la riproducibilità è un feature, non un bug.
Ma quando serve sicurezza vera (crittografia, token, password, chiavi), si usano i CSPRNG (Cryptographically Secure Pseudorandom Number Generator). In Python:
La differenza: un CSPRNG non ti fa ricostruire lo stato dagli output. Puoi osservare miliardi di numeri, lo stato resta sconosciuto. Perché? Perché sotto usano roba crittografica seria (AES-CTR, ChaCha20) che non si inverte. Il tempering del MT lo inverti con 15 righe di Python. AES senza la chiave non lo inverti neanche con tutta la determinazione del mondo. Come la differenza tra il PIN del mio telefono (che la iena conosce da anni, non le ho mai detto come) e un caveau della banca (che non apro neanche volendo). La iena ha crackato il mio stato interno molto prima che io cracassi il Mersenne Twister.
| Proprietà | MT19937 | CSPRNG | True Random |
|---|---|---|---|
| Deterministico | Sì | Sì | No |
| Predicibile | Sì (624 output) | No | No |
| Velocità | Molto alta | Alta | Bassa |
| Distribuzione | Uniforme perfetta | Uniforme | Dipende dalla fonte |
| Uso corretto | Simulazioni, giochi | Crittografia, token | Chiavi master, seed CSPRNG |
| Esempio Python | random.random() |
secrets.token_hex() |
os.urandom()* |
| Stato interno | 2.5 KB, ricostruibile | 256 bit, non ricostruibile | N/A |
* os.urandom() su Linux/macOS legge da /dev/urandom, che mescola entropia hardware (interrupt timing, input devices) con un CSPRNG. Non è true random puro, ma è crittograficamente sicuro.
// Il Caso Non Esiste
Sezione 10. ConclusioneOk, ricapitoliamo. Tre articoli, tre cose rotte:
- L'LLM: non pensa, lancia dadi pesati. 8 miliardi di parametri, 128.256 facce sul dado, un token alla volta.
- Stable Diffusion: non disegna, toglie rumore. 860 milioni di parametri, 20 passi, da seed 42 alla nave pirata.
- Il Mersenne Twister: non genera caso, esegue una funzione. 624 interi, 2.5 KB di stato, completamente predicibile.
E adesso il cerchio si chiude. I dadi pesati dell'LLM? Mersenne Twister. Il rumore di Stable Diffusion? Mersenne Twister. Abbiamo seguito il filo da "il modello sembra intelligente" a "lancia dadi" a "i dadi sono prevedibili". Non c'è caso da nessuna parte. C'è un algoritmo del 1997, 2.5 KB di stato, e la parola "random" usata come marketing.
624 numeri. 15 righe di untemper. Stato completo. Futuro predetto. Non serve conoscere il seed, non serve forza bruta, non serve un supercomputer. Serve solo osservare.
La iena si è svegliata mentre scrivevo. "Ancora al computer?" Sì. "Che fai?" Ho rotto il caso. "Quale caso?" Il caso. Il random. Il computer non sa fare i numeri a caso. "Neanche io so fare i numeri a caso. Se mi chiedi un numero a caso dico sempre 7."
E questa è la differenza. La iena dice sempre 7, ma non sai quando dirà 7. Non sai se la prossima volta ti risponderà con un numero, con una lamentela sulla Nera, o con un discorso sulle api che dura quaranta minuti. La iena è imprevedibile non perché genera caso, ma perché il suo stato interno ha più di 624 interi. Il Mersenne Twister no. Il Mersenne Twister ha esattamente 624 interi, e dopo averli visti, è finita. Lo conosci tutto. Per sempre.
La iena non puoi prevederla. Il random del computer sì.
624 osservazioni. 15 righe di inversione. Stato completo. Futuro predetto. Il "caso" del computer è 2.5 KB di stato deterministico. Il dado pesato che lancia l'LLM, il rumore che toglie Stable Diffusion: tutto parte da qui. Da un algoritmo del 1997 che finge di essere casuale. E che abbiamo appena smontato. La iena dice sempre 7. Ma non sai quando. Questo la rende più casuale del Mersenne Twister.
Signal Pirate