Offuscamento VM con eval e new Function
Come l'offuscamento VM gestisce la costruzione dinamica del codice (eval diretto e costruttore Function), cosa viene convertito in bytecode e cosa viene escluso, quali avvisi emette l'offuscatore e come diagnosticare un ReferenceError durante l'esecuzione.
Perché è importante
L'offuscamento VM compila i corpi delle funzioni in bytecode eseguito da un interprete incluso nel runtime. Anche gli identificatori
dello scope circostante vengono rinominati. Entrambe le trasformazioni interagiscono male con il codice costruito a partire da una stringa durante
l'esecuzione: eval(s), new Function(...s) e Function(...s). Se il codice costruito durante l'esecuzione fa riferimento a un identificatore che
l'offuscatore ha rinominato, ottieni Uncaught ReferenceError: <renamed-name> is not defined la prima volta che la funzione generata
viene eseguita.
L'offuscatore gestisce ogni schema in modo diverso. La tabella seguente è la versione breve; il resto della pagina spiega ogni riga.
Cosa fa l'offuscatore, in sintesi
| Schema nel tuo sorgente | Cosa succede |
|---|---|
eval('literal string') (il corpo è un letterale stringa) | Funziona correttamente (a meno che vmForceCompileDynamicCode non sia attivo, vedi Soluzioni alternative). La funzione che contiene questa chiamata, e ogni funzione definita al suo interno, non viene convertita in bytecode della VM (l'eval diretto legge le variabili locali circostanti, che la VM non preserva una volta che una funzione è compilata in bytecode). |
eval(dynamicExpression) | Può fallire durante l'esecuzione con un ReferenceError. Anche la funzione che contiene questa chiamata, e ogni funzione definita al suo interno, non viene convertita in bytecode della VM. |
(0, eval)(s) / window.eval(s) (indiretto) | La funzione che contiene questa chiamata viene convertita normalmente in bytecode della VM. L'eval indiretto viene eseguito nello scope globale e non vede le variabili locali circostanti, quindi le variabili locali rinominate non possono romperlo. Può comunque fallire se il codice valutato fa riferimento a una variabile globale rinominata o rimossa. |
new Function('a', 'b', 'return a + b') (tutti gli argomenti sono letterali stringa) | Funziona correttamente. La funzione che contiene questa chiamata viene convertita normalmente in bytecode della VM. |
new Function(dynamicBody) / Function(dynamicBody) | Può fallire durante l'esecuzione con un ReferenceError. Anche la funzione che contiene questa chiamata, e ogni funzione definita al suo interno, non viene convertita in bytecode della VM. |
Il compilatore rileva inoltre in modo conservativo eval?.(code) e lo gestisce come un eval diretto. JavaScript definisce questa
forma di chiamata opzionale come eval indiretto; il rilevamento del compilatore non cambia il comportamento del linguaggio.
Solo alcuni di questi schemi producono avvisi non bloccanti, e solo quando la chiamata si trova all'interno di una funzione: una chiamata dinamica
a eval, new Function o Function segnala sia DynamicCodeRenameRisk sia VMDynamicCodeSkipped
(VMDynamicCodeSkipped solo per una funzione che la VM compilerebbe altrimenti: non esclusa e, in modalità comment, marcata;
per una funzione annidata indica l'antenato di primo livello che è stato saltato), un eval('...') statico
segnala solo VMDynamicCodeSkipped, mentre l'eval indiretto e un new Function completamente statico non segnalano nulla. Una
chiamata dinamica al livello superiore di un file, fuori da qualsiasi funzione, non viene mai segnalata. Consulta
Rilevare il problema prima dell'esecuzione più avanti per i tipi di avviso e un esempio per la CI.
L'esclusione si propaga attraverso le IIFE. Se una IIFE di primo livello racchiude l'intero bundle e una qualsiasi funzione al suo interno usa
un eval diretto (statico o dinamico) o una chiamata dinamica new Function / Function, l'intera IIFE viene esclusa dalla conversione in bytecode della VM. Consulta
L'eval() diretto disattiva l'offuscamento VM per lo schema di rimozione della IIFE che limita la portata
del danno.
Perché statico e dinamico vengono trattati diversamente
eval(s) può leggere e scrivere le variabili locali della funzione in cui viene chiamato. Quando s è un letterale stringa,
l'offuscatore può analizzare il corpo in fase di offuscamento e rinominare gli identificatori in modo coerente con il codice circostante. Quando
s è un'espressione dinamica, l'analisi avviene solo durante l'esecuzione, quando gli identificatori sono già stati
rinominati, quindi il sorgente costruito durante l'esecuzione fa riferimento a vecchi nomi che non esistono più.
new Function(s) e l'eval indiretto funzionano diversamente: il corpo viene sempre eseguito nello scope globale, con accesso solo alle variabili
globali e mai a quelle locali attorno alla chiamata. Non possono dipendere da variabili locali rinominate, ma possono comunque fallire se
fanno riferimento a variabili globali rinominate o rimosse, oppure se costruisci il corpo concatenandovi un identificatore rinominato
(ad esempio tramite func.toString() di una funzione il cui contenuto è stato riscritto dall'offuscatore).
Un corpo statico che usa solo i propri parametri, come new Function('a', 'b', 'return a + b'), non ha questa
dipendenza: il corpo è una semplice stringa che il meccanismo di rinomina non esamina mai. L'offuscatore lascia la chiamata così com'è e la
funzione circostante resta idonea alla conversione in bytecode della VM. Cambiare soltanto la sintassi dell'eval non rende sicuro un codice
dinamico arbitrario: controlla gli avvisi e testa il bundle finale.
L'errore che vedi durante l'esecuzione
Il sintomo tipico è un ReferenceError la prima volta che viene eseguita la funzione costruita dinamicamente:
Qui TU è un identificatore rinominato che l'offuscatore ha introdotto nello scope della IIFE del bundle. La chiamata dinamica a eval /
costruttore Function valuta un corpo che vi fa riferimento, ma il corpo viene eseguito in uno scope in cui TU non è definito.
Rilevare il problema prima dell'esecuzione
L'offuscatore emette avvisi non bloccanti tramite l'API, così puoi individuare alcuni di questi schemi in CI prima della distribuzione.
Due tipi di avviso sono rilevanti. Entrambi vengono segnalati solo per le chiamate all'interno di una funzione, tranne il caso di .toString(),
che viene segnalato ovunque si trovi la chiamata quando la funzione serializzata è annidata in un'altra funzione o non può essere
risolta (solo build VM):
DynamicCodeRenameRisk- una funzione costruisce codice a partire da una stringa durante l'esecuzione: unevaldiretto o una chiamatanew Function/Functionil cui corpo non è statico, oppurefn.toString()iniettato in uno<script>o in un Worker. È l'avviso che preannuncia unReferenceError.VMDynamicCodeSkipped- la conversione in bytecode della VM è stata saltata per una funzione perché contiene unevaldiretto o una chiamata dinamicanew Function/Function. In modalità root (quella predefinita) viene saltata anche ogni funzione definita al suo interno; in modalità comment una funzione annidata che marchi separatamente viene comunque virtualizzata, a meno che non contenga essa stessa la chiamata. Scatta anche per uneval('literal')statico e sicuro, quindi segnala una perdita di protezione VM piuttosto che un crash durante l'esecuzione. Include il nome della funzione (se disponibile) e il costrutto che ha causato l'esclusione.
Il pacchetto npm javascript-obfuscator non espone gli avvisi nel suo risultato, quindi un controllo in CI li legge dalla risposta
dell'API: i messaggi result e chunk_end contengono un array warnings. L'esempio seguente usa
readObfuscationResponse(), il lettore dello stream presente nel Riferimento API, e fa fallire la build solo per
DynamicCodeRenameRisk; aggiungi VMDynamicCodeSkipped al filtro se anche la perdita della protezione VM su una funzione deve
bloccare il rilascio.
Se un tipo di avviso è previsto nella tua build e preferisci silenziarlo alla fonte invece di filtrarlo in CI,
l'opzione warnings (v7.8.0+) controlla quali avvisi vengono emessi: 'none' sopprime tutto, mentre una mappa per tipo
come { VMDynamicCodeSkipped: false } silenzia un solo tipo mantenendo gli altri.
Soluzioni alternative
Passa all'eval indiretto (
(0, eval)(s))Utile solo nel caso di
eval. L'eval indiretto viene eseguito nello scope globale, quindi non vede le variabili locali circostanti e, per lo stesso motivo, non può nemmeno fare riferimento a variabili locali rinominate. La funzione che contiene la chiamata resta convertita in bytecode della VM. Il codice valutato non deve comunque dipendere da variabili globali rinominate dall'offuscatore.Rendi il corpo completamente statico
Per
new Function, se puoi esprimere il corpo come un unico letterale stringa o template literal senza interpolazioni, la chiamata non comporta rischi di rinomina e la funzione che la contiene viene comunque convertita in bytecode.new Function('a', 'b', 'return a + b')va bene;new Function('return ' + expr)no.Sposta la chiamata in una funzione di primo livello dedicata ed elimina la IIFE
Ogni funzione di primo livello viene verificata in modo indipendente. Spostare la chiamata con codice dinamico in una propria funzione di primo livello significa che solo quella funzione perde la conversione in bytecode della VM, invece di propagare l'esclusione a una IIFE di primo livello che racchiude l'intero bundle.
Passa a
vmTargetFunctionsMode: 'comment'Modalità su richiesta: vengono convertite in bytecode solo le funzioni contrassegnate con
/* javascript-obfuscator:vm */. Non annotare la funzione che contiene la chiamata con codice dinamico e contrassegna le altre. Conserva questi commenti fino alla fase di offuscamento. Consulta Targeting di funzioni specifiche.Forza l'inclusione con
vmForceCompileDynamicCode: true(v6.14.0+)Via di fuga di ultima istanza. Quando è attiva, l'offuscatore converte comunque in bytecode la funzione circostante e sopprime l'avviso
VMDynamicCodeSkipped. Non può riparare lo scope: all'interno di una funzione compilata forzatamente, l'evaldiretto non può leggere né scrivere le variabili locali o i parametri della funzione, né le variabili di una funzione racchiudente anch'essa virtualizzata, anche quando il codice è un letterale stringa. Le letture lancianoReferenceErrore le scritture vanno perse silenziosamente. Uneval('...')statico in questa situazione non segnala alcun avviso, quindi un controllo in CI non lo intercetta;DynamicCodeRenameRiskviene comunque emesso solo per un corpo dinamico. Usala solo quando il codice valutato non fa riferimento ad altro che a variabili globali. Nella dashboard corrisponde all'interruttore Force Compile Dynamic Code nel gruppo Override della sezione VM.
Pagine correlate
- L'eval() diretto disattiva l'offuscamento VM - lo schema di rimozione della IIFE per limitare la portata dell'esclusione.
- Targeting di funzioni specifiche - usare
vmTargetFunctionsModeper includere o escludere singole funzioni.
