Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
px-vm — Reverse engineering toolkit for PerimeterX's bytecode VM, featuring a CFG-based disassembler, 5-layer decryption pipeline, opcode table reconstruction, and stack emulation cleaner for security research on bot detection fingerprinting. | Kitploit
Tools/GitHubGitHub/b9ph0met/px-vm
Dynamic Analysis (Sandboxing)IDS/IPS EvasionReverse EngineeringWeb SecurityMalware AnalysisCryptographyBinary AnalysisPapers & ResearchLearning & EducationAnti-BotFingerprint Spoofing
5514376 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
GitHub
b9ph0met/px-vm

px-vm

Reverse engineering toolkit for PerimeterX's bytecode VM, featuring a CFG-based disassembler, 5-layer decryption pipeline, opcode table reconstruction, and stack emulation cleaner for security research on bot detection fingerprinting.

View Repository
Share

PerimeterX Auditor VM Analysis

Summary

This repository documents the reverse engineering of PerimeterX's auditor.js, a bytecode virtual machine used as a secondary fingerprinting layer in PX's bot detection pipeline. This analysis covers:

  • Bytecode extraction and 5-layer decryption pipeline
  • Opcode table reconstruction (107 base + 40 honey + 24 padding + 16 superinstruction groups)
  • Constants pool decryption (1230 entries, 1095 encrypted strings)
  • CFG-based disassembler with superinstruction sub-dispatch resolution
  • Stack emulation cleaner producing readable pseudo-code
  • Anti-analysis techniques: honey opcodes, overlapping instructions, code integrity hashing

Note: This repository covers only one (static) VM version and is intended for security research and analysis purposes. It does not include dynamic solvers or production solver implementations.

Background

On Thursday April 2nd 2026, PerimeterX deployed a new bytecode VM as part of their bot detection pipeline.

What's Inside

auditor.js doesn't look like a normal PX sensor script. Instead of the usual obfuscated property lookups and collector functions:

  • 8 massive base64 strings (_fg0 through _fg7), the encrypted VM program split across variables
  • A XOR decryption function (_dp) with a per-site key (_pk) that unpacks the program JSON
  • A Fisher-Yates shuffle that permutes the opcode table so bytecode values differ per build
  • A dispatch loop with 107+ case handlers, the VM interpreter
  • BigInt arithmetic for RSA encryption of the fingerprint output
  • A code integrity hash (_0x8df7) that hashes the VM's own source to derive a decryption key, so any modification silently breaks bytecode decryption

Step 1: Bytecode Extraction

The VM program is split across 8 variables, concatenated, then decrypted via _dp() using a per-site XOR cipher keyed by _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("");
}

The result is a JSON object with obfuscated two-character key names (e.g., "uo" for seed, "dk" for nonce). A mapping table converts them to standard names.

node extractor.js
# -> program.json

Program Structure

FieldDescription
sSeed (12755), drives all cryptographic operations
nNonce (1603730985), per-program randomization
gGenerator flag, enables integrity hash decryption layer
xEncrypted flag, constants are XOR-encrypted
cConstants pool, 1230 entries
fFunctions, 112 entries with encrypted bytecode
eEntry point, function index 0

Step 2: Constants Decryption

All 1095 string constants are encrypted with two layers:

Layer 1: Static murmur XOR keyed by 4008000571, position-dependent.

Layer 2: PRNG stream XOR using a glibc LCG, seeded by combining the program seed with each constant's index via Knuth's multiplicative hash.

Before decryption, the seed is XOR'd with an environment fingerprint (_0xaf48), an 8-bit bitmask computed by probing browser APIs:

BitTestChrome
0typeof window.matchMedia === "function"1
1document.elementFromPoint exists1
2typeof window.requestAnimationFrame === "function"1
3typeof window.getComputedStyle === "function"1
4CSS.supports exists1
5navigator.sendBeacon exists1
6document.execCommand exists1
7process.versions.node exists (Node.js)0

For Chrome: _0xaf48 = 0b01111111 = 127, giving effective seed = 12755 ^ 127 = 12716.

This means the same program produces different decryption results in different environments. Running it in Node.js vs Chrome vs Firefox yields different seeds.

node decrypt_constants.js
# -> program_decrypted.json, constants_table.txt

What the Constants Reveal

The decrypted strings tell us exactly what the VM fingerprints:

Browser fingerprinting: screenWidth, screenHeight, innerWidth, innerHeight, devicePixelRatio, colorDepth, platform, userAgent, language, timezone, timezoneOffset, forcedColors, highContrast

Performance timing: navigationStart, domComplete, domLoading, fetchStart, requestStart, responseEnd, secureConnectionStart, serverTiming

RSA cryptography: BigInt, modPow, AQAB (65537 in base64), modulusLength, shiftLeft, shiftRight, getRandomValues

DOM/SVG probing: http://www.w3.org/2000/svg, createElementNS, getBoundingClientRect, getTotalLength, getBBox

PX field names: mtr, tst, mst, enc, sbx, fstec, pdc, prb, wvi, wva, pti, dis, los, cv, sc, jd, ads, enve, init

Endpoint references: https://fst-ec.perimeterx.net/?id=

Anti-debugger: _CMP_RCX_07;_JNZ_0x0A_EB_CC, CC|CD-04|BREAKPOINT-005

Step 3: Opcode Table

107 base opcodes covering the full JavaScript language, plus dynamically generated noise:

40 honey opcodes are alternate implementations of arithmetic/comparison ops using mathematically equivalent but syntactically different expressions. ADD might appear as (a^b) + 2*(a&b) or -((-a)-b) or a-(-b). Each base opcode can have up to 3 variants, generated deterministically from the seed. A simple ADD instruction can appear as 4 different bytecode values within the same program, breaking pattern-matching approaches.

24 padding opcodes are allocated in the permutation but have no handlers and are never emitted. They exist to expand the opcode space and make the shuffle harder to invert.

16 superinstruction groups are the most important anti-analysis feature. When the dispatch loop resolves an opcode to a superinstruction leader, the handler reads one additional byte from the bytecode stream and dispatches to a sub-handler. The sub-handler can be a completely different operation:

Leader resolves asSub-byteActually executes
FOR_IN_NEXT74FOR_IN_NEXT
FOR_IN_NEXT100MAKE_CLOSURE
ASSIGN_OP_VAR165ASSIGN_OP_VAR
ASSIGN_OP_VAR37JMP
GET_VAR_PROP_C143SET_VAR_POP
GET_VAR_PROP_C23JMP_NULLISH

The opcode table is shuffled via Fisher-Yates seeded by the effective seed, so bytecode values differ per build.

node build_opcodes.js
# -> opcode_table.json, opcode_table.txt

Step 4: CFG Builder

The core of the toolkit. cfg.js builds a control flow graph by following all execution paths from PC=0, decoding each instruction with the correct encryption context.

Why Not a Linear Disassembler

PX uses overlapping instructions at block boundaries. The same bytes decode as operands on one execution path and as opcodes on another, depending on the block encryption context. A linear scan decodes each byte position once and misses the alternate path. The CFG follows both fall-through and jump edges, decoding each path independently.

Five-Layer Bytecode Encryption

Download Tool