PerimeterX 字节码虚拟机的逆向工程工具包,包含基于控制流图的反汇编器、五层解密管道、操作码表重建以及栈模拟清理器,用于安全研究中的机器人检测指纹识别。
本仓库记录了 PerimeterX 的 auditor.js 的反向工程结果,该文件是一个字节码虚拟机,用作 PX 机器人检测管道中的二级指纹识别层。本分析涵盖以下内容:
注意: 本仓库仅涵盖一个(静态)VM 版本,旨在用于安全研究与分析目的。不包含动态求解器或生产级求解器实现。
2026 年 4 月 2 日星期四,PerimeterX 在其机器人检测管道中部署了新的字节码虚拟机。
auditor.js 看起来不像普通的 PX 传感器脚本。它没有常见的混淆属性查找和收集器函数:
_fg0 到 _fg7),加密的 VM 程序分散在多个变量中_dp),使用特定站点的密钥(_pk)解包程序 JSON_0x8df7),对 VM 自身的源代码进行哈希处理以派生解密密钥,因此任何修改都会静默地破坏字节码解密VM 程序被分割到 8 个变量中,拼接后通过 _dp() 使用特定站点的 XOR 密码进行解密,该密码由 _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("");
}
结果是一个 JSON 对象,其键名为混淆后的两位字符(例如 "uo" 表示种子,"dk" 表示随机数)。一个映射表将其转换为标准名称。
node extractor.js
# -> program.json
所有 1095 个字符串常量都经过两层加密:
第 1 层: 静态 murmur XOR,密钥为 4008000571,位置相关。
第 2 层: 使用 glibc LCG 的 PRNG 流 XOR,通过 Knuth 乘法哈希将程序种子与每个常量的索引组合作为种子。
解密前,种子会与环境指纹(_0xaf48)进行 XOR,该指纹是一个 8 位比特掩码,通过探测浏览器 API 计算得到:
对于 Chrome:_0xaf48 = 0b01111111 = 127,有效种子为 12755 ^ 127 = 12716。
这意味着相同的程序在不同环境中会产生不同的解密结果。在 Node.js、Chrome 和 Firefox 中运行会得到不同的种子。
node decrypt_constants.js
# -> program_decrypted.json, constants_table.txt
解密后的字符串精确告诉我们 VM 指纹收集的内容:
浏览器指纹识别: screenWidth、screenHeight、innerWidth、innerHeight、devicePixelRatio、colorDepth、platform、userAgent、language、timezone、timezoneOffset、forcedColors、highContrast
性能计时: navigationStart、domComplete、domLoading、fetchStart、requestStart、responseEnd、secureConnectionStart、serverTiming
RSA 加密: BigInt、modPow、AQAB(65537 的 base64 编码)、modulusLength、shiftLeft、shiftRight、getRandomValues
DOM/SVG 探测: http://www.w3.org/2000/svg、createElementNS、getBoundingClientRect、getTotalLength、getBBox
PX 字段名称: mtr、tst、mst、enc、sbx、fstec、pdc、prb、wvi、wva、pti、dis、los、cv、sc、jd、ads、、
端点引用: https://fst-ec.perimeterx.net/?id=
反调试器: _CMP_RCX_07;_JNZ_0x0A_EB_CC、CC|CD-04|BREAKPOINT-005
107 个基本操作码覆盖完整的 JavaScript 语言,加上动态生成的噪音:
40 个蜜罐操作码 是算术/比较运算的替代实现,使用数学上等价但语法不同的表达式。ADD 可能表现为 (a^b) + 2*(a&b) 或 -((-a)-b) 或 a-(-b)。每个基本操作码最多可以有 3 个变体,由种子确定性地生成。一个简单的 ADD 指令在同一个程序中可以出现 4 种不同的字节码值,从而破坏模式匹配方法。
24 个填充操作码 在排列中被分配但没有处理程序,也永远不会被发射。它们的存在是为了扩展操作码空间,使洗牌更难逆转。
16 个超级指令组 是最重要的反分析特性。当分发循环将一个操作码解析为超级指令引导码时,处理程序从字节码流中再读取一个额外的字节并分派到子处理程序。子处理程序可以是一个完全不同的操作:
操作码表通过使用有效种子作为种子的 Fisher-Yates 洗牌进行排列,因此字节码值每次构建都不同。
node build_opcodes.js
# -> opcode_table.json, opcode_table.txt
工具包的核心。cfg.js 通过追踪从 PC=0 开始的所有执行路径来构建控制流图,并在正确的加密上下文中解码每条指令。
PX 在块边界处使用了重叠指令。相同的字节在某条执行路径上解码为操作数,在另一条路径上则解码为操作码,具体取决于块的加密上下文。线性扫描只解码每个字节位置一次,会丢失替代路径。CFG 同时追踪直通边和跳转边,独立解码每条路径。
_0x3ca8):对原始 base64 字节进行静态 murmur XOR,密钥为 4008000571_0xece1):每个函数进行 XOR,包含两个子层:位置相关的静态密钥 + 代码完整性哈希密钥_0x427d):每个块的滚动 XOR。每个加密块(由 fn.bl 边界定义)获得一个额外的 XOR,该 XOR 由函数密钥和块索引派生。块 0 在首次访问时未加密,后续块 1+ 则加密。这就是为什么线性反汇编器对第一个块有效,但对后续块产生垃圾。CFG 以非破坏性方式应用所有五层(操作数 XOR 是即时计算的,而非就地计算),因此重叠指令区域不会相互损坏。
对于每个超级指令引导码,CFG 读取子字节,在 super_groups.json 中查找实际处理程序,并为真实操作码解码操作数。来自融合跳转操作码(例如看起来是 ASSIGN_OP_VAR 但实际上是 JMP)的跳转目标会被正确跟踪。
node cfg.js # 所有函数 -> cfg_output/
node cfg.js 79 # 单个函数,输出到标准输出
已通过浏览器执行轨迹验证:跨 14 个函数追踪了 600 条指令,堆栈增量零不匹配。所有 34 个唯一操作码均已验证。在初始化期间执行的所有 10 个引导码组中,超级指令分派确认正确。
接收 CFG 输出并运行堆栈模拟以生成表达式注释。将原始字节码转换为可读的伪代码。
node cleaner.js 79 # fn79 输出到标准输出
按顺序遍历指令,跟踪虚拟堆栈。每个压栈/出栈/调用都会构建一个表达式字符串:
0018 GET_VAR ; 0.0001
001e GET_VAR ; or
0024 PUSH_CONST ; "_0x166"
002b CALL_METHOD_C ; 0.0001._0x88(or, "_0x166")
...
0114 PUSH_CONST ; "fontSize"
011b PUSH_CONST ; "pdc"
0122 CALL_METHOD_C ; _0x18c.getHours("fontSize", "pdc")
仅当空堆栈上的纯算术/比较噪音被抑制。其他所有内容都保留,因为 CFG 已经过滤了蜜罐和填充。
112 个函数,保留了 5435 条指令,抑制了 207 条噪音。fn79(指纹收集器,1109 条指令)的表达式注释覆盖率达到 85%。
基于堆栈的虚拟机,具有 256 个槽位的堆栈、作用域链、try/catch 处理程序链和 for-in 迭代器堆栈。分发循环读取 2 字节小端操作码,通过排列 + 位置 XOR + 块偏移进行解析,就地解密操作数,执行处理程序,然后重新加密操作数,使得字节码在内存中永远不会完全解密。
BigInt、modPow、指数 65537 对指纹输出进行 RSA 加密getTotalLength() 和 getBBox() 进行 SVG 渲染指纹识别performance.timing 水瀑布收集CC|CD-04|BREAKPOINT-005)AI 已被用于帮助编写代码文档、开发工具和起草此自述文件。
纯属教育/安全研究目的。无求解器或绕过方法,仅作为记录虚拟机工作原理,因为其本身确实有趣。
如果 PerimeterX/HUMAN Security 的任何人对本仓库有异议,请随时联系:[email protected]
| 字段 | 描述 |
|---|
s | 种子(12755),驱动所有加密操作 |
n | 随机数(1603730985),每次程序随机化 |
g | 生成器标志,启用完整性哈希解密层 |
x | 加密标志,常量经过 XOR 加密 |
c | 常量池,1230 个条目 |
f | 函数,112 个条目,包含加密的字节码 |
e | 入口点,函数索引 0 |
| 位 | 测试 | Chrome |
|---|
| 0 | typeof window.matchMedia === "function" | 1 |
| 1 | document.elementFromPoint 存在 | 1 |
| 2 | typeof window.requestAnimationFrame === "function" | 1 |
| 3 | typeof window.getComputedStyle === "function" | 1 |
| 4 | CSS.supports 存在 | 1 |
| 5 | navigator.sendBeacon 存在 | 1 |
| 6 | document.execCommand 存在 | 1 |
| 7 | process.versions.node 存在(Node.js) | 0 |
enveinit| 引导码解析为 | 子字节 | 实际执行 |
|---|
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 |