Reverse engineering di una firma offuscata: il 90% è standard, il 10% che uccide sono i punti di deviazione

Ultimo Aggiornamento: 2026-07-30 10:24:38

Aprire un SDK di firma offuscato dà subito una sensazione ingannevole: sembra un artefatto esoterico, dove ogni riga esegue calcoli impossibili da seguire.

Dopo aver completato l'intero percorso di reverse engineering, di solito emerge l'esatto contrario: il 90% del codice implementa algoritmi standard. L'hash è pubblico, la codifica è pubblica, il cifrario a flusso è pubblico. Non serve nemmeno comprenderne ogni dettaglio: basta riconoscere l'algoritmo, riprenderlo dall'RFC o da un'implementazione di riferimento, e l'output coinciderà byte per byte.

Il problema letale è il 10% restante: i punti in cui un algoritmo noto è stato ritoccato quasi impercettibilmente. Un numero di round che dovrebbe essere fisso diventa variabile; una costante base viene sostituita con un valore vicino; gli indici delle word di un round vengono scambiati; la chiave viene nascosta nel ciphertext. Sono cambiamenti minimi, ma basta non vederne uno perché la riscrittura non corrisponda al target. E capire dove sta l'errore diventa difficile proprio perché l'altro 90% è corretto.

Il cuore del reverse engineering di una firma offuscata non è leggere quel 90%, ma trovare il 10% dei punti di deviazione. In questo articolo vediamo come affidare questa caccia a un modello di grandi dimensioni e perché è il passaggio in cui il modello è meno sostituibile.

(Il prerequisito — identificare la famiglia algoritmica, riconoscere dai valori costanti e dalla struttura frasi come «questo è ChaCha» o «questo è FNV» — è trattato nel precedente articolo. Qui do per acquisito lo scheletro dell'algoritmo e passo direttamente alla ricerca delle deviazioni.)

Perché le deviazioni sono proprio ciò che l'occhio salta

Quando riconosce uno schema, il cervello umano ha una tendenza forte: afferra l'idea generale e smette di controllare i dettagli.

Vedi un blocco di codice, trovi le costanti caratteristiche di ChaCha, la struttura torna, e mentalmente spunti la casella: «è ChaCha20». Poi prosegui. Non verifichi uno per uno il numero di round, ogni indice o l'ultimo bit di ciascuna costante, perché la soddisfazione di averlo già riconosciuto abbassa l'attenzione.

È esattamente su questo che contano gli autori dell'offuscamento. Non riscrivono l'algoritmo da zero — costerebbe troppo e introdurrebbe più bug — ma apportano la minima modifica possibile a uno standard: cambiano un numero, scambiano un indice, aggiungono un passaggio. Quanto basta perché il riconoscimento visivo lo ignori, ma anche perché una reimplementazione conforme allo standard fallisca completamente.

È una guerra dell'attenzione asimmetrica. A chi ha scritto l'offuscamento basta nascondere una sola deviazione; chi analizza deve trovarle tutte. E gli esseri umani sono naturalmente poco portati a restare sospettosi davanti a qualcosa che sembra giusto.

Qui il modello ha un vantaggio controintuitivo: non prova la soddisfazione del «l'ho già riconosciuto» che spegne l'attenzione. Se il prompt gli chiede esplicitamente di cercare le deviazioni, può controllare ogni elemento senza allentare la presa a metà strada — a condizione di guidarlo nel modo giusto, tema della Sezione 5. Prima, però, vediamo le quattro forme più comuni di deviazione.

Le quattro deviazioni più frequenti

Per ciascun caso userò un algoritmo standard pubblico: prima per mostrare l'implementazione normale, poi per descrivere l'alterazione. In un progetto reale, tutte e quattro possono convivere nello stesso signer.

1. Da costante a variabile: un parametro fisso diventa dipendente dal runtime

Un cifrario a flusso standard ha un numero di round fisso. ChaCha20 ne usa 20, senza eccezioni: ogni implementazione standard lo codifica direttamente e RFC 8439 lo chiarisce già nell'introduzione, specificando che descrive solo ChaCha a 20 round, mentre le varianti a 8 e 12 round sono definite altrove. Il numero di round è sempre stato una costante dello standard.

La deviazione consiste nel calcolare dinamicamente dal key un numero di round che dovrebbe essere fisso. La logica del quarter-round resta identica, ma il numero di esecuzioni dipende da determinati byte della chiave. Cambia la chiave, cambia anche il numero di round.

Perché sfugge all'analisi umana? Hai riconosciuto i quarter-round, hai riconosciuto le costanti σ, il cervello ha concluso «ChaCha20» e copi 20 round. Non controlli nemmeno se la variabile che governa il ciclo è una costante oppure un'espressione: in ogni ChaCha visto finora era sempre una costante.

Il segnale distintivo è questo: dove ti aspetti un valore costante, trovi invece un'espressione dipendente dall'input. Un passaggio di contro-evidenza ben guidato controllerà proprio se un valore hardcoded nell'implementazione standard viene qui calcolato.

Come confermarlo con test differenziali: esegui deliberatamente l'algoritmo con due chiavi che differiscono per un solo byte. Se la differenza in output è molto maggiore dell'influenza attesa da un byte, quel byte sta alimentando un parametro globale, come il numero di round, invece di limitarsi a entrare nello XOR del keystream.

2. Perturbazione di costante: una costante fondamentale sostituita con un valore vicino

L'hash FNV-1a usa due magic number pubblici: offset basis e prime. Ogni implementazione corretta di FNV-1a impiega esattamente quei valori, pubblicati nello standard. La pagina di riferimento FNV curata dal coautore Landon Curt Noll indica, cifra per cifra, l'offset basis a 32 bit come 2166136261 e il prime come 16777619.

La deviazione: sostituire una di queste costanti fondamentali con un valore vicino, differente dallo standard di pochissimo. La struttura dell'hash rimane identica — XOR, moltiplicazione e ciclo sono tutti corretti — ma il valore iniziale è stato modificato in modo quasi invisibile.

Per un umano, questo è probabilmente il caso più insidioso. Riconosci la struttura di FNV, vedi una costante grande che assomiglia all'offset basis e concludi «FNV-1a standard». Ma non confronti quel numero bit per bit con quello ufficiale: chi ricontrolla una magic number che pensa di aver già riconosciuto?

Questa deviazione si intercetta quasi soltanto con il testing differenziale, perché valutare a occhio numeri grandi è notoriamente inaffidabile. Prendi un input noto, eseguilo nell'implementazione standard e nella black box target, quindi confronta i risultati. Se la struttura è identica ma gli output differiscono, il problema è quasi certamente in una costante base. A quel punto passa la costante sospetta al modello e fagli confrontare il valore con lo standard: una macchina è molto più affidabile di te in questo compito.

Il contributo del modello, qui, è molto concreto: ricorda l'offset basis standard di FNV-1a fino all'ultimo bit, mentre tu no. Basta chiedergli «questa costante coincide esattamente con l'offset basis standard di FNV-1a?» e indicherà subito la differenza.

3. Modifica strutturale: un passaggio standard viene riscritto localmente

Ogni double round di ChaCha è composto da 8 quarter-round: i primi 4 operano sulle colonne, i 4 successivi sulle diagonali. Gli indici delle word su cui agisce ciascun quarter-round sono stabiliti dallo standard: RFC 8439 §2.3 descrive la block function e specifica esattamente quali word dello stato vengono toccate da ogni round.

La deviazione consiste nel scambiare silenziosamente uno o due indici di word in uno dei round. La grande maggioranza dei round resta standard; solo in un punto intermedio una word che dovrebbe essere elaborata viene sostituita da un'altra. Il codice continua a sembrare ChaCha e funziona senza errori, ma il keystream prodotto è completamente diverso da quello di ChaCha standard.

Perché è facile non notarlo? Gli indici dei quarter-round sono una sequenza lunga di numeri: otto gruppi come (0,4,8,12)(1,5,9,13)…. Lo sguardo scorre e conferma soltanto «sì, round di colonna e round diagonali», senza verificare che i quattro numeri di ogni gruppo siano nelle posizioni previste. Una modifica nascosta dentro una sequenza di indici già faticosa da leggere è un ottimo nascondiglio.

Neppure per il modello basta un colpo d'occhio: va guidato a elencare gli indici round per round e confrontarli con ChaCha standard. È un controllo meccanico, esattamente il tipo di lavoro in cui il modello non perde il filo mentre un umano tende a farlo. Fagli produrre una tabella «indice standard vs indice effettivo» e la deviazione emergerà da sola.

Il test differenziale la conferma così: se i primi due tipi sono esclusi — il numero di round è corretto e le costanti pure — ma l'output continua a non coincidere, il problema è strutturale. Salva lo stato intermedio round per round: il primo round che diverge da ChaCha standard è quello modificato.

4. Incorporazione del materiale: dati della chiave nascosti nell'output

I primi tre casi alterano l'algoritmo; questo modifica invece l'organizzazione dei dati.

Una tecnica comune prevede che la chiave di cifratura non venga trasmessa su un canale separato: viene invece spezzata e inserita direttamente nel ciphertext, da cui il ricevente la estrae con la stessa regola. La versione più subdola non usa una posizione fissa: la calcola dal contenuto stesso dei dati. Se cambia il ciphertext, cambia anche il punto in cui la chiave è nascosta. Lo strato più esterno avvolge poi il tutto in Base64 con alfabeto personalizzato e un byte prefisso marcatore.

Perché sfugge? Mentre combatti con l'algoritmo di cifratura, potresti non accorgerti che la chiave non va affatto violata: è già lì, nel ciphertext che hai davanti, solo che non sai in quale segmento cercarla. Chi è alle prime armi spesso resta bloccato tentando di «craccare» qualcosa il cui plaintext è già disponibile.

Il segnale da cercare è una regione di un blocco dati con caratteristiche statistiche diverse da ciò che la circonda. Una chiave contiene in genere byte casuali ad alta entropia e, inserita nel mezzo del ciphertext, forma un «segmento estraneo» riconoscibile. Il modello può aiutare ad analizzare quale intervallo dell'output presenta una distribuzione dei byte diversa dal resto, individuando i confini del materiale incorporato.

Per confermare l'ipotesi, se riesci a individuare la regola di inserimento — ad esempio un modulo sulla somma dei byte — usa alcune coppie input/output note per ricavare a ritroso la formula della posizione, poi verificala in avanti. Il modello può aiutare a inferire la regola da pochi campioni, ma la decisione finale spetta sempre a assert.

Il passaggio di contro-evidenza è il vero terreno di gioco dei modelli di ragionamento

Le quattro categorie hanno un filo conduttore: riconoscere la famiglia algoritmica, cioè il candidato, è facile; individuare la deviazione, cioè la contro-evidenza, è difficile.

Il passaggio del candidato è alla portata di quasi tutti i modelli. Le costanti σ di ChaCha e la struttura di FNV sono segnali che qualunque modello abbia visto i dati di addestramento sa riconoscere. La differenza reale tra modelli sta nel passaggio di contro-evidenza: nella capacità e nella volontà di continuare a mettere in discussione un algoritmo appena riconosciuto, dicendo «questa parte, però, non coincide con lo standard».

Un modello più debole tende a perdere slancio in questo punto. Dopo aver riconosciuto «questo è ChaCha20», il suo passaggio di contro-evidenza spesso degenera in una riformulazione del candidato: «l'implementazione segue la classica struttura ChaCha20, con i tradizionali quarter-round…». È una ripetizione, non un controllo effettivo delle deviazioni. Un'analisi del genere non può guidare il passo successivo.

Il passaggio di contro-evidenza di un modello di ragionamento solido è tutt'altra cosa. Scrive: «il candidato è ChaCha20, ma ci sono tre differenze rispetto all'implementazione standard: primo, il numero di round è regolato da un'espressione dipendente dalla chiave, mentre ChaCha20 standard usa sempre 20 round; secondo, gli indici delle word del round N non corrispondono al round diagonale standard; terzo…». Ogni osservazione punta a un punto di deviazione specifico e verificabile. Un passaggio di contro-evidenza così è già la checklist dei test differenziali che scriverai nello Stage 3.

Ecco perché conviene assegnare l'identificazione della famiglia algoritmica al tier di ragionamento e provarlo in prima persona prima di impegnarsi. La qualità della contro-evidenza dei vari modelli determina direttamente quanti test inutili scriverai e quante deviazioni di percorso farai.

I quattro tier che uso in questo workflow:

Fase

Capacità richiesta

Scelta

model id

Mappatura strutturale dopo lo split

Contesto lungo, lettura dell'intero modulo in un passaggio

Kimi K3

kimi-k3

Identificazione della famiglia algoritmica e contro-evidenza

Ragionamento forte, capacità di contraddirsi

Claude Opus 5

claude-opus-5

Rinominazione massiva dei simboli

Economico, alta concorrenza

Claude Sonnet 5

claude-sonnet-5

Attribuzione delle differenze

Ragionamento intermedio, spiegazioni basate su byte specifici

GPT-5.6 Sol

gpt-5.6-sol

Il secondo tier è il fulcro di questo articolo. Non fidarti della mia scelta: mettila alla prova. Il protocollo è semplice:

  1. Scegli 2–3 funzioni leaf dal tuo bundle offuscato, includendone almeno una di cui conosci già la risposta, come controllo.

  2. Usando il prompt in tre parti «candidato / evidenza / contro-evidenza» dell'articolo precedente, invia lo stesso input separatamente a claude-opus-5 e gpt-5.6-sol.

  3. Osserva solo il passaggio di contro-evidenza: controlla davvero le deviazioni una a una, oppure riformula il candidato? Sul controllo, quante deviazioni ha rilevato ciascun modello?

  4. Il numero e la qualità delle deviazioni individuate sono il criterio di scelta.

La differenza si vedrà già alla prima esecuzione, in modo più diretto di qualunque classifica benchmark.

Il vero ostacolo è il costo di cambiare modello

Quattro modelli, distribuiti su tre vendor. L'approccio ingenuo consiste nel collegare tre SDK, tre sistemi di autenticazione e tre gestori degli errori. Molti fanno i conti, decidono che non ne vale la pena e usano un solo modello per tutto: un tier debole nella contro-evidenza per identificare la famiglia algoritmica, con una serie di deviazioni di percorso di cui ignorano la causa.

AIReiter appiattisce questo livello: una chiave, un'interfaccia compatibile con OpenAI, tutti e quattro i modelli dietro la stessa API. Per cambiare modello basta modificare il campo model nel body della richiesta.

# Algorithm-family ID + counter-evidence: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<three-part prompt + leaf function + constants>"}]
  }'

# Bulk symbol renaming: change one field
#   "model": "claude-sonnet-5"
# Difference attribution:
#   "model": "gpt-5.6-sol"

Usi già l'SDK OpenAI? Imposta base_url su https://aireiter.com/api/v1 e non modificare altro. Usi l'SDK Anthropic? Chiama POST /api/v1/messages con la stessa chiave.

Sul fronte prezzi, i modelli Claude hanno uno sconto del 30% sul listino e i modelli GPT costano la metà. In questo workflow lo sconto incide proprio dove conta: il passaggio di identificazione della famiglia algoritmica richiede iterazioni ripetute del prompt, con la stessa funzione analizzata in molti round, ed è la parte più intensa in termini di chiamate; la rinominazione massiva dei simboli parte da centinaia di chiamate. Sono questi i due elementi che pesano maggiormente sul costo.

In sintesi

Il punto essenziale nel reverse engineering delle firme offuscate è questo: la maggior parte del codice usa algoritmi standard da riprodurre fedelmente; il lavoro vero è cercare i punti in cui uno standard è stato modificato senza dare nell'occhio.

Le quattro deviazioni — da costante a variabile, perturbazione di costante, modifica strutturale e incorporazione del materiale — hanno una caratteristica comune: sono abbastanza piccole da essere ignorate dal riconoscimento umano degli schemi, ma abbastanza grandi da far fallire completamente una riscrittura. Gli umani faticano a restare sospettosi di ciò che sembra corretto: con il prompt adeguato, questa è precisamente la forza del modello.

Il modello, però, genera soltanto sospetti; non conferma nulla. Ogni ipotesi su un punto di deviazione deve infine trasformarsi in un test differenziale, argomento dell'articolo sui test differenziali. Il modello ti consegna un elenco di «punti che potrebbero essere stati modificati»; assert stabilisce quali lo sono davvero.