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:
+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