Documentazione
/
Ricette
/

Nascondere i nomi delle funzioni all'analisi degli LLM

Nascondere i nomi delle funzioni all'analisi degli LLM

Pro

Il problema

Hai attivato vmObfuscation: true, l'hai eseguito su un file che contiene una funzione come validateLicense e hai notato che l'output offuscato contiene ancora il testo letterale validateLicense: il corpo è sparito, sostituito dal bytecode, ma il nome in sé è lì, in bella vista.

JavaScript

Moltiplica questo per una base di codice reale e otterrai un elenco di nomi di funzioni come validateLicense, decryptPayload, processPayment, checkSubscription. Un LLM non ha bisogno di decifrare il bytecode per capire cosa fa il programma: i nomi da soli gli bastano per produrre un riepilogo sicuro e accurato del comportamento del modulo. Il bytecode è opaco; l'indice no.

Perché l'offuscamento VM conserva questi nomi

Con vmTargetFunctionsMode: 'root' (il valore predefinito), l'offuscatore trasforma in bytecode della VM il corpo di ogni funzione di primo livello, ma lascia deliberatamente invariato il nome. Una dichiarazione di funzione di primo livello è, dal punto di vista semantico, un binding nello scope circostante: per uno script si tratta dell'oggetto globale, per un modulo dello scope del modulo (una funzione di primo livello non esportata ha scope di modulo, non globale, ma resta comunque di primo livello nel file). L'offuscatore non può rinominarla in sicurezza perché non ha modo di sapere chi altro vi fa riferimento: un altro bundle, uno <script> inline, un attributo HTML onclick="validateLicense(...)", una ricerca dinamica window['validateLicense'] e così via.

Il compromesso del comportamento predefinito è quindi: proteggere l'implementazione, preservare la superficie pubblica. In questo modo le integrazioni continuano a funzionare, ma un LLM ottiene anche un indice gratuito di tutti i punti di ingresso. Quando succede, il risultato dell'offuscamento segnala un avviso VMGlobalFunctionNamesNotRenamed che elenca i nomi rimasti leggibili.

Perché è importante per il reverse engineering assistito da LLM

Un attaccante umano davanti a qualche centinaio di righe di dispatch del bytecode di solito si arrende. Un LLM a cui viene dato lo stesso file non si prende nemmeno la briga di attaccare il bytecode: legge i nomi, li incrocia con i pochi letterali stringa che riesce a vedere e produce qualcosa del genere:

"Questo modulo controlla l'accesso a una funzionalità a pagamento. validateLicense verifica un token firmato, checkExpiry rifiuta le licenze scadute e activateFeature sblocca l'interfaccia una volta superato il controllo. L'helper di decifratura si trova in decryptPayload."

Questo riepilogo basta a un attaccante per pianificare un aggiramento mirato senza mai toccare la VM. La fuga di informazioni sono i nomi.

La soluzione: racchiudere il codice in una IIFE

Il modo più semplice e robusto per eliminare questa fuga di informazioni è spostare le funzioni sensibili un livello più in profondità nell'albero degli scope. Le funzioni dichiarate all'interno di un'altra funzione non sono di primo livello, quindi l'offuscatore è libero di rinominarle e di assorbire le loro dichiarazioni nel bytecode come qualsiasi altra istruzione.

Una IIFE (Immediately-Invoked Function Expression) è il modo più leggero per farlo: aggiunge un'unica funzione contenitore che viene eseguita una sola volta e non espone nulla per nome.

Prima: nomi esposti

JavaScript

Dopo l'offuscamento VM, sia validateLicense sia checkExpiry sopravvivono per nome nell'output.

Dopo: nomi nascosti dietro una IIFE

JavaScript

Ora entrambe le dichiarazioni di funzione si trovano nel corpo della IIFE. La IIFE stessa è l'unico costrutto di primo livello e una IIFE anonima non ha alcun nome da rivelare. Dopo l'offuscamento VM i nomi delle funzioni vengono rinominati e i corpi vengono convertiti in bytecode.

Cosa è cambiato. Le funzioni non sono più raggiungibili come variabili globali (ed è questo che ne rivelava i nomi). Sono ancora richiamabili, ma solo dall'interno della stessa IIFE. Se qualcosa aveva davvero bisogno di chiamare validateLicense dall'esterno, ora non può più raggiungerla; vedi più avanti il pattern del trampolino. Se nulla lo faceva, non hai perso niente.

I nomi esportati, gli identificatori riservati, le stringhe e il comportamento osservabile possono restare visibili. Testa le integrazioni dopo aver aggiunto la IIFE, in particolare le variabili globali, gli export dei moduli e il codice che esamina i nomi delle funzioni.

Un compromesso da conoscere: se il corpo della IIFE contiene un eval diretto, oppure una chiamata new Function(...) / Function(...) con un corpo dinamico, l'offuscamento VM esclude l'intera funzione e tutto ciò che vi è annidato (con un avviso VMDynamicCodeSkipped), perché il sorgente costruito durante l'esecuzione potrebbe fare riferimento a identificatori rinominati dall'offuscatore. Il codice che hai appena spostato nella IIFE ripiega quindi sull'offuscamento ordinario e perde la protezione del bytecode. Usa l'eval indiretto, (0, eval)(...), oppure consulta L'eval() diretto disattiva l'offuscamento VM per le alternative.

E se una funzione deve davvero essere globale?

A volte una funzione è davvero un punto di ingresso pubblico: un gestore di eventi inline, una callback JSONP, un hook di un SDK di terze parti. Hai due possibilità:

  • Esporre un sottile trampolino e mantenere la logica all'interno della IIFE. Dichiara un piccolo wrapper globale il cui unico compito è chiamare l'implementazione nello scope della IIFE. Il nome del trampolino resta visibile, ma non porta alcuna informazione semantica (chiamalo __entry1 o in modo simile) e tutta la logica significativa resta nascosta.

    JavaScript

  • Riscrivere il punto di chiamata. Se la variabile globale esiste solo perché un gestore inline onclick="validateLicense(...)" ne ha bisogno, sostituisci il gestore inline con addEventListener dall'interno della IIFE. L'HTML smette di nominare la funzione, la funzione non ha più bisogno di essere globale e la fuga di informazioni scompare del tutto.

Inizializzatori di variabili di primo livello: vmWrapTopLevelInitializers

Le dichiarazioni di funzione non sono l'unica cosa che si trova al livello più alto di un file. Gli inizializzatori di variabili di primo livello (costanti stringa, oggetti di configurazione, tabelle di lookup) sono altrettanto leggibili nell'output quando restano semplice JavaScript. Una riga come const API_BASE = '/api/v2/license' dice a un LLM tanto quanto function validateLicense.

L'opzione vmWrapTopLevelInitializers (booleana, predefinita false; gli attuali preset VM la attivano) racchiude in una IIFE gli inizializzatori di primo livello idonei, così che il valore stesso venga calcolato dal bytecode della VM durante l'esecuzione invece di restare nel sorgente come letterale. In modalità root, gli inizializzatori che restano in semplice JavaScript vengono segnalati con un avviso VMTopLevelInitializerNotVirtualized.

Senza l'opzione

JavaScript

Con vmWrapTopLevelInitializers: true

JavaScript

Il nome del binding (MY_STRING) resta di primo livello per lo stesso motivo dei nomi delle funzioni (qualcosa al di fuori del file potrebbe farvi riferimento), ma il valore che contiene viene ora prodotto dalla VM e non compare più come testo leggibile.

È efficace solo quando vmTargetFunctionsMode è 'root' (il valore predefinito) e vmAsyncExecutor è disattivato. In modalità comment l'opzione non ha effetto e non viene segnalato alcun avviso. In modalità root con vmAsyncExecutor (che virtualizza solo le funzioni async) non ha comunque effetto, e la build segnala VMTopLevelInitializerNotVirtualized per gli inizializzatori rimasti in JavaScript semplice.

Se un file non contiene alcuna funzione da virtualizzare per la VM, il risultato segnala VMNoFunctionsToVirtualize; racchiudi il codice che vuoi proteggere in una funzione, oppure usa la modalità comment per selezionare esplicitamente le funzioni sensibili.

Quando non basta

  • Nomi importati da altri moduli. Se esegui il bundling di più file e un modulo esporta validateLicense perché un altro lo importi, il bundler manterrà quel nome visibile nell'output del bundle, così come sono visibili le funzioni di primo livello. Racchiudi il bundle stesso in una IIFE (la maggior parte dei bundler può farlo), oppure sposta l'export all'interno di una IIFE e riesponilo tramite un trampolino dal nome privo di significato.
  • Il runtime resta osservabile. L'offuscamento aumenta lo sforzo necessario per comprendere e modificare il codice, ma non può garantire che il reverse engineering sia impossibile. Mantieni i segreti e le decisioni di sicurezza vincolanti sul server.

Non ricorrere per prima cosa a renameGlobals. Esiste e rinomina gli identificatori di primo livello, ma non ha modo di sapere quali di questi identificatori vengono referenziati dall'esterno del file (altri bundle, HTML inline, ricerche dinamiche). Attivarlo spesso rompe le integrazioni in modi sottili. Racchiudere il codice in una IIFE è più sicuro: non rinomina nulla di globale, smette semplicemente di creare variabili globali di cui non avevi bisogno.