AIREITER

Non trovi la logica di firma nel codice? Potrebbe non esserci affatto

Ultimo Aggiornamento: 2026-07-31 07:45:33

Hai scaricato l'intero bundle webpack, isolato l'AST, catalogato le primitive finali e ormai sai riconoscere le fingerprint delle routine note. Poi incontri un valore di firma in un header di richiesta: cambia ogni volta e vuoi trovare la funzione che lo genera. Cerchi operazioni sui bit sospette e scheletri di hash in tutte le risorse statiche, ma non salta fuori nulla.

Non è detto che la ricerca sia stata fatta male. Più semplicemente, la logica che cerchi non è nel codice che hai scaricato.

Esiste una categoria di firme per cui l'analisi statica è destinata a fallire: l'algoritmo compare soltanto a runtime. In questi casi non serve insistere nella caccia a una funzione inesistente. Bisogna cambiare approccio: invece di fare reverse engineering dell'algoritmo, eseguirlo oppure leggerne il risultato, con l'intervento minimo necessario.

Quando l'analisi statica non basta: due segnali chiari

Prima di concludere di essere davanti a questo scenario, assicurati di non aver semplicemente trascurato qualcosa. Ci sono due indizi facili da riconoscere.

Il primo: il valore cambia a ogni sessione o richiesta, ma nelle risorse statiche non c'è nulla che lo generi. Lo vedi in una richiesta di rete e cambia a ogni refresh; scarichi tutti i .js, cerchi nel testo completo e non trovi nessun punto in cui venga costruito. Il codice che lo compone viene consegnato a runtime.

Il secondo: il valore è cercabile come letterale nel bundle, ma quello annotato ieri oggi non funziona più. Nel build output appare come semplice costante stringa e, se la copi, continua a funzionare per quel giorno. Dopo qualche giorno l'endpoint restituisce un errore e scopri che quella presunta costante è stata sostituita da un nuovo letterale, anch'esso rintracciabile. È hardcoded in fase di build, ma cambia a ogni release del frontend.

I due segnali hanno una radice comune: la vista statica è un'istantanea, mentre il dato reale è un flusso che varia nel tempo o da una sessione all'altra. Puoi cercare nel codice senza trovare nulla, oppure trovarlo e accorgerti che non resta valido. In entrambi i casi, il metodo «scarica una volta e copia» ha smesso di funzionare. Non è un problema di incapacità nel riconoscere una famiglia algoritmica: non dipende dalla debolezza del tuo matching delle fingerprint, ma dal fatto che ciò da confrontare non esiste nella copia di codice che hai tra le mani.

Primo caso: il server ti invia l'algoritmo da eseguire

Nel primo scenario, per accedere a un endpoint il server invia prima un frammento JavaScript monouso. Lo script viene eseguito dal browser, produce un cookie o un token e devi includerlo per proseguire. Il suo contenuto varia in genere per sessione e, talvolta, a ogni richiesta.

Ecco perché l'analisi statica non può funzionare: la logica viene consegnata a runtime e non fa parte del bundle statico. Uno script catturato in un certo momento è soltanto un'istanza specifica. Il codice scaricato oggi e quello inviato domani possono essere completamente diversi; farne il reverse engineering significa inseguire un bersaglio mobile.

La strategia corretta è eseguirlo come una black box. Non devi sapere cosa calcola: ti basta offrirgli un ambiente abbastanza realistico da farlo arrivare al risultato, quindi recuperare quel risultato. In pratica:

  • Prepara uno shim minimo dell'ambiente browser: stub vuoti per document, location, navigator, cookie, qualche Observer e i timer. Aggiungi solo ciò che serve finché lo script smette di fallire per l'assenza di un oggetto globale.

  • Esegui il sorgente ricevuto in una sandbox locale Node/V8, tramite node:vm oppure un processo avviato da execjs, impostando un timeout di esecuzione.

  • Durante l'esecuzione lo script scrive un cookie, oppure inserisce un valore in una variabile globale. Intercetta quella scrittura ed estrai il token necessario.

Il controllo visitatori di Zhihu ha proprio questa forma: uno script ricevuto imposta un identificatore del visitatore nei cookie del browser; tu simuli l'ambiente DOM affinché creda di girare in una pagina reale, quindi prelevi il valore al termine dell'esecuzione. Non hai invertito neppure una riga dell'algoritmo: hai solo allestito una scena abbastanza credibile da permettergli di recitare la sua parte. Il costo non sta quindi nel «capire l'algoritmo», ma nel «mantenere l'ambiente sufficientemente realistico». Lo script può controllare navigator.webdriver, verificare l'esistenza di un nodo o aspettarsi una risposta da una certa API: lo shim deve ingannarlo con precisione, senza diventare troppo grande da mantenere. Questo compromesso è al centro della sezione sulla «superficie minima di esecuzione» più avanti.

Secondo caso: un identificatore dinamico che cambia a ogni release

Il secondo scenario è l'opposto: il valore è davvero nel bundle statico, come letterale, ma viene generato durante la build e cambia a ogni release del frontend.

Il caso classico riguarda i frontend moderni che, al posto di query GraphQL in chiaro, usano query preregistrate. Ogni operazione, come caricare la timeline o i risultati di ricerca, viene associata in fase di build a un operation id o query id, trasmesso nel percorso dell'endpoint. L'id è cercabile nel build output, ma quando il frontend pubblica una release l'operazione riceve un nuovo valore.

Qui l'analisi statica dà una falsa sensazione di successo. Hai trovato l'id, l'hai copiato, lo hai usato quel giorno e l'hai hardcoded. Due settimane dopo l'endpoint restituisce 400 e solo allora scopri che quella costante era tutt'altro che immutabile. È più insidioso del primo caso, perché eri convinto di aver già risolto il problema.

Non c'è un algoritmo da invertire: è una costante di build. La soluzione è leggere il valore corrente dalla pagina corrente al momento della richiesta e metterlo in cache per la sessione. Le opzioni vanno dalla meno alla più costosa, e conviene provarle in quest'ordine:

  1. Controlla le risorse già caricate dalla pagina. La pagina ha appena inviato una richiesta che contiene l'identificatore: il valore corrente è già nell'URL e puoi estrarlo dal record della risorsa. È l'opzione più economica: leggi un dato esistente senza toccare una riga di algoritmo.

  2. Se non puoi recuperarlo lì, estrailo con un pattern dal sorgente dello script. Cerca nel bundle corrente la dichiarazione che associa il nome dell'operazione al relativo id e prendi quel valore.

  3. Solo se anche questo non funziona, esplora la tabella dei moduli del bundler oppure scarica e analizza bundle correlati partendo da indizi. È la strada più costosa e va lasciata per ultima.

Gli operation id di timeline e ricerca di Twitter si recuperano esattamente così: si estrae l'id dall'URL di una richiesta GraphQL già inviata dalla pagina, lo si mette in cache per la sessione e lo si riutilizza per tutta la sua durata.

La versione sistematica di questo caso — i diversi modi per ottenere una query preregistrata, il loro costo e i criteri di scelta — è trattata nell'articolo sulle persisted operations. Qui basta collocarlo nella categoria più ampia dei valori derivati a runtime.

La differenza operativa tra i due scenari

Affiancandoli, la strategia da adottare diventa immediata.

Primo caso: challenge

Secondo caso: identificatore dinamico

Dove si trova il dato reale

Viene consegnato a runtime, mai nel codice statico

È nel codice statico, ma cambia a ogni release

Cosa fare

Eseguirlo: avvia l'algoritmo reale e recupera l'effetto collaterale

Leggerlo: individua e leggi una costante, senza eseguire algoritmi

Come si manifesta un errore

Lo shim dell'ambiente è troppo scarno e il codice non termina

Il reader fallisce dopo un riordino del bundle e restituisce un valore obsoleto o vuoto

Chi innesca il cambiamento

Il server, in qualunque momento

Una release del frontend, secondo la cadenza di deploy

In breve: entrambi invalidano il modello «scarica una volta e copia». L'unica differenza è stabilire se devi davvero eseguire il codice. Capirlo decide subito se il prossimo passo sarà una sandbox oppure un extractor.

Perché qui non conviene forzare la purificazione

Potresti obiettare: non è possibile fare reverse engineering completo dell'algoritmo dello script ricevuto, oppure ricostruire integralmente la regola che genera l'identificatore dinamico, implementandola nativamente e senza dipendere dal runtime originale? È la purificazione della fase quattro del workflow in quattro fasi: la soluzione più pulita, un investimento per l'indipendenza a lungo termine, da inserire nella CI.

Per questi due tipi di target, nella maggior parte dei casi non conviene. Un modello di ritorno sull'investimento semplice ma utile è questo:

La purificazione comporta un costo una tantum C, dato dal reverse engineering dell'algoritmo e dalla sua validazione tramite differential testing. Dopo la purificazione, risparmia s per unità di tempo rispetto a «eseguire o leggere ogni volta». Il tempo di rientro è circa C / s.

La variabile decisiva non è né C né s, ma il ciclo di cambiamento del target T: quanto spesso cambia.

  • T < C/s: il target cambia prima che l'investimento rientri; l'implementazione purificata smette di corrispondere dopo pochi giorni dalla pubblicazione e devi rifarla. Rendimento negativo.

  • T è molto maggiore di C/s: la purificazione è certamente conveniente; un singolo investimento dura a lungo e dovresti salire nella scala della purificazione fino al livello di riscrittura nativa.

I due tipi di target si collocano naturalmente in punti diversi:

  • Nel primo caso, T è controllato dal server e può essere arbitrariamente breve. La logica dello script consegnato può cambiare in qualsiasi momento e non puoi prevederlo. Per questo quasi sempre conviene «eseguire». Forzare la purificazione di un algoritmo che qualcuno potrebbe modificare domani significa dipendere dalla sua cadenza di release.

  • Nel secondo caso, T coincide con il ciclo di release del frontend, da giorni a mesi. Più il reader è robusto, con qualche fallback in più e ancore più stabili, più C/s diminuisce. Entrambe le scelte possono quindi essere ragionevoli: qui vale la pena fare davvero i conti.

Questo è il senso completo del titolo. Se non trovi qualcosa nel codice, non significa necessariamente che ti sia sfuggito: potrebbe non esserci alcun algoritmo stabile che valga la pena purificare.

Come definire una superficie minima di esecuzione

Una volta scelta l'esecuzione anziché la purificazione, cambia anche l'obiettivo ingegneristico. Non cerchi più il codice più pulito possibile, ma una superficie di esecuzione ridotta al minimo, controllata e con errori attribuibili. Ci sono tre aspetti da curare.

Esegui soltanto il frammento indispensabile. Non portare dentro l'intero runtime della pagina. Fornisci solo il codice da cui l'algoritmo dipende davvero, più uno shim minimo. Esiste un punto di equilibrio: lo shim deve essere abbastanza piccolo da limitarsi a far girare il codice senza eccezioni. Ogni stub aggiuntivo è un onere di manutenzione in più — cambiano una sonda e devi inseguirla — mentre ogni stub mancante provoca un crash immediato. Non importare un intero ambiente browser per risparmiare tempo: altrimenti non stai mantenendo un signer, ma mezzo browser.

Proteggi il contesto. Un contesto V8/Node compilato non è thread-safe. Una challenge monouso apre naturalmente una sandbox nuova ogni volta, quindi le esecuzioni non interferiscono. Ma se riusi un contesto compilato per ridurre il costo di avvio, oppure metti in cache un parser di identificatori dinamici da condividere fra le richieste, le chiamate concorrenti devono essere serializzate:

class RuntimeSigner:
    def __init__(self, source: Path) -> None:
        self._context = execjs.get("Node").compile(source.read_text())
        self._lock = threading.Lock()  # V8 context is not thread-safe

    def call(self, fn: str, *args):
        with self._lock:
            return self._context.call(fn, *args)

Rendi attribuibili i fallimenti. È l'aspetto più trascurato, ma anche quello che fa risparmiare più tempo quando si recuperano valori tramite esecuzione. Un errore deve indicare il livello in cui si è verificato:

  • Ambiente troppo scarno: il codice lancia un ReferenceError, oppure resta bloccato fino al timeout. Lo shim non fornisce qualcosa che lo script richiede.

  • Protocollo cambiato: il codice termina e produce un output, ma il formato non è corretto o il JSON non viene parsato. È cambiato il formato di output.

  • Semantica non soddisfatta: hai ottenuto un valore, viene parsato, ma è incompleto oppure non supera la validazione quando lo usi. È stato modificato l'algoritmo stesso.

Queste tre situazioni richiedono tre correzioni completamente diverse: aggiornare lo shim, seguire il protocollo oppure verificare l'algoritmo. Un executor che si limita a segnalare genericamente «non ha funzionato» costringe a ricominciare l'indagine da zero ogni volta. Un executor maturo distingue esplicitamente errore di esecuzione, output non parsabile, risultato incompleto e timeout.

Il modello giusto per ogni fase

In questo flusso il ruolo del modello è diverso dal lavoro di fingerprinting. Lì il modello identifica la famiglia algoritmica; qui non c'è un algoritmo da identificare. Il suo compito principale è aiutare nella scelta architetturale tra «purificare o eseguire» e nell'attribuzione degli errori di esecuzione. Le quattro sottofasi richiedono capacità molto diverse:

Sottofase

Capacità richiesta

Scelta

model id

Decidere tra purificare ed eseguire, argomentando entrambe le opzioni

Ragionamento forte, capacità di contestare la propria conclusione

Claude Opus 5

claude-opus-5

Individuare dove si nasconde l'identificatore dinamico: trovare il punto di iniezione della costante e leggere i candidati nel bundle

Contesto lungo, lettura dell'intero bundle in una sola volta

Kimi K3

kimi-k3

Filtrare in massa gli script candidati che potrebbero contenere l'operazione cercata

Economico, centinaia di chiamate con alta concorrenza

Claude Sonnet 5

claude-sonnet-5

In caso di errore di esecuzione, leggere log o stack per capire quale livello ha fallito

Ragionamento intermedio, spiegazione basata su un errore specifico

GPT-5.6 Sol

gpt-5.6-sol

Vale la pena soffermarsi sulla prima riga, perché è l'unico passaggio di questo articolo in cui cambiare modello modifica visibilmente il risultato. Qui viene testata proprio la capacità di argomentare entrambi i lati e contraddire se stessi, la stessa richiesta dalla sezione sulle controprove nel lavoro di fingerprinting. Un modello più debole sceglie una strada e accumula motivazioni a suo favore senza discutere seriamente l'alternativa. Un modello di ragionamento forte porta fino in fondo sia l'ipotesi «purificare» sia quella «eseguire», esplicita l'argomento migliore e il possibile fallimento di ciascuna, poi formula una conclusione comparativa.

Non fidarti sulla parola: prova direttamente.

  1. Prendi un target reale per cui hai già preso una decisione, da usare come controllo: sai per esperienza se dovrebbe essere eseguito o purificato.

  2. Fornisci i fatti osservati — logica consegnata a runtime o costante di release, frequenza dei cambiamenti, profondità delle dipendenze dall'ambiente — a claude-opus-5 e gpt-5.6-sol, chiedendo a entrambi un memo decisionale «purificare vs eseguire».

  3. Valuta due aspetti: ha riconosciuto il «ciclo di cambiamento» come variabile decisiva? Se confronta soltanto la difficoltà di implementazione, ha sbagliato. Inoltre, le condizioni indicate per cambiare idea sono osservabili? Una formulazione come «se l'identificatore non cambia nella prossima release, torna alla purificazione», accompagnata da un segnale di attivazione, è utilizzabile.

Un singolo giro basta per capire quale modello sta davvero prendendo una decisione e quale si limita a prenderla al posto tuo.

Il vero problema è il costo del cambio di modello

Quattro modelli di tre vendor, tre SDK, tre schemi di autenticazione, tre formati di errore. Riscrivere il client tre volte solo per usare modelli diversi nelle varie sottofasi non vale lo sforzo. Per questo molti usano un solo modello dall'inizio alla fine: scelgono un modello incapace di contraddirsi nella fase «purificare o eseguire», si lanciano nella purificazione di un target che l'altra parte modificherà domani e percorrono una lunga strada prima di accorgersi che la direzione era sbagliata fin dall'inizio.

AIReiter elimina questo livello di complessità: una chiave, un'interfaccia compatibile con OpenAI e tutti e quattro i livelli dietro le quinte. Per cambiare modello basta modificare il campo model nel body della richiesta.

# Purify or execute: the reasoning tier arguing both sides
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": "<observed facts + argue the strongest case for both purify and execute>"}]
  }'

# Locate the dynamic identifier across the bundle: change the model field, leave the rest
#   "model": "kimi-k3"
# Bulk-filter candidate scripts:
#   "model": "claude-sonnet-5"
# Attribute an execution failure:
#   "model": "gpt-5.6-sol"

Se utilizzi già l'SDK OpenAI, imposta base_url su https://aireiter.com/api/v1 senza modificare altro. Con l'SDK Anthropic, usa POST /api/v1/messages con la stessa chiave.

Sul fronte dei costi, il peso in token di questo flusso è concentrato in pochi punti: Kimi K3 legge l'intero bundle frontend per individuare l'identificatore dinamico, con alcune centinaia di migliaia di token per input, mentre Claude Sonnet 5 filtra in massa gli script candidati, spesso con centinaia di chiamate. Sono questi due a incidere sulla maggior parte della spesa. Lo sconto del 30% su Claude si applica proprio al filtro massivo con Sonnet e all'argomentazione decisionale con Opus; GPT a metà prezzo copre l'attribuzione dei fallimenti con GPT-5.6 Sol; inoltre la lettura dell'intero bundle con contesto lungo su K3 è disponibile con la stessa chiave. Lo sconto si concentra sul batch più pesante in termini di token e sul livello di ragionamento più costoso, non su un generico modello economico.

Conclusioni

Quando l'analisi statica non trova nulla, non è sempre un limite delle tue capacità: talvolta dipende dal target. La firma non è nel codice perché viene consegnata a runtime, oppure cambia a ogni release.

In questi due casi, smetti di inseguire una funzione che non esiste. Per le challenge, esegui il codice nel modo più leggero possibile, predisponendo una sandbox appena abbastanza realistica da farlo arrivare al risultato. Per gli identificatori dinamici, leggili a runtime e mettili in cache per la sessione. Entrambe le strade richiedono prima di tutto di riconoscere che la prospettiva dell'«istantanea statica» non è più valida.

La scelta tra «purificare o eseguire» è interamente architetturale e dipende da una variabile: il ciclo di cambiamento del target è più breve del tuo periodo di rientro una tantum? Per i target a ciclo breve, in particolare quelli che il server può distribuire in qualsiasi momento, forzare la purificazione produce un rendimento negativo. Affida questa valutazione da entrambi i lati a un livello di ragionamento capace di contraddirsi; non decidere a sensazione e non lasciare che il modello purifichi al posto tuo. La purificazione è engineering deterministico e la sua verifica spetta al differential testing, trattato nelle fasi tre e quattro del workflow in quattro fasi. Qui il modello serve soltanto a valutare una cosa: se valga davvero la pena fare reverse engineering del target.