Come proteggere il codice lato client e certificare l'autenticità della raccolta dei dati
Sommario
È risaputo che un'efficiente protezione di applicazioni e siti web richiede l'utilizzo del codice JavaScript per la raccolta dei dati lato client dal browser, che, solitamente, riguardano le caratteristiche di dispositivi e browser, le preferenze degli utenti (fingerprinting) e i dati che riflettono l'interazione degli utenti con i loro dispositivi, come movimenti del mouse, tocchi dello schermo e pressioni dei tasti (telemetria).
I dati vengono elaborati dagli addetti alla sicurezza web e dai vendor con vari metodi di rilevamento (da semplici regole a modelli di AI avanzati) per verificare la probabilità che una richiesta legittima, originata da un dispositivo legittimo, venga controllata da un utente umano.
La combinazione dei diversi dati aiuta anche a distinguere gli utenti e a valutare le loro attività nel corso del tempo. Questo è il principio fondamentale utilizzato nei prodotti di gestione dei bot e rilevamento delle frodi per individuare gli attacchi, come il credential stuffing, il controllo degli account, l'abuso di apertura degli account e lo scraping dei contenuti, solo per citarne alcuni.
Integrità della raccolta dei dati
Garantire l'autenticità e l'integrità dei dati è fondamentale per valutare in modo accurato l'interazione degli utenti con il sito e per segnalare eventuali minacce. Come è possibile affermare l'autenticità e l'integrità dei dati sapendo che tutto ciò che viene eseguito sul lato client può essere manomesso e manipolato?
Durante l'esecuzione del codice JavaScript lato client, sono due i motivi per cui è fondamentale garantire un'adeguata protezione del codice.
1. Il codice JavaScript fa parte della proprietà intellettuale di un'organizzazione e deve essere protetto il più possibile dai criminali e dai concorrenti.
2. L'integrità dei dati è fondamentale per comprendere correttamente l'ambiente e i suoi fattori di rischio. Il codice JavaScript protetto garantisce che i dati siano affidabili perché sono stati raccolti in modo efficace eseguendo lo script e non sono stati manipolati né trasformati.
Come proteggere il codice lato client e garantire l'autenticità dei dati
Come per altri aspetti della sicurezza, nessuna soluzione da sola può risolvere il problema. In questo blog, presenteremo una serie di metodi utilizzati da Akamai per proteggere il codice JavaScript, rafforzare la sua esecuzione e garantire l'autenticità dei dati raccolti con varie tecniche, tra cui:
- Offuscamento del codice
- Controllo dell'integrità dei dati
- Offuscamento della VM
- Inserimento di codice aggiuntivo e fuorviante
- Rotazione del codice JavaScript
- Rotazione del campo dinamico
- Verifica dei dati e pipeline della build di JavaScript
Se la vostra azienda dovesse decidere di seguire pratiche simili per proteggere il suo codice, vi consigliamo di utilizzare una combinazione di questi metodi in base alle esigenze del vostro team, della vostra organizzazione e degli strumenti tecnologici utilizzati.
Offuscamento del codice
L'offuscamento è uno dei metodi più comuni per proteggere il codice JavaScript perché rende più difficile seguire e comprendere il codice.
Alcune valide pratiche di sviluppo consigliano di assegnare a funzioni e variabili nomi il più possibile descrittivi e di strutturare il codice in modo logico per semplificare le operazioni di debug e manutenzione. Anche se si tratta di una pratica che consente di risparmiare una notevole quantità di tempo e fatica, il codice pulito è un obiettivo semplice per il reverse engineering.
Quando viene applicato l'offuscamento, queste valide pratiche di sviluppo vengono interrotte e i nomi descrittivi di variabili/funzioni vengono sostituiti da nomi casuali., che possono essere riordinati e codificati, mentre una parte della logica può essere suddivisa. Un browser web può comunque eseguire il codice senza problemi e i risultati saranno identici. Tuttavia, le cose si complicano per chi prova ad eseguire il reverse engineer del codice.
Gli sviluppatori continuano ad utilizzare il codice ben strutturato per scopi di manutenzione e miglioramento. Una volta disponibile una nuova versione, il codice viene eseguito tramite un motore di offuscamento prima di venire rilasciato. Vari prodotti commerciali e gratuiti/open source, come Code Beautify, JScrambler e Digital.ai, sono disponibili per offuscare rapidamente e facilmente il codice JavaScript.
Nella Figura 1 viene illustrato un esempio di una semplice funzione JavaScript comunemente utilizzata durante il fingerprinting, che è stata progettata per estrarre varie caratteristiche dei dispositivi, mostrate prima dell'offuscamento.
function getDeviceInfo() {
return {
userAgent: navigator.userAgent,
hardwareConcurrency: navigator.hardwareConcurrency || "unknown",
screenOrientation: screen.orientation.type,
};
}
Figura 1. Il codice originale prima dell'offuscamento
Potete vedere come è semplice comprendere il codice nel suo stato originale. Anche un utente con limitate conoscenze nella scrittura di codice può capire lo scopo prefissato e comprendere come raggiunge il suo obiettivo.
Nella Figura 2 viene illustrata la stessa funzione JavaScript una volta eseguita tramite lo strumento online Code Beautify.
(function(_0xbf521e,_0x43c80b){var _0x4ad763=_0x3e09,_0x18fc85=_0xbf521e();while(!![]){try{var_0x40d2a7=parseInt(_0x4ad763(0xfc))/(0x18d1+-0xe6d+-0xa63)+-parseInt(_0x4ad763(0xf6))/(0x2*-0x7e4+0x171a+-0x750)+-parseInt(_0x4ad763(0xfb))/(-0x2e7*-0xb+0x6b*0x1f+-0x2cdf)*(parseInt(_0x4ad763(0xef))/(0x40f*-0x4+-0x897+0x18d7))+-parseInt(_0x4ad763(0xf3))/(0x3*-0xb5f+0x462+0x1dc*0x10)*(parseInt(_0x4ad763(0xf0))/(-0xb87*-0x1+0x18e8+-0x3*0xc23))+-parseInt(_0x4ad763(0xfa))/(0x2258+0x8f7+-0x2b48)*(-parseInt(_0x4ad763(0xee))/(0x3e9+-0xe93+0xab2))+parseInt(_0x4ad763(0xf1))/(0x1*-0x81e+0x525*-0x5+0x4*0x878)+parseInt(_0x4ad763(0xed))/(-0x59*-0x1f+0x779+-0x6f*0x2a);if(_0x40d2a7===_0x43c80b)break;else _0x18fc85['push'](_0x18fc85['shift']());}catch(_0x4460fc){_0x18fc85['push'](_0x18fc85['shift']());}}}(_0x1950,-0x1f*-0x38cb+0x17f2fa+-0x10aebf));function getDeviceInfo(){var _0x7a196=_0x3e09,_0x52340e={'VEDsL':_0x7a196(0xf8)};return{'userAgent':navigator[_0x7a196(0xf4)],'hardwareConcurrency':navigator[_0x7a196(0xf2)+_0x7a196(0xfd)]||_0x52340e[_0x7a196(0xf5)],'screenOrientation':screen[_0x7a196(0xf9)+'n'][_0x7a196(0xf7)]};}function _0x3e09(_0x56cbb3,_0x1167d0){var _0xddc250=_0x1950();return _0x3e09=function(_0x363b57,_0x27d74c){_0x363b57=_0x363b57-(-0x6d9+0x1316*0x1+-0xb50);var _0x1b2eec=_0xddc250[_0x363b57];return _0x1b2eec;},_0x3e09(_0x56cbb3,_0x1167d0);}function _0x1950(){var _0x1d7105=['ncurrency','20162890GviEyp','2488DLGTpn','4rCTHCm','65154TKsGUe','7673175smCphy','hardwareCo','670lOXWEG','userAgent','VEDsL','1749116JlgXKK','type','unknown','orientatio','12971xihUJr','2027775PnQRTc','487370FufNiT'];_0x1950=function(){return _0x1d7105;};return _0x1950();}
Figura 2. Codice offuscato (tramite Code Beautify)
Se non altro per la sua lunghezza, il codice offuscato è chiaramente più difficile da comprendere. Il codice può sembrare complesso, ma i metodi per eludere queste tecniche di offuscamento più semplici esistono e sono ben note ai criminali. Tuttavia, almeno questo aspetto alza l'asticella, scoraggiando quindi i criminali meno sofisticati e meno esperti.
Una vittoria parziale nel campo della sicurezza si ottiene stancando il criminale e/o rendendo poco attraente la prospettiva di prendere di mira un'organizzazione basandosi sullo sforzo percepito o reale che è necessario per sferrare un attacco riuscito..
Controllo dell'integrità dei dati
Come abbiamo visto, l'offuscamento del codice è un buon punto di partenza, ma non è sufficiente di per sé per scoraggiare i criminali più motivati poiché alcuni metodi e strumenti di deoffuscamento consentono di ripristinare il formato originale del codice. Oltre ai metodi di offuscamento, l'implementazione di codice aggiuntivo e delle funzioni di controllo dell'integrità dei dati può ulteriormente proteggere l'integrità delle informazioni raccolte.
I controlli dell'integrità dei dati e del codice sono piccole funzioni aggiunte in vari punti nel codice per verificare che l'output prodotto dallo script sia effettivamente legittimo. Di solito, i controlli utilizzano più variabili, incluso l'output delle principali funzioni JavaScript esistenti insieme con un seed univoco, che è specifico per una sessione utente, in modo da produrre un output secondario.
Nella Figura 3 viene illustrato un esempio di una funzione che prende tre variabili come input, utilizza le variabili all'interno di una semplice formula matematica e una funzione hash, quindi restituisce il risultato. Le variabili a e b possono corrispondere all'output delle due funzioni principali, mentre la variabile c può essere un seed univoco. In questo esempio, tutte le proprietà devono essere valori numerici.
function IntegrityCheck(a, b, c) {
const mathResult = a + b * c;
const stringResult = String(mathResult);
let hash = 0;
for (let i = 0; i < stringResult.length; i++) {
hash = (hash * 31 + stringResult.charCodeAt(i)) >>> 0;
}
return hash;
}
Figura 3. Esempio di codice con più variabili per l'integrità dei dati
Più concretamente, le proprietà screen.colorDepth e navigator.hardwareConcurrency che restituiscono valori numerici possono essere usate come variabili a e b nella semplice funzione illustrata nella Figura 3. Questa funzione non è effettivamente limitata alle proprietà che restituiscono un valore numerico poiché un qualsiasi valore può essere sottoposto ad hash e trasformato in un valore intero prima di passare alla funzione del controllo di integrità. Ed è stato proprio così per il nostro semplice esempio.
Alcune funzioni di controllo dell'integrità possono eseguire l'hash dell'output della funzione principale, come illustrato nell'esempio nella Figura 4.
import { createHash } from 'crypto';
function hashTwoVariables(a, b) {
const concatenatedString = String(a) + String(b);
const hash = createHash('sha256').update(concatenatedString).digest('hex');
return hash;
}
Figura 4. Esempio dell'output dell'hash
Possono esserci dozzine di queste piccole funzioni, ciascuna delle quali esegue diverse operazioni e utilizza diversi output dalle funzioni principali disseminate nel codice per proteggere i dati più importanti. Come controllo finale, potete anche "firmare" l'intero payload, inclusi tutti i dati relativi al fingerprinting e ai comportamenti, nonché i risultati delle singole funzioni di controllo dell'integrità. Un modo per eseguire questa operazione consiste nel sottoporre ad hash l'intero payload e confrontare l'output iniziale. Se gli hash dei dispositivi di invio e ricezione corrispondono, il payload viene considerato sicuro e non alterato.
Offuscamento della VM
Queste semplici funzioni di controllo dell'integrità non possono essere lasciate visibili né nascoste tramite semplici metodi di offuscamento. È a questo punto che entra in gioco la tecnica di offuscamento della macchina virtuale (VM) più avanzata, che rende più difficile per i criminali comprendere cosa succede "nell'ombra" e come produrre un payload valido.
L'offuscamento della VM trasforma il codice a byte della macchina virtuale in qualcosa che una macchina può interpretare, ma un po' più complicato per i criminali da sottoporre a reverse engineer.
Diversi vendor offrono metodi di offuscamento della VM, che, tuttavia, non sempre supporta tutti i tipi di logica delle funzioni. Durante l'utilizzo dell'offuscamento della VM, attenetevi alle linee guida del vostro vendor ed eseguite i test di regressione del codice.
I test di regressione sono un'ottima pratica in generale, non solo per l'offuscamento della VM e vale la pena implementarli come parte delle routine di sicurezza. Sono particolarmente utili, tuttavia, in combinazione con l'offuscamento della VM, considerando l'output complesso del codice del metodo.
Inserimento di codice aggiuntivo e fuorviante
Per rendere più complicato ai criminali tentare di eseguire il reverse engineering del codice, un ulteriore livello implica l'aggiunta di codice senza uno scopo reale per la logica principale con l'intento di condurre i criminali fuori strada, di farli stressare e di indurli ad abbandonare i loro piani.
Analogamente, potete pensare di variare la struttura delle funzioni di controllo dell'integrità per rendere il deoffuscamento e il reverse engineering più complicati. Un modo per eseguire questa operazione è sviluppare diverse funzioni strutturalmente distinte, ma equivalenti, che producono lo stesso output.
Una funzione identica dal punto di vista funzionale, ma strutturalmente diversa determina una codifica della funzione differente una volta sottoposta all'offuscamento della VM, rendendo molto più difficile eseguire il reverse engineering del codice.
Nella Figura 5 viene illustrato un esempio di tre funzioni di questo tipo che restituiscono sempre lo stesso output, ma sono leggermente diverse.
function IntegrityCheck_1(a, b) {
return a + b * 1;
}
function IntegrityCheck_2(a, b) {
return a + 0 + b;
}
function IntegrityCheck_3(a, b, c) {
return a + b + c * 0;
}
Figura 5. Tre esempi di codice diverso che restituisce lo stesso output
Rotazione del codice JavaScript
Disporre di codice fuorviante, un offuscamento avanzato e i controlli di integrità implementati è ottimo, tuttavia, i criminali possono essere molto persistenti e nessun codice stagnante è impossibile da sottoporre a reverse engineer con il giusto livello di tempo, impegno e competenze, a meno che non limitiamo la validità dello script.
Immaginate di generare migliaia di iterazioni univoche dello stesso codice equivalente da un punto di vista funzionale, ciascuna delle quali con diverse funzioni di controllo dell'integrità implementate per ogni nuova release del codice JavaScript. Ogni iterazione viene solo utilizzata ed è valida per 10 - 20 minuti e vengono attuati i controlli necessari per forzare il client a ricaricare regolarmente una nuova iterazione, il che rende le iterazioni precedenti subito obsolete e non più valide.
L'obiettivo di questo metodo è sovraccaricare il criminale con un'eccessiva complessità e superare la sua efficienza in modo da non lasciargli altra scelta, se non quella di eseguire il codice JavaScript tramite un browser senza sapere cosa fa il codice.
Rotazione del campo dinamico
Il codice può essere difficile da leggere e da decifrare, ma, spesso, si può dedurre il suo scopo esaminando l'output insieme ai dati raccolti e inviati. Alcune delle informazioni inviate al server potrebbero sembrare banali, specialmente quelle relative a dati come le caratteristiche del dispositivo e del browser.
Tuttavia, potrebbe risultare più difficile dedurne lo scopo per quelle funzioni che restituiscono semplicemente un valore booleano o per una funzione di controllo dell'integrità che restituisce un numero intero.
Per rendere la struttura del payload meno prevedibile e meno chiara per i criminali, è possibile cambiare i nomi dei campi utilizzati per segnalare tutti i dati raccolti, nonché le relative posizioni nel payload per ogni iterazione.
Come abbiamo discusso, ogni iterazione JavaScript presenta una serie univoca di controlli dell'integrità del codice. Inoltre, il payload utilizza diversi nomi per i campi e la posizione di uno specifico dato cambia con ogni iterazione.
I nomi dei campi e le loro posizioni sono definiti al momento della creazione di JavaScript in base ad un algoritmo predefinito che anche il server che elabora i dati può eseguire per recuperare le varie informazioni critiche per un accurato rilevamento di bot e frodi nella posizione corretta.
Nella Figura 6 viene mostrato come ciascun campo e la sua posizione possono variare da un'iterazione all'altra. I nomi dei campi non devono essere descrittivi per risultare meno semplici da comprendere.
Payload Iteration #1
mx01: [user-agent]
mx02: [display-mode]
mx03: [hardconcur]
mx04: [pixelDepth]
mx05: [language]
mx06: [WebGL_Rend]
mx07: [intg_chck_1]
Payload Iteration #2
yw01: [display-mode]
yw02: [intg_chck_1]
yw03: [user-agent]
yw04: [pixelDepth]
yw05: [hardconcur]
yw06: [WebGL_Rend]
yw07: [language]
Payload Iteration #3
za01: [language]
za02: [WebGL_Rend]
za03: [hardconcur]
za04: [pixelDepth]
za05: [intg_chck_1]
za06: [user-agent]
za07: [display-mode]
Figura 6. Esempi di iterazioni dei nomi dei campi
Con soli sette campi nell'output (come illustrato nell'esempio precedente), è facile individuare la modifica apportata da un'iterazione all'altra, ma immaginate di doverlo fare nel caso in cui vengano raccolti e restituiti centinaia di dati.
Verifica dei dati e pipeline della build di JavaScript
I vari metodi utilizzati per proteggere il codice JavaScript e per garantire l'integrità dei dati raccolti richiede lo sviluppo di un processo complesso di release e di una pipeline di creazione. Innanzitutto, gli sviluppatori aggiornano il file JavaScript non elaborato e formattato correttamente, verificano le funzionalità ed eseguono i test di regressione,
quindi usano un algoritmo per generare migliaia di iterazioni, che produce versioni univoche, ciascuna con i seguenti componenti diversi:
- Funzioni di controllo dell'integrità dei dati che variano i dati dal codice JavaScript principale, dalle funzioni matematiche/hash utilizzate e dalla loro relativa posizione nella logica complessiva
- Set di codici fuorvianti o non utilizzati
- Nomi dei campi di output del payload
- Ordine dei campi di output del payload
Una volta generati questi componenti univoci, l'iterazione del file JavaScript avvia i seguenti processi:
- Offuscamento del controllo dell'integrità dei dati e altre funzioni critiche tramite la VM
- Offuscamento del codice complessivo
- Caricamento dell'iterazione del server web
Una volta generate e caricate tutte le iterazioni, il nuovo set JavaScript deve essere avviato alla fase di produzione. Questa modifica viene coordinata con il server che esegue il motore di rilevamento di bot e frodi che riceve i dati. Questa operazione viene eseguita come parte dell'algoritmo utilizzato nel sistema di creazione JavaScript per consentire di:
- Verificare che il client stia inviando il payload dell'iterazione JavaScript corrente e non un'iterazione obsoleta
- Analizzare i diversi campi del payload in base all'iterazione JavaScript con cui è stato generato
- Verificare i valori del controllo di integrità del codice eseguendo funzioni equivalenti
Il prodotto finale, con l'offuscamento finale, deve essere accuratamente verificato dall'inizio alla fine nella fase precedente alla produzione prima della release per garantire che tutti i componenti siano sincronizzati e producano il risultato atteso. Pertanto, è necessario progettare un workflow di creazione alquanto complesso per JavaScript
allo scopo di preservare il suo contenuto da curiosi concorrenti e dai criminali e perché il suo output influisce sulla sicurezza degli utenti su Internet e sui siti web che visitano.
Conclusione
È fondamentale proteggere il codice JavaScript eseguito sul lato client ed utilizzato per raccogliere i dati di fingerprinting e telemetria, nonché la logica personalizzata che è stata progettata per rilevare bot e frodi. Sono disponibili varie strategie per proteggere il codice e i dati, tuttavia l'implementazione di un paio di strategie fornisce solo una protezione marginale dai criminali più sofisticati.
Proteggere il codice lato client e il relativo payload richiede una strategia complessa che riguarda più livelli di difesa e tecnologie, tra cui offuscamento del codice, codice fuorviante o non utilizzato, funzioni di controllo dell'integrità del codice insieme all'offuscamento della VM, randomizzazione della struttura del payload per renderla meno prevedibile e aggiornamenti regolari del codice.
L'equazione mostrata nella Figura 7 riepiloga la difficoltà legata alla combinazione complessiva delle strategie necessarie per sviluppare una protezione efficiente.
[JS Code obfuscation[
+ Misleading code
+ unused code
+ VM Obfuscation [code integrity check]
+ unique field names
+ field relative position shift]
x [Number of unique iterations]
+ Limited version validity (10 minutes)
+ Force JS reload]
Figura 7. Equazione delle strategie di protezione JavaScript
Infine, questa combinazione forza il client ad eseguire il codice JavaScript, riducendo l'opportunità di manomettere i dati e di sconfiggere il motore di rilevamento. Per limitare la difficoltà dello sviluppo, l'utilizzo di soluzioni commerciali è vivamente consigliato per alcune delle fasi più complesse, come l'offuscamento della VM. Alcune strategie, tuttavia, come i controlli dell'integrità del codice, gli snippet di codice fuorviante e le iterazioni multiple, devono essere progettate e mantenute in-house per fornire un'adeguata protezione nel caso in cui uno strumento di deoffuscamento venga creato dai criminali.