docs(vm): 新增 Chaos VM 规范 + WIP 纯 Python 解释器

做了什么:
- 新增 tcaptcha/VM_SPEC.md: 从源码/反汇编事实整理 VM 全套规格
  * §1 架构(VM(H,z,c,E,I,D,U,C), c=window, 主循环 M=G[z[H++]]())
  * §2 数据模型(E 一物两用=寄存器区+求值栈; 寄存器单元 E[i][0]; 引用=二元数组 [base,key])
  * §3 完整 opcode 表(70 槽/58 handler/12 空洞, 附 handler 源码与 argc, 由 AST 自动导出)
  * §4 闭包与调用(CREATE_CLOSURE 动态布局, caps 共享单元, CALL_FUNC/METHOD 的 this 绑定)
  * §5 引导流程(实测顶层只注册模块) §6 宿主与驱动(c 即 window, 环境全走 c)
  * §7 工具链映射 §8 解释器路线图(已完成①-④, 未完成⑤模块系统+宿主驱动)
- 新增 tools/tdc_vm_run.py: 58 个 opcode 的具体解释器 + 宽松 JS 宿主 (WIP, 跑通顶层引导)

为什么:
- 之前"没头绪"的根因是数据模型(E 一物两用/寄存器单元/引用二元数组), 必须写成确定规格
- 并定位真正的卡点: 不在 VM 而在"模块系统 + 宿主调用序列"

怎么验证:
- tools/tdc_vm_run.py 跑 739 步后 RET_TRUE (仅顶层引导), 与 CFG 可达性一致
This commit is contained in:
yml2213
2026-10-11 21:59:06 +08:00
parent 32a78318ad
commit 70f2ece32c
2 changed files with 785 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
# 腾讯天御 Chaos VM 规范(纯粹从源码/反汇编fact整理)
> 目的:把 TDC 的 JSVMP(`__TENCENT_CHAOS_VM`)从"读不懂"变成"有确定规格",供纯 Python 复现与静态提取使用。
> 事实来源:`tcaptcha/core_js/tdc_sample.js` 的 VM 源码(AST 导出)+ `tools/tdc_disasm.py` 的正确反汇编。
> 工具:`tools/tdc_disasm.py`(提取+CFG 反汇编)、`tools/tdc_vm_run.py`(纯 Python 解释器,WIP)。
---
## 1. 架构总览
VM 就是一个**基于栈的字节码解释器**,入口签名(源码原样):
```js
function __TENCENT_CHAOS_VM(H, z, c, E, I, D, U, C) {
var g, A = !E,
B = (H = +H, z = z || [0], E = E || [[this], [{}]], I = I || {}, []),
w = null;
function Q() { return function (A, B, w) { return new (Function.bind.apply(A, B))(); }.apply(null, arguments); }
/* ... 定义 58 个 handler,装配成 G 数组 ... */
for (var G = [ /* handlers */ ], M = !1; !M; ) M = G[z[H++]]();
}
```
| 形参 | 含义 |
| --- | --- |
| `H` | 程序计数器 PC(字节码下标) |
| `z` | 字节码整数数组(45636 个) |
| `c` | **宿主全局对象**(页面里就是 `window`)—— `.apply(c, ...)` 的默认 this |
| `E` | **寄存器区 + 求值栈**(同一个数组!)默认 `[[this],[{}]]` |
| `I D U C` | 其它透传参数(本样本用不到) |
| `B` | try 栈(异常帧) |
| `w` | 异常标志(非空即"有异常") |
主循环:`M = G[z[H++]]()` —— 取 `z[H++]` 当 opcode 下标调用 handler;**handler 返回真值时循环结束**(对应 `RET_TRUE`=19、`IS_EXCEPTION`=1 在异常时)。
---
## 2. 数据模型(关键,之前所有混乱都出在这里)
1. **`E` 同时是"寄存器区"和"求值栈"**:`E[0..n-1]` 是寄存器,`E[n..]` 是栈。
2. **寄存器是"单元数组"**:寄存器 `i` 的**值**是 `E[i][0]`,而 `E[i]` 本身是个数组(`ALLOC_REG i` → `E[i] = []`)。
3. **"寄存器引用"惯用法**:
- `PUSH_IMM_ARRAY i`(op 31)压入字面数组 `[i]`(这里 `i` 是**寄存器号**,不是数组元素);
- `MAKE_REG_PROP_REF`(op 32)`A=E.pop(); E.push([E[E.pop()][0], A])` → 得到 `[E[i][0], key]`,即"**对寄存器值的属性引用**"。
4. **引用 = 两元素数组 `[base, key]`**;`GET_PROP`(59)/`SET_PROP`(14) 负责解引用。
5. 栈操作:`DUP`(2)、`SWAP n`(17,`n` 是相对栈顶的偏移)、`POP`(51)、`REVERSE_TOP2`(54)、`SET_STACK_LEN n`(53,**直接截断/扩张 E 的长度**)、`REPLACE_TOP_IMM v`(64)。
> ⚠️ 因为 `E` 一物两用、寄存器有"单元数组"这层壳、且引用是普通二元数组,**任何"按寄存器号做正则匹配"的做法都会失效** —— 必须做真正的数据流/执行。
---
## 3. 完整 opcode 表(70 槽 / 58 handler / 12 空洞)
`argc` = handler 体内 `z[H++]` 次数(= 固定操作数字节数);`57` 是动态长度(见 §4)。
**空洞 opcode**:`7, 16, 21, 24, 27, 40, 43, 44, 50, 58, 63, 67` —— 它们**永远不会**出现在可执行字节码里;一旦在反汇编里看到空洞 opcode,说明**解码失步**(旧线性反汇编器就是这么坏的)。
| opcode | argc | handler 源码(VM 内 G 数组第 i 项) |
| --- | --- | --- |
| 0 | 1 | `function () { E[E.length - 1] += String.fromCharCode(z[H++]); }` |
| 1 | 0 | `function () { return !!w; }` |
| 2 | 0 | `function () { E.push(E[E.length - 1]); }` |
| 3 | 0 | `function () { E[E.length - 2] = E[E.length - 2] ^ E.pop(); }` |
| 4 | 0 | `function () { E[E.length - 2] = E[E.length - 2] === E.pop(); }` |
| 5 | 0 | `function () { E[E.length - 2] = E[E.length - 2] % E.pop(); }` |
| 6 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : []; E.length -= A, E.push(E.pop().apply(c, B)); }` |
| 7 | - | _(空洞: 无 handler)_ |
| 8 | 0 | `function () { E[E.length - 1] = c[E[E.length - 1]]; }` |
| 9 | 0 | `function () { var A = E.pop(), B = E.pop(); E.push([B[0][B[1]], A]); }` |
| 10 | 0 | `function () { E.push(typeof E.pop()); }` |
| 11 | 0 | `function () { var A = E.pop(); E.push(delete A[0][A[1]]); }` |
| 12 | 0 | `function () { E.push(!E.pop()); }` |
| 13 | 0 | `function () { E[E.length - 2] = E[E.length - 2] == E.pop(); }` |
| 14 | 0 | `function () { var A = E[E.length - 2]; A[0][A[1]] = E[E.length - 1]; }` |
| 15 | 0 | `function () { E[E.length - 2] = E[E.length - 2] > E.pop(); }` |
| 16 | - | _(空洞: 无 handler)_ |
| 17 | 1 | `function () { var A = z[H++], B = E[E.length - 2 - A]; E[E.length - 2 - A] = E.pop(), E.push(B); }` |
| 18 | 0 | `function () { E[E.length - 2] = E[E.length - 2] in E.pop(); }` |
| 19 | 0 | `function () { return !0; }` |
| 20 | 0 | `function () { E[E.length - 2] = E[E.length - 2] >= E.pop(); }` |
| 21 | - | _(空洞: 无 handler)_ |
| 22 | 1 | `function () { E.push(z[H++]); }` |
| 23 | 0 | `function () { E[E.length - 2] = E[E.length - 2] * E.pop(); }` |
| 24 | - | _(空洞: 无 handler)_ |
| 25 | 0 | `function () { E.push(E[E.pop()[0]][0]); }` |
| 26 | 0 | `function () { E[E.length - 1].length ? E.push(E[E.length - 1].shift(), !0) : E.push(undefined, !1); }` |
| 27 | - | _(空洞: 无 handler)_ |
| 28 | 0 | `function () { E.push([c, E.pop()]); }` |
| 29 | 0 | `function () { E.push(null); }` |
| 30 | 0 | `function () { E[E.length - 2] = E[E.length - 2] & E.pop(); }` |
| 31 | 1 | `function () { E.push([z[H++]]); }` |
| 32 | 0 | `function () { var A = E.pop(); E.push([E[E.pop()][0], A]); }` |
| 33 | 0 | `function () { E[E.length - 2] = E[E.length - 2] - E.pop(); }` |
| 34 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : [], A = (E.length -= A, E.pop()); E.push(A[0][A[1]].apply(A[0], B)); }` |
| 35 | 0 | `function () { E[E.length - 2] = E[E.length - 2] >> E.pop(); }` |
| 36 | 0 | `function () { E[E.length - 2] = E[E.length - 2] << E.pop(); }` |
| 37 | 1 | `function () { var A = z[H++]; E[E.length - 1] && (H = A); }` |
| 38 | 0 | `function () { var A, B = []; for (A in E.pop()) B.push(A); E.push(B); }` |
| 39 | 0 | `function () { E[E.length - 2] = E[E.length - 2] + E.pop(); }` |
| 40 | - | _(空洞: 无 handler)_ |
| 41 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : [], A = (E.length -= A, B.unshift(null), E.pop()); E.push(Q(A[0][A[1]], B)); }` |
| 42 | 0 | `function () { E.push(undefined); }` |
| 43 | - | _(空洞: 无 handler)_ |
| 44 | - | _(空洞: 无 handler)_ |
| 45 | 0 | `function () { E.push(!1); }` |
| 46 | 0 | `function () { E[E[E.length - 2][0]][0] = E[E.length - 1]; }` |
| 47 | 1 | `function () { var A = z[H++]; E[A] = E[A] === undefined ? [] : E[A]; }` |
| 48 | 0 | `function () { B.pop(); }` |
| 49 | 0 | `function () { throw E[E.length - 1]; }` |
| 50 | - | _(空洞: 无 handler)_ |
| 51 | 0 | `function () { E.pop(); }` |
| 52 | 0 | `function () { E[E.length - 2] = E[E.length - 2] / E.pop(); }` |
| 53 | 1 | `function () { E.length = z[H++]; }` |
| 54 | 0 | `function () { E.push([E.pop(), E.pop()].reverse()); }` |
| 55 | 0 | `function () { E.push(""); }` |
| 56 | 2 | `function () { B.push([z[H++], E.length, z[H++]]); }` |
| 57 | 6 | `function () { for (var w = z[H++], Q = [], A = z[H++], B = z[H++], G = [], M = 0; M < A; M++) Q[z[H++]] = E[z[H++]]; for (M = 0; M < B; M++) G[M] = z[H++]; E.push(function g() { var A = Q.slice(0); A[0] = [this], A[1] = [arguments …` |
| 58 | - | _(空洞: 无 handler)_ |
| 59 | 0 | `function () { var A = E.pop(); E.push(A[0][A[1]]); }` |
| 60 | 1 | `function () { H = z[H++]; }` |
| 61 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : []; E.length -= A, B.unshift(null), E.push(Q(E.pop(), B)); }` |
| 62 | 0 | `function () { E[E.length - 2] = E[E.length - 2] \| E.pop(); }` |
| 63 | - | _(空洞: 无 handler)_ |
| 64 | 1 | `function () { E[E.length - 1] = z[H++]; }` |
| 65 | 0 | `function () { E[E.length - 2] = E[E.length - 2] >>> E.pop(); }` |
| 66 | 0 | `function () { E.push(!0); }` |
| 67 | - | _(空洞: 无 handler)_ |
| 68 | 1 | `function () { E.push(E[z[H++]][0]); }` |
| 69 | 0 | `function () { w = null; }` |
---
## 4. 闭包与调用约定
### 4.1 `CREATE_CLOSURE`(57) —— 操作数布局(动态)
```
@pc: 57 target capN parN [reg,reg]*capN [reg]*parN
```
handler 语义(源码):
```js
for (var w = z[H++], Q = [], A = z[H++], B = z[H++], G = [], M = 0; M < A; M++) Q[z[H++]] = E[z[H++]];
for (M = 0; M < B; M++) G[M] = z[H++];
E.push(function g() {
var A = Q.slice(0);
A[0] = [this], A[1] = [arguments], A[2] = [g];
for (var B = 0; B < G.length && B < arguments.length; B++) 0 < G[B] && (A[G[B]] = [arguments[B]]);
return __TENCENT_CHAOS_VM(w, z, c, A, I, D, U, C);
});
```
- `Q[capRegIdx] = E[parentRegIdx]` —— **捕获是按"寄存器号 → 寄存器号"成对出现的**(`caps=[r7=r8]` 表示新闭包的 r7 ← 父闭包 r8,**共享同一单元对象**);
- `G` 是形参寄存器号表;
- **调用闭包 = 以 `target` 为新 PC、带着"捕获拷贝 + this/arguments/g"重新进入 VM**。
> 🔑 由此得出一条反汇编铁律:**`CREATE_CLOSURE` 的 `target` 是"被调用时才执行"的代码,必须作为反汇编遍历根**,否则大半个程序都算不到(这是旧工具的第二处致命缺陷)。
### 4.2 调用类 opcode
| op | 名字 | this 绑定 | 说明 |
| --- | --- | --- | --- |
| 6 | CALL_FUNC n | `c`(全局) | `E.push(E.pop().apply(c, B))` |
| 34 | CALL_METHOD n | 引用基对象 | `A[0][A[1]].apply(A[0], B)` |
| 61 | NEW_FUNC n | — | 用 `Q(...)` 做 `new` |
| 41 | NEW_METHOD n | — | 同上,参数前插 `null` |
| 28 | BIND_THIS | — | `E.push([c, E.pop()])` 造"以 c 为基"的引用 |
### 4.3 其它结构
- **异常**:`PUSH_TRY`(56) 压 `[catchPC, E.length, catchVarReg]` 到 `B`;`POP_TRY`(48) 弹;`IS_EXCEPTION`(1) 检查 `w`;`CLEAR_EXCEPTION`(69) `w=null`;`THROW`(49)。
- **属性**:`GET_GLOBAL_PROP`(8) `c[E[-1]]`;`MAKE_PROP_REF`(9) 通用引用;`FOR_IN_KEYS`(38)。
- **字符串**:`PUSH_EMPTY_STR`(55) 压 `""`,随后一串 `PUSH_STR_CHAR`(0) 逐字符追加 —— 所以**字面串在字节码里是一串 <256 的立即数**(折叠即可还原,`tdc_disasm.py` 已做)。
---
## 5. 引导流程(bootstrap,实测)
```
@00000: SET_STACK_LEN 3
@00002: ALLOC_REG 2
@00004: JUMP @1518
...
@01518: CREATE_CLOSURE target=@7 caps=[] params=[p0->r3] <- 模块工厂(之一)
@01523: PUSH_EMPTY_STR "Array" ... GET_GLOBAL_PROP / NEW_FUNC ...
...
(很快 RET_TRUE 返回)
```
**我们的 Python 解释器跑 739 步后 `RET_TRUE` 返回,且寄存器里没有可用的模块闭包** —— 说明:
> **tdc.js 的顶层只"注册/装配"模块;真正做 XTEA、生成密钥的代码,是在宿主调用进模块时才执行的**(浏览器里最终由 `TDC.getData()` 触发;`tools/tdc_probe.js` 也是这么驱动的)。
---
## 6. 宿主接口与"如何驱动"(纯 Python 路线的真正缺口)
- JS 侧调用是 `__TENCENT_CHAOS_VM(0, <zCall 结果>, window)` → **`c = window`**。
- VM 本身不碰环境;**环境访问全部通过 `c`**(`GET_GLOBAL_PROP` / `CALL_METHOD` 的基对象)。
- 因此"能跑出 collect"靠的是:**页面加载 → 模块系统装配 → 宿主调用模块入口 → 模块内部用 `c` 读环境**。
- 所以纯 Python 静态路线要补的不是 VM,而是**模块系统 + 宿主调用序列**。
---
## 7. 工具链映射
| 工具 | 作用 | 状态 |
| --- | --- | --- |
| `tools/tdc_disasm.py` | 纯 Python:提取字节码 + 可达性 CFG 反汇编(0 非法 opcode) | ✅ 可用(canonical) |
| `tools/tdc_vm_run.py` | 纯 Python:58 个 opcode 的具体解释器 + 宽松 JS 宿主 | 🚧 WIP(跑通顶层引导,未达密钥) |
| `tools/tdc_probe.js` | 深度 Proxy 沙箱驱动真实 tdc.js(**分析参考**,非交付路径) | 分析工具 |
| `tcaptcha/tdc_worker.js` | Node 插桩抓 XTEA 密钥常量(当前唯一 Node 依赖) | 可用·有缓存 |
---
## 8. 解释器路线图(要"彻底理顺"必须补齐的点)
1. **模块表**:记录每个 `CREATE_CLOSURE` 工厂的 `target`,识别"注册 vs 调用"。
2. **模块系统语义**:复刻 `require/exports` 式的装配顺序(谁在何时被调用)。
3. **宿主 `c` 的最小可用面**:`Object/Array/String/Math/JSON/RegExp/Date` + 事件/DOM 桩(严格只补"模块初始化真正读到的",不盲目补)。
4. **驱动入口**:复刻页面初始化序列,直到进入 XTEA 那段闭包。
5. **抓取产物**:寄存器中出现的"4 个可打印字符拼成的 32bit 数组" = XTEA 密钥(已确证形态)。
6. **校验**:与 `extract_xtea_key`(Node)结果逐字对齐,对齐后才敢替换。
> 已完成:①字节码提取 ②正确反汇编 ③全部 opcode 语义 ④密钥形态与流向定位(`caps=[r7=r8]`,XTEA 轮 `reg7[(sum>>>11)&3]`,本构建轮加盐 `723726`/`855311`)。
> 未完成:⑤模块系统 + 宿主驱动(本路线图的 1–4)。
+560
View File
@@ -0,0 +1,560 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
腾讯天御 Chaos VM —— **纯 Python 具体执行器** (目标: 静态解出 XTEA 密钥, 去掉 Node 依赖)
思路
----
密钥字是 4 个 "ASCII 打包的 4 字符串", 但既不是立即数、也不是任何字面字符串/其子串,
说明它是**运行时算出来的**。所以这里把 VM 的 70 个 opcode 语义 (由 handlers 源码逐一确认)
在 Python 里重新实现, 直接跑字节码, 把寄存器里出现的 "4 个可打印字符拼成的 32bit 数组" 抓出来。
VM 模型 (与源码一致):
* `E` 同时是**寄存器区** (E[0..n-1]) 与**求值栈** (E[n..]);
* 寄存器单元是长度为 1 的数组: 值 = E[i][0];
* `z` = 字节码, `H` = PC, `c` = 宿主全局对象 (window)。
用法:
.venv/bin/python tools/tdc_vm_run.py tcaptcha/core_js/tdc_sample.js
KEY=<已知密钥> .venv/bin/python tools/tdc_vm_run.py tdc.js # 命中即打印所在寄存器
"""
from __future__ import annotations
import json
import math
import os
import re
import sys
import time
from typing import Any, Dict, List, Optional
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from tdc_disasm import extract_bytecode # noqa: E402
# --------------------------------------------------------------------------- #
# JS 风格容器: 既支持整数下标, 也支持字符串属性
# --------------------------------------------------------------------------- #
class JArr(list):
def __init__(self, *a):
super().__init__(*a)
self._ext: Dict[str, Any] = {}
def __getitem__(self, k):
if isinstance(k, int) or (isinstance(k, str) and k.lstrip("-").isdigit()):
i = int(k)
try:
return list.__getitem__(self, i)
except IndexError:
return None
return self._ext.get(k)
def __setitem__(self, k, v):
if isinstance(k, int) or (isinstance(k, str) and k.lstrip("-").isdigit()):
i = int(k)
if 0 <= i < len(self):
list.__setitem__(self, i, v)
else:
self._ext[str(i)] = v
else:
self._ext[k] = v
JS_UNDEF = object()
class Stub:
"""未知宿主对象的占位: 可调用、可属性访问、可下标, 一律返回自身/None。"""
def __init__(self, name="stub"):
self._name = name
def __call__(self, *a, **k):
return Stub(self._name + "()")
def __getattr__(self, k):
if k.startswith("_"):
raise AttributeError(k)
return Stub(f"{self._name}.{k}")
def __getitem__(self, k):
return Stub(f"{self._name}[{k}]")
def __setitem__(self, k, v):
pass
def __repr__(self):
return f"<{self._name}>"
def _to_host(**kw):
o = JArr()
o._ext.update(kw)
return o
def make_host() -> Any:
"""最小宿主 (c = window)。未知项退化为 Stub, 保证不因缺属性而崩。"""
win = _to_host()
class MathMod:
pass
win._ext.update({
"Math": Stub("Math"),
"JSON": type("J", (), {"parse": staticmethod(json.loads), "stringify": staticmethod(json.dumps)})(),
"Object": JArr,
"Array": JArr,
"String": str,
"Number": float,
"Boolean": bool,
"Date": int(time.time() * 1000),
"RegExp": Stub("RegExp"),
"Error": Stub("Error"),
"TypeError": Stub("TypeError"),
"Uint8Array": bytearray,
"Uint8ClampedArray": bytearray,
"parseInt": int,
"parseFloat": float,
"encodeURIComponent": lambda s: s,
"decodeURIComponent": lambda s: s,
"atob": lambda s: s,
"btoa": lambda s: s,
"undefined": JS_UNDEF,
"NaN": float("nan"),
})
return win
def getprop(base, key):
"""JS 的 base[key] (非严格语义: 对原始值取属性返回 undefined)"""
if isinstance(base, (Stub, JArr)):
return base[key]
if isinstance(base, dict):
return base.get(str(key), JS_UNDEF)
if isinstance(base, list):
return base[int(key)] if isinstance(key, (int, str)) and str(key).lstrip("-").isdigit() and 0 <= int(key) < len(base) else JS_UNDEF
if isinstance(base, str):
return base[int(key)] if isinstance(key, (int, str)) and str(key).lstrip("-").isdigit() and 0 <= int(key) < len(base) else JS_UNDEF
return JS_UNDEF # number/bool/null/undefined: 属性访问为 undefined
def setprop(base, key, val):
"""JS 的 base[key] = val (非严格语义: 对原始值赋值静默忽略)"""
if isinstance(base, Stub):
return
if isinstance(base, JArr):
base[key] = val
return
if isinstance(base, dict):
base[str(key)] = val
return
if isinstance(base, list):
i = int(key)
if 0 <= i < len(base):
base[i] = val
else:
base.append(val)
return
return # 原始值: 静默忽略
def as_int(v):
if isinstance(v, bool):
return 1 if v else 0
if v is JS_UNDEF or v is None:
return 0
if isinstance(v, (int, float)):
return int(v) & 0xFFFFFFFF
if isinstance(v, str):
return len(v) and (ord(v[0]) & 0xFFFFFFFF) or 0
return 0
def truthy(v):
if v is JS_UNDEF or v is None or v is False:
return False
if isinstance(v, (int, float)):
return v != 0
if isinstance(v, str):
return len(v) > 0
return True
# --------------------------------------------------------------------------- #
class VM:
def __init__(self, z: List[int], c, max_steps: int = 20_000_000):
self.z = z
self.c = c
self.E: List[Any] = [] # 寄存器区 + 栈
self.B: List[Any] = [] # try 栈
self.w = None # 异常标志
self.H = 0
self.steps = 0
self.max_steps = max_steps
self.done = False
self.ret = None
self.key_hits: List[Any] = []
# ---- 工具
def pu(self):
return self.E.pop()
def look(self):
self.record_cell(self.E[-1])
def record_cell(self, v):
if isinstance(v, list) and len(v) == 4 and all(isinstance(x, int) for x in v):
if all(32 <= (x & 0xFF) < 127 and 32 <= ((x >> 24) & 0xFF) < 127 for x in v):
packed = b"".join(x.to_bytes(4, "big") for x in v)
if all(32 <= b < 127 for b in packed):
self.key_hits.append(list(v))
# ---- 主循环
def run(self):
z = self.z
while not self.done and self.H < len(z):
self.steps += 1
if self.steps > self.max_steps:
raise RuntimeError("step limit exceeded")
op = z[self.H]; self.H += 1
h = self.OP.get(op)
if h is None:
raise RuntimeError(f"unknown opcode {op} at pc {self.H-1}")
if h() is True:
self.done = True
return self.ret
# ------------------------------------------------------------------ ops
def _run(self):
z, E = self.z, self.E
OP = {}
def op(n):
def deco(f):
OP[n] = f
return f
return deco
@op(0) # PUSH_STR_CHAR
def _0():
E[-1] = (E[-1] if isinstance(E[-1], str) else "") + chr(z[self.H]); self.H += 1
@op(1) # IS_EXCEPTION -> 结束
def _1():
if truthy(self.w):
self.ret = self.w
return True
return None
@op(2)
def _2():
E.append(E[-1])
def binop(f):
def g():
b = E.pop(); E[-2] = f(E[-2], b)
return g
OP[3] = binop(lambda a, b: as_int(a) ^ as_int(b))
OP[4] = binop(lambda a, b: a is b or a == b)
OP[5] = binop(lambda a, b: (as_int(a) % as_int(b)) if as_int(b) else float("nan"))
OP[13] = binop(lambda a, b: a == b)
OP[15] = binop(lambda a, b: as_int(a) > as_int(b))
OP[20] = binop(lambda a, b: as_int(a) >= as_int(b))
OP[23] = binop(lambda a, b: (as_int(a) * as_int(b)) & 0xFFFFFFFF)
OP[30] = binop(lambda a, b: as_int(a) & as_int(b))
OP[33] = binop(lambda a, b: (as_int(a) - as_int(b)) & 0xFFFFFFFF)
OP[35] = binop(lambda a, b: as_int(a) >> (as_int(b) & 31))
OP[36] = binop(lambda a, b: (as_int(a) << (as_int(b) & 31)) & 0xFFFFFFFF)
OP[39] = binop(lambda a, b: ((as_int(a) + as_int(b)) & 0xFFFFFFFF) if (isinstance(a, int) and isinstance(b, int)) else (a + b))
OP[52] = binop(lambda a, b: (as_int(a) / as_int(b)) if as_int(b) else float("nan"))
OP[62] = binop(lambda a, b: as_int(a) | as_int(b))
OP[65] = binop(lambda a, b: (as_int(a) & 0xFFFFFFFF) >> (as_int(b) & 31))
OP[18] = binop(lambda a, b: isinstance(b, (list, dict, JArr)) and (a in (b._ext if isinstance(b, JArr) else b)))
@op(6) # CALL_FUNC n
def _6():
n = z[self.H]; self.H += 1
args = E[-n:] if n else []
del E[len(E) - n:]
fn = E.pop()
E.append(self.call(fn, args, None))
@op(8) # GET_GLOBAL_PROP
def _8():
E[-1] = getprop(self.c, E[-1])
@op(9) # MAKE_PROP_REF
def _9():
a = E.pop(); b = E.pop()
E.append([getprop(b[0], b[1]), a])
@op(10)
def _10():
E[-1] = type(E[-1]).__name__
@op(11)
def _11():
a = E.pop(); E.append(True)
@op(12)
def _12():
E[-1] = not truthy(E[-1])
@op(14) # SET_PROP
def _14():
a = E[-2]; setprop(a[0], a[1], E[-1])
@op(17) # SWAP n
def _17():
n = z[self.H]; self.H += 1
i = len(E) - 2 - n
b = E[i]; E[i] = E.pop(); E.append(b)
@op(19) # RET_TRUE
def _19():
self.ret = True
return True
@op(22) # PUSH_IMM
def _22():
E.append(z[self.H]); self.H += 1
@op(25) # LOAD_REG_INDIRECT
def _25():
cell = E[E.pop()[0]]
E.append(cell[0] if cell else JS_UNDEF)
@op(26) # ITER_NEXT
def _26():
it = E[-1]
if it:
E.append(it.pop(0)); E.append(True)
else:
E.append(JS_UNDEF); E.append(False)
@op(28)
def _28():
E.append([self.c, E.pop()])
@op(29)
def _29():
E.append(None)
@op(31) # PUSH_IMM_ARRAY
def _31():
E.append(JArr([z[self.H]])); self.H += 1
@op(32) # MAKE_REG_PROP_REF
def _32():
a = E.pop()
idx = E.pop()
cell = E[idx[0]] if isinstance(idx, list) else E[idx]
E.append([cell[0] if isinstance(cell, list) and cell else JS_UNDEF, a])
@op(34) # CALL_METHOD n
def _34():
n = z[self.H]; self.H += 1
args = E[-n:] if n else []
del E[len(E) - n:]
ref = E.pop()
E.append(self.call(getprop(ref[0], ref[1]), args, ref[0]))
@op(37) # JUMP_IF_TRUE
def _37():
t = z[self.H]; self.H += 1
if truthy(E[-1]):
self.H = t
@op(38) # FOR_IN_KEYS
def _38():
o = E.pop()
E.append(JArr(list(o._ext.keys()) if isinstance(o, JArr) else list(o.keys()) if isinstance(o, dict) else []))
@op(41) # NEW_METHOD n
def _41():
n = z[self.H]; self.H += 1
args = E[-n:] if n else []
del E[len(E) - n:]
args.insert(0, None)
fn = E.pop()
E.append(self.new(fn, args))
@op(42)
def _42():
E.append(JS_UNDEF)
@op(45)
def _45():
E.append(False)
@op(46) # STORE_REG
def _46():
idx = E[-2][0]
cell = E[idx]
if not isinstance(cell, list):
cell = []
E[idx] = cell
v = E[-1]
if cell:
cell[0] = v
else:
cell.append(v)
self.record_cell(v)
@op(47) # ALLOC_REG
def _47():
a = z[self.H]; self.H += 1
while len(E) <= a:
E.append(JS_UNDEF)
if not isinstance(E[a], list):
E[a] = []
@op(48)
def _48():
self.B.pop()
@op(49) # THROW
def _49():
raise RuntimeError("VM throw")
@op(51)
def _51():
E.pop()
@op(53) # SET_STACK_LEN
def _53():
n = z[self.H]; self.H += 1
while len(E) < n:
E.append(JS_UNDEF)
del E[n:]
@op(54)
def _54():
a = E.pop(); b = E.pop()
E.append(JArr([a, b]))
@op(55)
def _55():
E.append("")
@op(56) # PUSH_TRY
def _56():
self.H += 2
@op(57) # CREATE_CLOSURE
def _57():
target = z[self.H]; self.H += 1
cap = z[self.H]; self.H += 1
par = z[self.H]; self.H += 1
caps = []
for _ in range(cap):
q = z[self.H]; self.H += 1
r = z[self.H]; self.H += 1
caps.append((q, r))
params = []
for _ in range(par):
params.append(z[self.H]); self.H += 1
E.append(("closure", target, caps, params))
@op(59) # GET_PROP
def _59():
a = E.pop()
E.append(getprop(a[0], a[1]))
@op(60) # JUMP
def _60():
self.H = z[self.H]
@op(61) # NEW_FUNC n
def _61():
n = z[self.H]; self.H += 1
args = E[-n:] if n else []
del E[len(E) - n:]
args.insert(0, None)
fn = E.pop()
E.append(self.new(fn, args))
@op(64) # REPLACE_TOP_IMM
def _64():
E[-1] = z[self.H]; self.H += 1
@op(66)
def _66():
E.append(True)
@op(68) # LOAD_REG_IMM
def _68():
a = z[self.H]; self.H += 1
cell = E[a] if a < len(E) else None
E.append(cell[0] if isinstance(cell, list) and cell else JS_UNDEF)
@op(69)
def _69():
self.w = None
self.OP = OP
return self.run()
# ---- 调用/构造
def call(self, fn, args, this):
if isinstance(fn, tuple) and fn and fn[0] == "closure":
sub = VM(self.z, self.c, self.max_steps - self.steps)
sub.key_hits = self.key_hits # 子 VM 共享命中列表
sub.E = [JArr() for _ in range(32)]
sub.E[0] = JArr([this]); sub.E[1] = JArr([JArr(args)]); sub.E[2] = JArr([fn])
for (q, r) in fn[2]:
sub.E[q] = self.E[r]
for i, r in enumerate(fn[3]):
sub.E[r] = JArr([args[i] if i < len(args) else JS_UNDEF])
sub.H = fn[1]
try:
v = sub._run()
self.steps += sub.steps
return v
except Exception:
return JS_UNDEF
if isinstance(fn, Stub):
return Stub("call")
return JS_UNDEF
def new(self, fn, args):
return self.call(fn, args, {})
def main() -> int:
path = sys.argv[1] if len(sys.argv) > 1 else "tcaptcha/core_js/tdc_sample.js"
js = open(path, encoding="utf-8", errors="replace").read()
z = extract_bytecode(js)
known = os.environ.get("KEY")
known_key = [int(x) for x in known.split(",")] if known else None
print(f"bytecode={len(z)} known_key={known_key}")
vm = VM(z, make_host(), max_steps=30_000_000)
try:
vm._run()
except Exception as e:
print("VM halted with:", type(e).__name__, str(e)[:200])
print("steps:", vm.steps, "done:", vm.done, "ret:", str(vm.ret)[:80])
# tdc.js 顶层只"注册"模块工厂; 这里主动调用寄存器里保存的闭包 (模拟模块系统/宿主调用)
closures = [cell[0] for cell in vm.E
if isinstance(cell, list) and cell and isinstance(cell[0], tuple)
and cell[0] and cell[0][0] == "closure"]
print("registered closures in registers:", len(closures))
for i, cl in enumerate(closures):
try:
vm.call(cl, [Stub("arg")], {})
except Exception as e:
print(f" closure#{i} (target=@{cl[1]}) raised {type(e).__name__}: {str(e)[:80]}")
print("total steps after invoking closures:", vm.steps)
hits = [h for h in vm.key_hits if known_key is None or h == known_key]
print("candidate 4-char packed arrays:", len(hits))
for h in hits[:10]:
print(" ", h, "->", b"".join(x.to_bytes(4, "big") for x in h))
return 0
if __name__ == "__main__":
raise SystemExit(main())