Kit de ingeniería inversa para la máquina virtual de bytecode de PerimeterX, que incluye un desensamblador basado en CFG, un pipeline de descifrado de 5 capas, reconstrucción de tabla de códigos de operación y un limpiador de emulación de pila para la investigación de seguridad sobre la huella digital de detección de bots.
Este repositorio documenta la ingeniería inversa de auditor.js de PerimeterX, una máquina virtual de bytecode utilizada como capa secundaria de huellas digitales en el pipeline de detección de bots de PX. Este análisis cubre:
Nota: Este repositorio cubre solo una versión (estática) de la VM y está destinado a fines de investigación y análisis de seguridad. No incluye solucionadores dinámicos ni implementaciones de solucionadores de producción.
El jueves 2 de abril de 2026, PerimeterX implementó una nueva VM de bytecode como parte de su pipeline de detección de bots.
auditor.js no parece un script de sensor PX normal. En lugar de las habituales búsquedas de propiedades ofuscadas y funciones de recolección:
_fg0 a _fg7), el programa VM cifrado dividido en variables_dp) con una clave por sitio (_pk) que desempaqueta el JSON del programa_0x8df7) que hashea la propia fuente de la VM para derivar una clave de descifrado, de modo que cualquier modificación rompe silenciosamente el descifrado del bytecodeEl programa VM está dividido en 8 variables, concatenado y luego descifrado mediante _dp() usando un cifrado XOR por sitio con clave _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("");
}
El resultado es un objeto JSON con nombres de clave de dos caracteres ofuscados (por ejemplo, "uo" para seed, "dk" para nonce). Una tabla de mapeo los convierte a nombres estándar.
node extractor.js
# -> program.json
| Campo | Descripción |
|---|---|
s | Semilla (12755), impulsa todas las operaciones criptográficas |
n | Nonce (1603730985), aleatorización por programa |
g | Indicador de generador, habilita la capa de descifrado de hash de integridad |
x | Indicador cifrado, las constantes están cifradas con XOR |
c | Pool de constantes, 1230 entradas |
f | Funciones, 112 entradas con bytecode cifrado |
e | Punto de entrada, índice de función 0 |
Todas las 1095 constantes de cadena están cifradas con dos capas:
Capa 1: XOR murmur estático con clave 4008000571, dependiente de la posición.
Capa 2: XOR de flujo PRNG usando un LCG glibc, sembrado combinando la semilla del programa con el índice de cada constante mediante el hash multiplicativo de Knuth.
Antes del descifrado, la semilla se XOR con una huella digital del entorno (_0xaf48), una máscara de bits de 8 bits calculada probando APIs del navegador:
| Bit | Prueba | Chrome |
|---|---|---|
| 0 | typeof window.matchMedia === "function" | 1 |
| 1 | document.elementFromPoint existe | 1 |
| 2 | typeof window.requestAnimationFrame === "function" | 1 |
| 3 | typeof window.getComputedStyle === "function" | 1 |
| 4 | CSS.supports existe | 1 |
| 5 | navigator.sendBeacon existe | 1 |
| 6 | document.execCommand existe | 1 |
| 7 | process.versions.node existe (Node.js) | 0 |
Para Chrome: _0xaf48 = 0b01111111 = 127, lo que da una semilla efectiva = 12755 ^ 127 = 12716.
Esto significa que el mismo programa produce diferentes resultados de descifrado en diferentes entornos. Ejecutarlo en Node.js vs Chrome vs Firefox produce diferentes semillas.
node decrypt_constants.js
# -> program_decrypted.json, constants_table.txt
Las cadenas descifradas nos dicen exactamente qué huellas digitales recoge la VM:
Huellas digitales del navegador: screenWidth, screenHeight, innerWidth, innerHeight, devicePixelRatio, colorDepth, platform, userAgent, language, timezone, timezoneOffset, forcedColors, highContrast
Temporización de rendimiento: navigationStart, domComplete, domLoading, fetchStart, requestStart, responseEnd, secureConnectionStart, serverTiming
Criptografía RSA: BigInt, modPow, AQAB (65537 en base64), modulusLength, shiftLeft, shiftRight, getRandomValues
Sondeo DOM/SVG: http://www.w3.org/2000/svg, createElementNS, getBoundingClientRect, getTotalLength, getBBox
Nombres de campos PX: mtr, tst, mst, enc, sbx, fstec, pdc, prb, wvi, wva, pti, dis, los, cv, sc, jd, ads, enve, init
Referencias de endpoints: https://fst-ec.perimeterx.net/?id=
Anti-depurador: _CMP_RCX_07;_JNZ_0x0A_EB_CC, CC|CD-04|BREAKPOINT-005
107 opcodes base que cubren todo el lenguaje JavaScript, más ruido generado dinámicamente:
40 opcodes honey son implementaciones alternativas de operaciones aritméticas/de comparación usando expresiones matemáticamente equivalentes pero sintácticamente diferentes. ADD puede aparecer como (a^b) + 2*(a&b) o -((-a)-b) o a-(-b). Cada opcode base puede tener hasta 3 variantes, generadas deterministicamente a partir de la semilla. Una simple instrucción ADD puede aparecer como 4 valores diferentes de bytecode dentro del mismo programa, rompiendo enfoques de coincidencia de patrones.
24 opcodes de relleno se asignan en la permutación pero no tienen manejadores y nunca se emiten. Existen para expandir el espacio de opcodes y hacer que el barajado sea más difícil de invertir.
16 grupos de superinstrucciones son la característica anti-análisis más importante. Cuando el bucle de despacho resuelve un opcode a un líder de superinstrucción, el manejador lee un byte adicional del flujo de bytecode y despacha a un sub-manejador. El sub-manejador puede ser una operación completamente diferente:
| El líder se resuelve como | Sub-byte | Realmente ejecuta |
|---|---|---|
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 tabla de opcodes se baraja mediante Fisher-Yates con semilla efectiva, por lo que los valores de bytecode difieren por compilación.
node build_opcodes.js
# -> opcode_table.json, opcode_table.txt
El núcleo del kit de herramientas. cfg.js construye un grafo de flujo de control siguiendo todas las rutas de ejecución desde PC=0, decodificando cada instrucción con el contexto de cifrado correcto.