Documentazione
/
Ricette
/

Offuscamento VM con eval e new Function

Offuscamento VM con eval e new Function

Come l'offuscamento VM gestisce la costruzione dinamica di codice (eval diretto e il costruttore Function), che cosa viene convertito in bytecode e che cosa viene escluso, quali avvisi emette l'offuscatore e come diagnosticare i ReferenceError a runtime.

Perché è importante

L'offuscamento VM compila i corpi delle funzioni in bytecode, eseguito da un interprete integrato nel runtime. Anche gli identificatori dello scope circostante vengono rinominati. Entrambe le trasformazioni interagiscono male con il codice costruito a partire da una stringa in fase di esecuzione: eval(s), new Function(...s) e Function(...s). Se il codice costruito a runtime fa riferimento a un identificatore rinominato dall'offuscatore, alla prima esecuzione della funzione generata si ottiene Uncaught ReferenceError: <renamed-name> is not defined.

L'offuscatore gestisce ciascun pattern in modo diverso. La matrice seguente ne offre una sintesi; il resto della pagina illustra ogni singola voce.

Che cosa fa l'offuscatore, in sintesi

Pattern presente nel sorgenteChe cosa accade
eval('literal string') (il corpo è un letterale stringa)Funziona correttamente. La funzione che contiene questa chiamata perde la conversione in bytecode VM (eval diretto legge le variabili locali circostanti, che la VM non conserva una volta compilata la funzione in bytecode).
eval(dynamicExpression)Può generare un ReferenceError a runtime. Anche la funzione che contiene questa chiamata — e ogni funzione definita al suo interno — perde la conversione in bytecode VM.
(0, eval)(s) / window.eval(s) (indiretto)Funziona correttamente. La funzione che contiene questa chiamata viene normalmente convertita in bytecode VM. L'eval indiretto non vede le variabili locali circostanti, per cui la rinomina non può comprometterlo.
new Function('a', 'b', 'return a + b') (tutti gli argomenti sono letterali stringa)Funziona correttamente. La funzione che contiene questa chiamata viene normalmente convertita in bytecode VM.
new Function(dynamicBody) / Function(dynamicBody)Può generare un ReferenceError a runtime. Anche la funzione che contiene questa chiamata — e ogni funzione definita al suo interno — perde la conversione in bytecode VM.

Tutti questi pattern vengono inoltre segnalati come avvisi non fatali sul risultato dell'offuscamento: si veda Rilevare il problema prima dell'esecuzione più avanti per la struttura degli avvisi e uno snippet per la CI.

Perché forma statica e dinamica vengono trattate diversamente

eval(s) può leggere e scrivere le variabili locali della funzione in cui viene invocato. Quando s è un letterale stringa, l'offuscatore può analizzarne il corpo in fase di offuscamento e rinominare gli identificatori in modo coerente con il codice circostante. Quando invece s è un'espressione dinamica, l'analisi avviene soltanto a runtime, quando gli identificatori sono già stati rinominati: il sorgente costruito a runtime fa quindi riferimento a nomi obsoleti che non esistono più.

new Function(s) funziona diversamente: il corpo viene sempre eseguito come se fosse definito all'inizio del file, con accesso alle sole variabili globali e mai alle variabili locali che circondano la chiamata. Di per sé si tratta di una situazione sicura, ma se il corpo viene costruito concatenandovi un identificatore rinominato (ad esempio tramite func.toString() di una funzione le cui parti interne sono state riscritte dall'offuscatore), la funzione compilata a runtime incorrerà comunque nello stesso tipo di ReferenceError.

La forma statica new Function('return 42') non comporta mai questo rischio: il corpo è una semplice stringa che il meccanismo di rinomina non esamina mai e, a runtime, deve poter accedere soltanto alle variabili globali. L'offuscatore lascia la chiamata invariata e la funzione circostante resta idonea alla conversione in bytecode VM.

L'errore visibile a runtime

Il sintomo più comune è un ReferenceError alla prima esecuzione della funzione costruita dinamicamente:

browser console

Errore

In questo caso TU è un identificatore rinominato che l'offuscatore ha introdotto nello scope della IIFE del bundle. La chiamata dinamica a eval o al costruttore Function valuta un corpo che vi fa riferimento, ma tale corpo viene eseguito in uno scope in cui TU non è definito.

Rilevare il problema prima dell'esecuzione

L'offuscatore emette avvisi non fatali tramite l'API, così da poter intercettare questi pattern in CI prima della distribuzione. Sono rilevanti due tipi di avviso:

  • DynamicCodeRenameRisk — una funzione contiene una chiamata dinamica a eval / new Function / Function il cui corpo viene costruito a runtime.
  • VMDynamicCodeSkipped — la conversione in bytecode VM è stata esclusa per una funzione a causa di uno dei pattern precedenti. L'avviso include il nome della funzione (se disponibile) e il tipo di costrutto che ha determinato l'esclusione.

ci-build.mjs

JavaScript

Se un determinato tipo di avviso è atteso nella propria build e si preferisce silenziarlo all'origine anziché filtrarlo in CI, l'opzione warnings (v7.8.0+) controlla ciò che getWarnings() emette: 'none' sopprime qualsiasi avviso, mentre una mappa per tipo come { VMDynamicCodeSkipped: false } silenzia un solo tipo mantenendo tutti gli altri.

Soluzioni alternative

  • Passare all'eval indiretto ((0, eval)(s))

    Utile soltanto nel caso di eval. L'eval indiretto viene eseguito nello scope globale e pertanto non vede le variabili locali circostanti, ma proprio per questo motivo non può nemmeno fare riferimento a identificatori rinominati. La funzione che contiene la chiamata resta convertita in bytecode VM.

  • Rendere il corpo completamente statico

    Nel caso di new Function, se è possibile esprimere il corpo come un singolo letterale stringa o template literal privo di interpolazioni, la chiamata non comporta alcun rischio di rinomina e la funzione che la contiene continua a essere convertita in bytecode. new Function('a', 'b', 'return a + b') va bene; new Function('return ' + expr) no.

  • Spostare la chiamata in una funzione di primo livello dedicata e rimuovere la IIFE

    Ogni funzione di primo livello viene valutata in modo indipendente. Isolare la chiamata di costruzione dinamica del codice in una funzione di primo livello dedicata fa sì che soltanto quella funzione perda la conversione in bytecode VM, evitando che l'esclusione si propaghi attraverso una IIFE di primo livello che racchiude l'intero bundle.

  • Passare a vmTargetFunctionsMode: 'comment'

    Modalità a inclusione esplicita: vengono convertite in bytecode soltanto le funzioni contrassegnate con /* javascript-obfuscator:vm */. È sufficiente non annotare la funzione che contiene la chiamata dinamica e convertire tutto il resto. Si veda Targeting delle funzioni.

  • Forzare la conversione con vmForceCompileDynamicCode: true (v6.14.0+)

    Via di uscita di ultima istanza. Quando è attiva, l'offuscatore converte comunque in bytecode la funzione circostante e sopprime l'avviso VMDynamicCodeSkipped. Da utilizzare soltanto quando si ha la certezza che il corpo costruito a runtime non faccia mai riferimento a un identificatore rinominato dall'offuscatore, altrimenti si baratta un offuscamento pulito con un ReferenceError in fase di esecuzione. DynamicCodeRenameRisk continua comunque a essere emesso, così che la CI possa proseguire i propri controlli. Nella dashboard corrisponde all'interruttore "Force Compile Dynamic Code" nel gruppo Overrides della sezione VM.

Pagine correlate