
Kit di reverse engineering per la VM bytecode di PerimeterX, con un disassemblatore basato su CFG, pipeline di decrittazione a 5 livelli, ricostruzione della tabella degli opcode e pulitore di emulazione stack per la ricerca sulla sicurezza nel fingerprinting del rilevamento bot.
Questo repository documenta il reverse engineering di auditor.js di PerimeterX, una macchina virtuale basata su bytecode utilizzata come livello secondario di fingerprinting nella pipeline di rilevamento bot di PX. Questa analisi copre:
Nota: Questo repository copre solo una versione (statica) della VM ed è inteso per scopi di ricerca sulla sicurezza e analisi. Non include solver dinamici o implementazioni di solver per produzione.
Giovedì 2 aprile 2026, PerimeterX ha distribuito una nuova VM basata su bytecode come parte della loro pipeline di rilevamento bot.
auditor.js non assomiglia a un normale script sensore PX. Invece delle solite ricerche di proprietà offuscate e funzioni di raccolta:
_fg0 a _fg7), il programma VM crittato suddiviso tra variabili_dp) con una chiave per sito (_pk) che decomprime il JSON del programma_0x8df7) che esegue l'hash del codice sorgente della VM per derivare una chiave di decrittazione, in modo che qualsiasi modifica rompa silenziosamente la decrittazione del bytecodeIl programma VM è suddiviso in 8 variabili, concatenate e poi decrittate tramite _dp() usando un cifrario XOR per sito con chiave _pk:
var _pk = 893686289;
function _dp(_b) {
var _r = atob(_b), _o = new Array(_r.length);
for (var _i = 0; _i < _r.length; _i++) {
_o[_i] = String.fromCharCode(
_r.charCodeAt(_i) ^ (((_pk >>> (8 * (_i % 4))) ^ Math.imul(_i + 1, 0x6B8B4567)) & 0xFF)
);
}
return _o.join("");
}
Il risultato è un oggetto JSON con nomi di chiave offuscati di due caratteri (es. "uo" per seed, "dk" per nonce). Una tabella di mappatura li converte in nomi standard.
node extractor.js
# -> program.json
| Campo | Descrizione |
|---|---|
s | Seed (12755), guida tutte le operazioni crittografiche |
n | Nonce (1603730985), randomizzazione per programma |
g | Flag generatore, abilita il livello di decrittazione con hash di integrità |
x | Flag crittato, le costanti sono crittate con XOR |
c | Pool di costanti, 1230 voci |
f | Funzioni, 112 voci con bytecode crittato |
e | Punto di ingresso, indice funzione 0 |
Tutte le 1095 stringhe costanti sono crittate con due livelli:
Livello 1: Murmur XOR statico con chiave 4008000571, dipendente dalla posizione.
Livello 2: XOR del flusso PRNG utilizzando un LCG di glibc, inizializzato combinando il seed del programma con l'indice di ogni costante tramite l'hash moltiplicativo di Knuth.
Prima della decrittazione, il seed viene XORato con un impronta ambientale (_0xaf48), una maschera di bit a 8 bit calcolata sondando le API del browser:
| Bit | Test | Chrome |
|---|---|---|
| 0 | typeof window.matchMedia === "function" | 1 |
| 1 | document.elementFromPoint esiste | 1 |
| 2 | typeof window.requestAnimationFrame === "function" | 1 |
| 3 | typeof window.getComputedStyle === "function" | 1 |
| 4 | CSS.supports esiste | 1 |
| 5 | navigator.sendBeacon esiste | 1 |
| 6 | document.execCommand esiste | 1 |
| 7 | process.versions.node esiste (Node.js) | 0 |
Per Chrome: _0xaf48 = 0b01111111 = 127, dando seed effettivo = 12755 ^ 127 = 12716.
Ciò significa che lo stesso programma produce risultati di decrittazione diversi in ambienti diversi. Eseguirlo in Node.js vs Chrome vs Firefox produce seed diversi.
node decrypt_constants.js
# -> program_decrypted.json, constants_table.txt
Le stringhe decrittate ci dicono esattamente cosa la VM sta raccogliendo come fingerprint:
Fingerprinting del browser: screenWidth, screenHeight, innerWidth, innerHeight, devicePixelRatio, colorDepth, platform, userAgent, language, timezone, timezoneOffset, forcedColors, highContrast
Tempi di performance: navigationStart, domComplete, domLoading, fetchStart, requestStart, responseEnd, secureConnectionStart, serverTiming
Crittografia RSA: BigInt, modPow, AQAB (65537 in base64), modulusLength, shiftLeft, shiftRight, getRandomValues
Sondaggio DOM/SVG: http://www.w3.org/2000/svg, createElementNS, getBoundingClientRect, getTotalLength, getBBox
Nomi dei campi PX: mtr, tst, mst, enc, sbx, fstec, pdc, prb, wvi, wva, pti, dis, los, cv, sc, jd, ads, enve, init
Riferimenti endpoint: https://fst-ec.perimeterx.net/?id=
Anti-debugger: _CMP_RCX_07;_JNZ_0x0A_EB_CC, CC|CD-04|BREAKPOINT-005
107 opcode base che coprono l'intero linguaggio JavaScript, più rumore generato dinamicamente:
40 honey opcode sono implementazioni alternative di operazioni aritmetiche/di confronto che utilizzano espressioni matematicamente equivalenti ma sintatticamente diverse. ADD potrebbe apparire come (a^b) + 2*(a&b) o -((-a)-b) o a-(-b). Ogni opcode base può avere fino a 3 varianti, generate deterministicamente dal seed. Una semplice istruzione ADD può apparire come 4 diversi valori di bytecode all'interno dello stesso programma, rompendo gli approcci di pattern-matching.
24 padding opcode sono allocati nella permutazione ma non hanno gestori e non vengono mai emessi. Esistono per espandere lo spazio degli opcode e rendere più difficile invertire lo shuffle.
16 gruppi di superistruzioni sono la caratteristica anti-analisi più importante. Quando il ciclo di dispatch risolve un opcode in un leader di superistruzione, il gestore legge un byte aggiuntivo dal flusso di bytecode e lo invia a un sotto-gestore. Il sotto-gestore può essere un'operazione completamente diversa:
| Leader si risolve come | Sotto-byte | Effettivamente esegue |
|---|---|---|
FOR_IN_NEXT | 74 | FOR_IN_NEXT |
FOR_IN_NEXT | 100 | MAKE_CLOSURE |
ASSIGN_OP_VAR | 165 | ASSIGN_OP_VAR |
ASSIGN_OP_VAR | 37 | JMP |
GET_VAR_PROP_C | 143 | SET_VAR_POP |
GET_VAR_PROP_C | 23 | JMP_NULLISH |
La tabella degli opcode viene mescolata tramite Fisher-Yates inizializzato con il seed effettivo, quindi i valori del bytecode differiscono per build.
node build_opcodes.js
# -> opcode_table.json, opcode_table.txt
Il nucleo del toolkit. cfg.js costruisce un grafo di flusso di controllo seguendo tutti i percorsi di esecuzione da PC=0, decodificando ogni istruzione con il corretto contesto di crittazione.