- 解包 tcaptcha-frame bundle,圈定 122 个模块中的核心加签与通信模块 - 还原 __TENCENT_CHAOS_VM 架构:映射全部 70 个操作码语义并实现字符串折叠反汇编 - 提取 246 个核心字符串,全量排查并枚举 95 项环境检测与自动化探针特征 - 实现零浏览器依赖的深度 Proxy 探针,原生生成与会话自洽的 collect 与 eks
201 lines
5.1 KiB
JavaScript
201 lines
5.1 KiB
JavaScript
/**
|
|
* Tencent Turing Captcha Chaos VM (TDC VM) Disassembler
|
|
* --------------------------------------------------------
|
|
* 依据 AGENTS.md §3.3 规范,将 TDC 混淆字节码反汇编为可读指令流与结构化操作。
|
|
*/
|
|
|
|
const fs = require("fs");
|
|
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",
|
|
};
|
|
|
|
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);
|
|
}
|
|
|
|
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 };
|