/** * 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 };