feat(工具链): 纯 Python 可达性 CFG 反汇编器 + 废弃旧线性反汇编器
做了什么: - 新增 tools/tdc_disasm.py: 纯 Python 提取 tdc.js 字节码数组 + 可达性 CFG 反汇编 * opcode 表由 VM 源码 AST 推导 (70 槽 / 58 handler, 操作数个数 = handler 内 z[H++] 次数) * 复刻 VM 内联的 base64 解码 + 游程展开 (含科学计数法常量), 无任何 JS 运行时 * 遍历根含 JUMP/JUMP_IF_TRUE/PUSH_TRY catch/CREATE_CLOSURE 闭包体 - tools/tdc_vm_disasm.js 降级为废弃 shim (转发到 Python 版) - TOOLCHAIN_REPORT.md 更新工具行与复验命令; tdc_sample.disasm.txt 用正确实现重新生成 为什么: - 旧线性反汇编对字节码里的数据区继续扫描会失步: 实测 73 处 UNKNOWN (16 处 >69 的 "不可能" opcode), 且未把闭包体当遍历根 -> 大半个程序不可达, 输出不可信 怎么验证: - .venv/bin/python tools/tdc_disasm.py tcaptcha/core_js/tdc_sample.js -> bytecode=45636 (与旧实现一致), 可达指令 26168, illegal opcodes: [] - 反汇编首次干净读出 XTEA 轮函数: PUSH_IMM 2654435769 / ADD / ... / SHR_UNSIGNED 11 / &3 并定位密钥数组在寄存器 7, 本构建轮加盐为 723726 / 855311 (均为立即数)
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
| 工具 | 路径 | 核心功能 | 解决什么问题 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Webpack 解包索引器** | [`tools/unpack_webpack.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/unpack_webpack.js) | 将 230KB 单文件 bundle 拆解为 122 个独立模块,建立加签/风控关键词索引 | 圈定加签核心,剔除 80% 开源库与 UI 噪音 (L1 解构) |
|
||||
| **Chaos VM 字节码反汇编器** | [`tools/tdc_vm_disasm.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_vm_disasm.js) | 映射全部 70 个操作码语义,自动折叠字符拼接,反编译 45,636 字节码为 8,151 条指令流 | 彻底透明化 TDC 字节码调度器 (L2/L3 解构) |
|
||||
| **Chaos VM 字节码反汇编器 (纯 Python)** | [`tools/tdc_disasm.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_disasm.py) | 纯 Python 提取 45,636 字节码 + 可达性 CFG 反汇编(26,168 条可达指令,**0 处非法 opcode**);opcode 表由 VM 源码 AST 推导 | 彻底透明化 TDC 字节码调度器 (L2/L3 解构) |
|
||||
| **深度 Proxy 语义探针** | [`tools/tdc_probe.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_probe.js) | 零 Headless/CDP 依赖,通过深度 Proxy 沙箱执行 `tdc.js`,拦截并记录全部 95 项环境读取 | 毫秒级生成与当前会话时间戳和密钥 100% 自洽的动态 `collect` 与 `eks` (L4 数据层) |
|
||||
|
||||
---
|
||||
@@ -96,7 +96,8 @@
|
||||
node tools/unpack_webpack.js
|
||||
|
||||
# 2. 验证 TDC 虚拟机反汇编与字符串提取
|
||||
node tools/tdc_vm_disasm.js tcaptcha/core_js/tdc_sample.js
|
||||
node tools/tdc_vm_disasm.js tcaptcha/core_js/tdc_sample.js # 已废弃, 转发到下面的纯 Python 实现
|
||||
.venv/bin/python tools/tdc_disasm.py tcaptcha/core_js/tdc_sample.js
|
||||
|
||||
# 3. 验证深度 Proxy 语义探针执行并产出 100% 合法 collect 与 eks
|
||||
node tools/tdc_probe.js tcaptcha/core_js/tdc_sample.js
|
||||
|
||||
+25567
-7551
File diff suppressed because one or more lines are too long
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
腾讯天御 TDC 混沌虚拟机 (Chaos VM) —— **纯 Python** 字节码提取 + 反汇编器
|
||||
|
||||
为什么需要它
|
||||
------------
|
||||
旧 `tools/tdc_vm_disasm.js` 依赖 Node + Babel,且用**线性扫描**解码。实测该字节码数组
|
||||
里混有数据区/不可达区,线性扫描一进入数据区就失步,产出 `UNKNOWN_7`(opcode 7 其实是空洞)、
|
||||
`UNKNOWN_5096` 这类把操作数当操作码的垃圾 —— 换句话说旧的"8151 条指令流"是坏的。
|
||||
|
||||
本工具:
|
||||
1. 纯 Python 从 tdc.js 文本里取出字节码数组 `z`(复刻其内联的 base64 解码 + 游程展开),
|
||||
不需要任何 JS 运行时;
|
||||
2. 用**可达性驱动的 CFG 反汇编**(从 @0 出发,沿 fall-through / JUMP / JUMP_IF_TRUE /
|
||||
PUSH_TRY 的 catch 目标推进),只在真正可执行的地址上解码 —— 数据区自然留空,
|
||||
不再失步。
|
||||
|
||||
opcode 表来自对 VM 源码 AST 的推导(`G = [ ... ]` 中第 i 个 handler 即 opcode i,
|
||||
操作数个数 = handler 体内 `z[H++]` 的出现次数),已固化在下方。
|
||||
|
||||
用法:
|
||||
.venv/bin/python tools/tdc_disasm.py [tdc.js] # 默认 tcaptcha/core_js/tdc_sample.js
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# opcode 表 (从 VM 源码 G 数组推导)
|
||||
# 名字: 语义 (部分沿用旧工具命名); arity: 固定操作数字节数
|
||||
# 57 = CREATE_CLOSURE: 动态 (3 + 2*capCount + paramCount)
|
||||
# --------------------------------------------------------------------------- #
|
||||
OP_NAMES: Dict[int, str] = {
|
||||
0: "PUSH_STR_CHAR", 1: "IS_EXCEPTION", 2: "DUP", 3: "XOR", 4: "EQ_STRICT",
|
||||
5: "MOD", 6: "CALL_FUNC", 8: "GET_GLOBAL_PROP", 9: "MAKE_PROP_REF",
|
||||
10: "TYPEOF", 11: "DELETE", 12: "LOGICAL_NOT", 13: "EQ_LOOSE", 14: "SET_PROP",
|
||||
15: "GREATER_THAN", 17: "SWAP", 18: "IN", 19: "RET_TRUE", 20: "GREATER_EQ",
|
||||
22: "PUSH_IMM", 23: "MUL", 25: "LOAD_REG_INDIRECT", 26: "ITER_NEXT",
|
||||
28: "BIND_THIS", 29: "PUSH_NULL", 30: "BITWISE_AND", 31: "PUSH_IMM_ARRAY",
|
||||
32: "MAKE_REG_PROP_REF", 33: "SUB", 34: "CALL_METHOD", 35: "SHR", 36: "SHL",
|
||||
37: "JUMP_IF_TRUE", 38: "FOR_IN_KEYS", 39: "ADD", 41: "NEW_METHOD",
|
||||
42: "PUSH_UNDEFINED", 45: "PUSH_FALSE", 46: "STORE_REG", 47: "ALLOC_REG",
|
||||
48: "POP_TRY", 49: "THROW", 51: "POP", 52: "DIV", 53: "SET_STACK_LEN",
|
||||
54: "REVERSE_TOP2", 55: "PUSH_EMPTY_STR", 56: "PUSH_TRY", 57: "CREATE_CLOSURE",
|
||||
59: "GET_PROP", 60: "JUMP", 61: "NEW_FUNC", 62: "BITWISE_OR", 64: "REPLACE_TOP_IMM",
|
||||
65: "SHR_UNSIGNED", 66: "PUSH_TRUE", 68: "LOAD_REG_IMM", 69: "CLEAR_EXCEPTION",
|
||||
}
|
||||
|
||||
# 固定操作数字节数 (未列出的 opcode = 0 字节)
|
||||
ARITY: Dict[int, int] = {
|
||||
0: 1, 6: 1, 17: 1, 22: 1, 31: 1, 34: 1, 37: 1, 41: 1, 47: 1, 53: 1,
|
||||
56: 2, 60: 1, 61: 1, 64: 1, 68: 1,
|
||||
}
|
||||
DYNAMIC_ARITY = {57} # 需要读操作数才能定长
|
||||
OP_JUMP = 60
|
||||
OP_JUMP_IF_TRUE = 37
|
||||
OP_PUSH_TRY = 56 # operands = (catchTarget, catchVarReg)
|
||||
OP_CREATE_CLOSURE = 57
|
||||
|
||||
_B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 1. 纯 Python 取出字节码数组 z (复刻 tdc.js 内联的 I() 解码 + 游程展开)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _b64_decode_custom(text: str) -> str:
|
||||
"""复刻 tdc.js 内联的 I(A): 标准 base64 码表, 忽略非码表字符 (含换行)。"""
|
||||
g = re.sub(r"[=]+$", "", str(text))
|
||||
idx = {ch: i for i, ch in enumerate(_B64_ALPHABET)}
|
||||
out: List[str] = []
|
||||
b = 0
|
||||
n = 0
|
||||
for ch in g:
|
||||
w = idx.get(ch, -1)
|
||||
if w == -1: # ~w falsy -> 跳过 (换行/空白)
|
||||
continue
|
||||
b = (64 * b + w) if (n % 4) else w
|
||||
old = n
|
||||
n += 1
|
||||
if old % 4: # 每组 4 个字符产出 3 字节
|
||||
out.append(chr(255 & (b >> ((-2 * n) & 6))))
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def _unescape_js_string(raw: str) -> str:
|
||||
"""还原 JS 字符串字面量里的转义 (重点: `\\n` -> 真换行)。
|
||||
|
||||
注意: `n` 本身是合法 base64 字符, 若把 `\\n` 当成字面 `n` 会**污染**解码结果,
|
||||
必须先把转义还原成真实字符 (非码表字符随后被解码器忽略)。
|
||||
"""
|
||||
out: List[str] = []
|
||||
i = 0
|
||||
while i < len(raw):
|
||||
c = raw[i]
|
||||
if c == "\\" and i + 1 < len(raw):
|
||||
nxt = raw[i + 1]
|
||||
out.append({"n": "\n", "r": "\r", "t": "\t", "\\": "\\",
|
||||
'"': '"', "'": "'", "/": "/"}.get(nxt, nxt))
|
||||
i += 2
|
||||
continue
|
||||
out.append(c)
|
||||
i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def extract_bytecode(js_text: str) -> List[int]:
|
||||
"""从 tdc.js 文本里取出混沌 VM 的字节码数组 z (纯 Python, 无 JS 运行时)。
|
||||
|
||||
结构 (实测): `__TENCENT_CHAOS_VM(0, function(A){...}(["<base64>",[<int>,...]]), ...)`
|
||||
这里以 `__TENCENT_CHAOS_VM` 为锚点定位 `}(["...` 实参, 不再用"最长字符串"猜测。
|
||||
"""
|
||||
anchor = js_text.rfind("__TENCENT_CHAOS_VM")
|
||||
if anchor < 0:
|
||||
raise ValueError("未找到 __TENCENT_CHAOS_VM 锚点")
|
||||
|
||||
# 找锚点之后的 `}([` -> 数组起始的 `[`
|
||||
k = js_text.find("}([", anchor)
|
||||
if k < 0:
|
||||
k = js_text.find("[", js_text.find("function", anchor))
|
||||
start = js_text.find("[", k)
|
||||
if start < 0 or start + 1 >= len(js_text) or js_text[start + 1] != '"':
|
||||
raise ValueError("未能定位字节码实参数组")
|
||||
|
||||
q1 = start + 1
|
||||
q2 = js_text.find('"', q1 + 1) # base64 字面量内不含 `"`
|
||||
if q2 < 0:
|
||||
raise ValueError("base64 字面量未闭合")
|
||||
b64 = _unescape_js_string(js_text[q1 + 1:q2])
|
||||
|
||||
# 取内层整数数组 (括号配对)
|
||||
i = q2 + 1
|
||||
while i < len(js_text) and js_text[i] in " \t\r\n,":
|
||||
i += 1
|
||||
if i >= len(js_text) or js_text[i] != "[":
|
||||
raise ValueError("base64 之后不是整数数组")
|
||||
depth = 0
|
||||
j = i
|
||||
while j < len(js_text):
|
||||
if js_text[j] == "[":
|
||||
depth += 1
|
||||
elif js_text[j] == "]":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
j += 1
|
||||
# 该整数数组会混用科学计数法 (如 `1e9` / `16e3`), 需按数值解析
|
||||
ints = [int(float(x)) for x in js_text[i + 1:j].split(",") if x.strip() != ""]
|
||||
|
||||
# 3) 复刻 VM 的游程展开
|
||||
chars = _b64_decode_custom(b64) # G
|
||||
w: List[int] = ints
|
||||
|
||||
q: List[int] = []
|
||||
h = 0
|
||||
m = w.pop(0) if w else -1
|
||||
g = w.pop(0) if w else 0
|
||||
|
||||
def flush() -> None:
|
||||
nonlocal h, m, g
|
||||
while h == m:
|
||||
q.append(g)
|
||||
h += 1
|
||||
m = w.pop(0) if w else -1
|
||||
g = w.pop(0) if w else 0
|
||||
|
||||
for ch in chars:
|
||||
flush()
|
||||
q.append(ord(ch))
|
||||
h += 1
|
||||
flush()
|
||||
return q
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 2. 可达性驱动的 CFG 反汇编 (只在可执行地址解码)
|
||||
# --------------------------------------------------------------------------- #
|
||||
def _decode_one(z: List[int], pc: int) -> Tuple[Optional[int], List[int], Optional[List[str]], int]:
|
||||
"""解码 pc 处一条指令 -> (opcode, operands, note, next_fallthrough_pc)。
|
||||
|
||||
返回值 (None,...) 表示该地址无法构成合法指令。
|
||||
"""
|
||||
if pc >= len(z):
|
||||
return None, [], None, pc
|
||||
op = z[pc]
|
||||
if op > 69 or op not in OP_NAMES: # 空洞 opcode / 越界 -> 非法
|
||||
return None, [], None, pc + 1
|
||||
pos = pc + 1
|
||||
operands: List[int] = []
|
||||
note: Optional[List[str]] = None
|
||||
|
||||
if op in DYNAMIC_ARITY:
|
||||
# 57: target, capCount, paramCount, [reg, reg]*capCount, [reg]*paramCount
|
||||
if pos + 3 > len(z):
|
||||
return None, [], None, pos
|
||||
target, cap, param = z[pos], z[pos + 1], z[pos + 2]
|
||||
pos += 3
|
||||
need = 2 * cap + param
|
||||
if pos + need > len(z):
|
||||
return None, [], None, pos
|
||||
operands = z[pos:pos + need]
|
||||
pos += need
|
||||
caps = [f"r{operands[2*i]}=r{operands[2*i+1]}" for i in range(cap)]
|
||||
params = [f"p{i}->r{operands[2*cap+i]}" for i in range(param)]
|
||||
note = [f"target=@{target}", f"caps=[{','.join(caps)}]", f"params=[{','.join(params)}]"]
|
||||
else:
|
||||
n = ARITY.get(op, 0)
|
||||
if pos + n > len(z):
|
||||
return None, [], None, pos
|
||||
operands = z[pos:pos + n]
|
||||
pos += n
|
||||
if op in (OP_JUMP, OP_JUMP_IF_TRUE) and operands:
|
||||
note = [f"target=@{operands[0]}"]
|
||||
elif op == OP_PUSH_TRY and len(operands) == 2:
|
||||
note = [f"catchTarget=@{operands[0]}", f"catchVarReg=r{operands[1]}"]
|
||||
return op, operands, note, pos
|
||||
|
||||
|
||||
def disassemble_cfg(z: List[int], entry: int = 0) -> List[dict]:
|
||||
"""从 entry 出发按可达性反汇编; 返回按 pc 排序的指令列表。"""
|
||||
visited: Dict[int, dict] = {}
|
||||
work = [entry]
|
||||
while work:
|
||||
pc = work.pop()
|
||||
if pc in visited or pc >= len(z) or pc < 0:
|
||||
continue
|
||||
op, operands, note, nxt = _decode_one(z, pc)
|
||||
if op is None:
|
||||
# 非法地址 (落在数据区): 记录但不继续
|
||||
visited[pc] = {"pc": pc, "op": None, "name": "DATA/INVALID", "operands": [], "note": None}
|
||||
continue
|
||||
visited[pc] = {"pc": pc, "op": op, "name": OP_NAMES[op], "operands": operands, "note": note}
|
||||
if op == OP_JUMP:
|
||||
if operands:
|
||||
work.append(operands[0])
|
||||
continue # 无条件跳转: 无 fall-through
|
||||
if op == OP_JUMP_IF_TRUE:
|
||||
if operands:
|
||||
work.append(operands[0])
|
||||
work.append(nxt) # 条件跳转: 有 fall-through
|
||||
continue
|
||||
if op == OP_PUSH_TRY and operands:
|
||||
work.append(operands[0]) # catch 目标可达
|
||||
if op == OP_CREATE_CLOSURE:
|
||||
# 57 的操作数 [0] 是闭包体入口: 该段代码在闭包被调用时才执行, 但它是**代码**,
|
||||
# 必须作为遍历根加入, 否则大半个程序都算不到。
|
||||
work.append(z[pc + 1])
|
||||
work.append(nxt)
|
||||
return [visited[k] for k in sorted(visited)]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
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)
|
||||
insts = disassemble_cfg(z)
|
||||
invalid = [i for i in insts if i["op"] is None]
|
||||
print(f"bytecode length : {len(z)}")
|
||||
print(f"reachable instrs: {len(insts)} (invalid/data slots: {len(invalid)})")
|
||||
op_used = sorted({i["op"] for i in insts if i["op"] is not None})
|
||||
holes = [o for o in op_used if o not in OP_NAMES]
|
||||
print(f"opcodes used : {op_used}")
|
||||
print(f"illegal opcodes : {holes}")
|
||||
out = path.rsplit(".", 1)[0] + ".disasm.txt"
|
||||
with open(out, "w", encoding="utf-8") as f:
|
||||
for i in insts:
|
||||
if i["op"] is None:
|
||||
f.write(f"@{i['pc']:05d}: {i['name']}\n")
|
||||
else:
|
||||
extra = (" " + " ".join(i["note"])) if i["note"] else ""
|
||||
argc = (" " + str(i["operands"])) if (i["operands"] and not i["note"]) else ""
|
||||
f.write(f"@{i['pc']:05d}: {i['name']:<18}{extra}{argc}\n")
|
||||
print(f"written : {out}")
|
||||
for i in insts[:12]:
|
||||
print(" ", i)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+21
-194
@@ -1,200 +1,27 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Tencent Turing Captcha Chaos VM (TDC VM) Disassembler
|
||||
* --------------------------------------------------------
|
||||
* 依据 AGENTS.md §3.3 规范,将 TDC 混淆字节码反汇编为可读指令流与结构化操作。
|
||||
* [已废弃] 旧 TDC VM 反汇编器 (线性扫描, 依赖 Node + Babel)
|
||||
*
|
||||
* 实测它有两个缺陷, 会产出**错误**的反汇编, 故已废弃:
|
||||
* 1. opcode 表是手抄的, 而且对字节码里的**数据区**做线性扫描 —— 一旦扫进数据区就失步,
|
||||
* 产出 73 处 UNKNOWN (其中 16 处是 >69 的"不可能" opcode);
|
||||
* 2. 未把 CREATE_CLOSURE(57) 的闭包体 (target) 作为遍历根 —— 导致大半个程序不可达。
|
||||
*
|
||||
* 请改用**纯 Python** 的正确实现 (可达性 CFG 反汇编, 0 处非法 opcode):
|
||||
* .venv/bin/python tools/tdc_disasm.py <tdc.js>
|
||||
*
|
||||
* 本文件仅保留为兼容入口: 直接转发给上面的 Python 工具。
|
||||
*/
|
||||
|
||||
const fs = require("fs");
|
||||
const { spawnSync } = require("child_process");
|
||||
const path = require("path");
|
||||
const parser = require("@babel/parser");
|
||||
const generate = require("@babel/generator").default;
|
||||
|
||||
const OP_NAMES = {
|
||||
0: "PUSH_STR_CHAR",
|
||||
1: "IS_EXCEPTION",
|
||||
2: "DUP",
|
||||
3: "XOR",
|
||||
4: "EQ_STRICT",
|
||||
5: "MOD",
|
||||
6: "CALL_FUNC",
|
||||
8: "GET_GLOBAL_PROP",
|
||||
9: "MAKE_PROP_REF",
|
||||
10: "TYPEOF",
|
||||
11: "DELETE",
|
||||
12: "LOGICAL_NOT",
|
||||
13: "EQ_LOOSE",
|
||||
14: "SET_PROP",
|
||||
15: "GREATER_THAN",
|
||||
17: "SWAP",
|
||||
18: "IN",
|
||||
19: "RET_TRUE",
|
||||
20: "GREATER_EQ",
|
||||
22: "PUSH_IMM",
|
||||
23: "MUL",
|
||||
25: "LOAD_REG_INDIRECT",
|
||||
26: "ITER_NEXT",
|
||||
28: "BIND_THIS",
|
||||
29: "PUSH_NULL",
|
||||
30: "BITWISE_AND",
|
||||
31: "PUSH_IMM_ARRAY",
|
||||
32: "MAKE_REG_PROP_REF",
|
||||
33: "SUB",
|
||||
34: "CALL_METHOD",
|
||||
35: "SHR",
|
||||
36: "SHL",
|
||||
37: "JUMP_IF_TRUE",
|
||||
38: "FOR_IN_KEYS",
|
||||
39: "ADD",
|
||||
41: "NEW_METHOD",
|
||||
42: "PUSH_UNDEFINED",
|
||||
45: "PUSH_FALSE",
|
||||
46: "STORE_REG",
|
||||
47: "ALLOC_REG",
|
||||
48: "POP_TRY",
|
||||
49: "THROW",
|
||||
51: "POP",
|
||||
52: "DIV",
|
||||
53: "SET_STACK_LEN",
|
||||
54: "REVERSE_TOP2",
|
||||
55: "PUSH_EMPTY_STR",
|
||||
56: "PUSH_TRY",
|
||||
57: "CREATE_CLOSURE",
|
||||
59: "GET_PROP",
|
||||
60: "JUMP",
|
||||
61: "NEW_FUNC",
|
||||
62: "BITWISE_OR",
|
||||
64: "REPLACE_TOP_IMM",
|
||||
65: "SHR_UNSIGNED",
|
||||
66: "PUSH_TRUE",
|
||||
68: "LOAD_REG_IMM",
|
||||
69: "CLEAR_EXCEPTION",
|
||||
};
|
||||
const tdcFile = process.argv[2] || "tcaptcha/core_js/tdc_sample.js";
|
||||
const tool = path.join(__dirname, "tdc_disasm.py");
|
||||
|
||||
function extractBytecode(tdcPath) {
|
||||
const code = fs.readFileSync(tdcPath, "utf-8");
|
||||
const ast = parser.parse(code, { sourceType: "script" });
|
||||
const fn = ast.program.body[3].declarations[0].init.callee;
|
||||
const iFuncCode = generate(fn.body.body[1]).code;
|
||||
const returnStmt = fn.body.body[2];
|
||||
const vmCall = returnStmt.argument.expressions ? returnStmt.argument.expressions[1] : returnStmt.argument;
|
||||
const zCall = vmCall.arguments[1];
|
||||
const zCallCode = generate(zCall).code;
|
||||
|
||||
const runner = `
|
||||
${iFuncCode}
|
||||
(${zCallCode});
|
||||
`;
|
||||
return eval(runner);
|
||||
console.error("[deprecated] tools/tdc_vm_disasm.js 已废弃 -> 转发到 tools/tdc_disasm.py (纯 Python)");
|
||||
const r = spawnSync("python3", [tool, tdcFile], { stdio: "inherit" });
|
||||
if (r.error) {
|
||||
console.error("无法启动 python3; 请直接运行: .venv/bin/python tools/tdc_disasm.py " + tdcFile);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function disassemble(z) {
|
||||
let H = 0;
|
||||
const rawInstructions = [];
|
||||
const total = z.length;
|
||||
|
||||
while (H < total) {
|
||||
const pc = H;
|
||||
const op = z[H++];
|
||||
const opName = OP_NAMES[op] || `UNKNOWN_${op}`;
|
||||
const args = [];
|
||||
|
||||
switch (op) {
|
||||
case 0: // PUSH_STR_CHAR
|
||||
case 22: // PUSH_IMM
|
||||
case 31: // PUSH_IMM_ARRAY
|
||||
case 47: // ALLOC_REG
|
||||
case 53: // SET_STACK_LEN
|
||||
case 64: // REPLACE_TOP_IMM
|
||||
case 68: // LOAD_REG_IMM
|
||||
args.push(z[H++]);
|
||||
break;
|
||||
case 6: // CALL_FUNC
|
||||
case 17: // SWAP
|
||||
case 34: // CALL_METHOD
|
||||
case 41: // NEW_METHOD
|
||||
case 61: // NEW_FUNC
|
||||
args.push(`args=${z[H++]}`);
|
||||
break;
|
||||
case 37: // JUMP_IF_TRUE
|
||||
case 60: // JUMP
|
||||
args.push(`target=@${z[H++]}`);
|
||||
break;
|
||||
case 56: // PUSH_TRY
|
||||
args.push(`catchTarget=@${z[H++]}`);
|
||||
args.push(`catchVarReg=${z[H++]}`);
|
||||
break;
|
||||
case 57: { // CREATE_CLOSURE
|
||||
const targetPc = z[H++];
|
||||
const capCount = z[H++];
|
||||
const paramCount = z[H++];
|
||||
const captures = [];
|
||||
for (let m = 0; m < capCount; m++) {
|
||||
captures.push(`r${z[H++]}=r${z[H++]}`);
|
||||
}
|
||||
const params = [];
|
||||
for (let m = 0; m < paramCount; m++) {
|
||||
params.push(`p${m}->r${z[H++]}`);
|
||||
}
|
||||
args.push(`target=@${targetPc}`);
|
||||
args.push(`caps=[${captures.join(",")}]`);
|
||||
args.push(`params=[${params.join(",")}]`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// no extra operand
|
||||
break;
|
||||
}
|
||||
|
||||
rawInstructions.push({ pc, op, opName, args });
|
||||
}
|
||||
|
||||
// Pass 2: Fold PUSH_EMPTY_STR + PUSH_STR_CHAR ... into PUSH_STR "..."
|
||||
const folded = [];
|
||||
for (let i = 0; i < rawInstructions.length; i++) {
|
||||
const ins = rawInstructions[i];
|
||||
if (ins.opName === "PUSH_EMPTY_STR") {
|
||||
let str = "";
|
||||
let j = i + 1;
|
||||
while (j < rawInstructions.length && rawInstructions[j].opName === "PUSH_STR_CHAR") {
|
||||
str += String.fromCharCode(rawInstructions[j].args[0]);
|
||||
j++;
|
||||
}
|
||||
if (j > i + 1) {
|
||||
folded.push({
|
||||
pc: ins.pc,
|
||||
op: 0,
|
||||
opName: "PUSH_STR",
|
||||
args: [JSON.stringify(str)],
|
||||
});
|
||||
i = j - 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
folded.push(ins);
|
||||
}
|
||||
|
||||
return folded;
|
||||
}
|
||||
|
||||
// CLI usage
|
||||
if (require.main === module) {
|
||||
const tdcFile = process.argv[2] || "tcaptcha/core_js/tdc_sample.js";
|
||||
console.log(`Extracting and disassembling ${tdcFile}...`);
|
||||
const z = extractBytecode(tdcFile);
|
||||
console.log(`Bytecode size: ${z.length} bytes`);
|
||||
const insts = disassemble(z);
|
||||
console.log(`Total instructions: ${insts.length}`);
|
||||
|
||||
const outFile = tdcFile.replace(/\.js$/, ".disasm.txt");
|
||||
const lines = insts.map(
|
||||
(ins) => `@${ins.pc.toString().padStart(5, "0")}: ${ins.opName.padEnd(18)} ${ins.args.join(" ")}`
|
||||
);
|
||||
fs.writeFileSync(outFile, lines.join("\n"), "utf-8");
|
||||
console.log(`Disassembly written to ${outFile}`);
|
||||
|
||||
// Preview first 30 instructions
|
||||
console.log("\n--- Preview first 30 instructions ---");
|
||||
lines.slice(0, 30).forEach((l) => console.log(l));
|
||||
}
|
||||
|
||||
module.exports = { extractBytecode, disassemble, OP_NAMES };
|
||||
process.exit(r.status == null ? 1 : r.status);
|
||||
|
||||
Reference in New Issue
Block a user