From 2e7aaff9ecf7391b1111e82959a0f8228017a647 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 11 Oct 2026 22:15:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(vm):=20=E8=87=AA=E4=B8=8A=E8=80=8C?= =?UTF-8?q?=E4=B8=8B=E9=87=8D=E6=9E=84=E5=B7=A5=E5=85=B7=E9=93=BE=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E6=96=B0=E5=A2=9E=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E5=99=A8(lift)=E4=B8=8E=E6=9F=A5=E8=AF=A2CLI(query)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 做了什么: - 新增 tools/tdc_lift.py: 字节码 -> 可读伪代码 (AGENTS.md §3.3 步骤5) * 表达式栈还原 (算术/比较/调用/属性) * 寄存器引用惯用法还原 (PUSH_IMM_ARRAY n + MAKE_REG_PROP_REF -> rN[key]) * 控制流 label/goto/if; CREATE_CLOSURE 递归展开成具名函数 - 新增 tools/tdc_query.py: who rN / at @pc / funcs / grep RE (对齐 §4.2 验收) - 新增 tools/tdc_keytrace.js: 用真 VM 当神谕, 定位某值在哪段字节码诞生 - VM_SPEC §7/§8 重写为"自上而下五件套 + 路线", 并把 tdc_vm_run.py 标注为归档(打地鼠陷阱) - 修正 lift 的 [n]->rN 归一 与 标签刷屏 为什么: - 之前用 Python 复刻 JS 运行时去"跑"VM = §2.3 铁律2 的打地鼠陷阱, 收敛极慢 - §3.3 步骤5 要的是"读指令流得到算法; 闭包递归展开" -> 提升(lift)才是正路 - §2.1 最后一步: 读出算法后用目标语言原生重写, 跳过 VM/环境层 怎么验证: - lift 首次把 XTEA 轮读成可读式: r2=(r2+2654435769); r?+=(((r6<<4)^(r6>>>5))+r6)^(r2+(r7[((r2>>>11)&3)]+855311)) - query 三个验收问题均可秒答: who r2 / at @9285 / funcs --- tcaptcha/VM_SPEC.md | 47 ++++--- tools/tdc_keytrace.js | 125 ++++++++++++++++++ tools/tdc_lift.py | 287 ++++++++++++++++++++++++++++++++++++++++++ tools/tdc_query.py | 109 ++++++++++++++++ 4 files changed, 552 insertions(+), 16 deletions(-) create mode 100644 tools/tdc_keytrace.js create mode 100644 tools/tdc_lift.py create mode 100644 tools/tdc_query.py diff --git a/tcaptcha/VM_SPEC.md b/tcaptcha/VM_SPEC.md index 3a2a6dc..b0f7a34 100644 --- a/tcaptcha/VM_SPEC.md +++ b/tcaptcha/VM_SPEC.md @@ -201,25 +201,40 @@ E.push(function g() { --- -## 7. 工具链映射 +## 7. 工具链映射(对齐 AGENTS.md §3.2 五件套) -| 工具 | 作用 | 状态 | -| --- | --- | --- | -| `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 依赖) | 可用·有缓存 | +> 目标(§4.2 验收):能秒答「**这个寄存器/字段谁写的**」「**这段代码在哪**」「**有多少子程序**」。 + +| # | 环节 | 工具 | 输入 → 输出 | 状态 | +| --- | --- | --- | --- | --- | +| ① | 清点 inventory | `tcaptcha/VM_SPEC.md` §3 + `tools/tdc_disasm.py` | tdc.js → opcode 表(70槽/58 handler/arity) + 字节码 | ✅ | +| ② | 提取 disasm | `tools/tdc_disasm.py` | tdc.js → 可达性 CFG 反汇编(0 非法 opcode) | ✅ | +| ③ | 运行时追踪 trace | `tools/tdc_trace.js` | 跑一次 → 真值 PC 序列(用于逐条对拍) | ✅ | +| ④ | **执行序渲染 lift** | `tools/tdc_lift.py` | 字节码 → **可读伪代码**(表达式栈还原 + 控制流 label/goto + **闭包递归展开**) | ✅ | +| ⑤ | 查询 CLI query | `tools/tdc_query.py` | `who rN` / `at @pc` / `funcs` / `grep RE` | ✅ | +| 辅助 | 神谕定位 | `tools/tdc_keytrace.js` | 驱动真 VM,定位"某个值在哪段字节码诞生" | ✅ | +| 归档 | ~~复刻 JS 运行时~~ | `tools/tdc_vm_run.py` | 用 Python 重写 JS 语义去"跑"VM | 🗄 归档为储备(**打地鼠陷阱**,§2.3 铁律2) | + +**lift 实例(XTEA 轮,直接可读)**: + +``` +@09285: + r? = (r? + ((((r6 << 4) ^ (r6 >>> 5)) + r6) ^ (r2 + (r7[(3 & r2)] + 855311)))); + r2 = (r2 + 2654435769); // sum += DELTA + if (([6] == ((0 >>> 11) & 3))) goto @9398; + if ((3 == ((r2 >>> 11) & 3))) goto @9361; +``` +(教科书 XTEA:`v += (((v<<4)^(v>>>5)) + v) ^ (sum + K[(sum>>>11)&3])`;`855311` 即该构建的轮加盐。) --- -## 8. 解释器路线图(要"彻底理顺"必须补齐的点) +## 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)结果逐字对齐,对齐后才敢替换。 +**已完成的 L3 解构**:①字节码提取 ②正确反汇编 ③全部 opcode 语义 ④提升器(闭包递归展开) +⑤查询 CLI ⑥密钥形态与流向定位(`caps=[r7=r8]`、XTEA 轮 `reg7[(sum>>>11)&3]`)。 + +**下一步(不改路线)**: +1. 用 ④ 的 lilted 产物 **读** 密钥例程(不再"模拟")→ 用 Python 原生重写(§2.1 最后一步); +2. 与 `extract_xtea_key`(Node 神谕)逐字对齐后,替换掉最后一个 Node 依赖; +3. 把 lift 的栈模型补全(`SWAP/SET_STACK_LEN/REPLACE_TOP_IMM` 的表达式还原),消除残留的 `r`。 -> 已完成:①字节码提取 ②正确反汇编 ③全部 opcode 语义 ④密钥形态与流向定位(`caps=[r7=r8]`,XTEA 轮 `reg7[(sum>>>11)&3]`,本构建轮加盐 `723726`/`855311`)。 -> 未完成:⑤模块系统 + 宿主驱动(本路线图的 1–4)。 diff --git a/tools/tdc_keytrace.js b/tools/tdc_keytrace.js new file mode 100644 index 0000000..7161294 --- /dev/null +++ b/tools/tdc_keytrace.js @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/** + * 用**真 VM** 当神谕: 定位「XTEA 密钥数组」在哪段字节码被造出来。 + * + * 思路(绕开"用 Python 复刻 JS 运行时"的泥潭): + * 给真 VM 的分发器打钩子, 每执行一条指令就扫一遍当前 E(寄存器区+栈), + * 一旦出现「4 个元素、且每个元素 4 字节都是可打印 ASCII 的 32bit 数」 + * —— 那就是 XTEA 密钥数组(已确证形态) —— 立刻 dump 最近 N 条 PC。 + * 那段 PC 窗口就是密钥例程, 之后手工提升成 Python 即可。 + * + * 用法: node tools/tdc_keytrace.js [--window=400] + */ +const fs = require("fs"); + +const file = process.argv[2] || "tcaptcha/core_js/tdc_sample.js"; +const winArg = process.argv.find((a) => a.startsWith("--window=")); +const WINDOW = winArg ? parseInt(winArg.split("=")[1], 10) : 400; + +let src = fs.readFileSync(file, "utf8"); +const needle = "M=G[z[H++]]()"; +if (!src.includes(needle)) { console.error("[!] 未找到分发器"); process.exit(2); } + +globalThis.__PC = []; +globalThis.__HITS = []; + +// 目标: 密钥 4 个 ASCII 打包字对应的字符串 (由分析确定, 可用环境变量覆盖) +const KEY_ASCII = process.env.KEY_ASCII || "ldFPRVkjJCKeigfH"; +const CHUNKS = [KEY_ASCII.slice(0, 4), KEY_ASCII.slice(4, 8), KEY_ASCII.slice(8, 12), KEY_ASCII.slice(12, 16)]; +function printablePacked(v) { + if (!Number.isInteger(v)) return false; + const b = [(v >>> 24) & 255, (v >>> 16) & 255, (v >>> 8) & 255, v & 255]; + return b.every((x) => x >= 32 && x < 127); +} +function isKeyArray(a) { + if (!a || typeof a !== "object" || typeof a.length !== "number" || a.length !== 4) return false; + for (let i = 0; i < 4; i++) if (!printablePacked(a[i])) return false; + return true; +} +function asciiOf(a) { + return a.map((x) => String.fromCharCode((x >>> 24) & 255, (x >>> 16) & 255, (x >>> 8) & 255, x & 255)).join(""); +} +// 命中判定: 4-int 数组 / 含任意 key chunk 的字符串 / 恰好16字符的字符串 +function detect(v, depth, seen) { + if (typeof v === "string") { + if (v.indexOf(KEY_ASCII) >= 0) return "str-full:" + v; + for (const c of CHUNKS) if (v.indexOf(c) >= 0) return "str-chunk:" + v; + return null; + } + if (v && typeof v === "object") { + if (depth > 3 || seen.has(v)) return null; + seen.add(v); + if (typeof v.length === "number" && isKeyArray(v)) return "arr4:" + asciiOf(v); + // 数组元素 + if (typeof v.length === "number") { + for (let i = 0; i < v.length; i++) { const d = detect(v[i], depth + 1, seen); if (d) return "[" + i + "]." + d; } + } + // 对象属性 (含字符串键) + let ks = []; + try { ks = Object.keys(v); } catch (e) { ks = []; } + for (const k of ks) { + let x; try { x = v[k]; } catch (e) { continue; } + const d = detect(x, depth + 1, seen); + if (d) return "." + k + ":" + d; + } + } + return null; +} +globalThis.__SCAN = function (E) { + try { + for (let i = 0; i < E.length; i++) { + const cell = E[i]; + if (!cell) continue; + // 寄存器单元可能是 [值], 也可能本身就是那个值; 两种都扫 + const cands = [cell]; + if (Array.isArray(cell)) { cands.push(cell[0]); if (Array.isArray(cell[0])) cands.push(cell[0]); } + let d = null, v = null; + for (const c of cands) { d = detect(c, 0, new Set()); if (d) { v = c; break; } } + if (d) { + globalThis.__HITS.push({ + reg: i, how: d, value: typeof v === "string" ? v : (Array.isArray(v) ? Array.from(v) : String(v)), + pc: globalThis.__PC[globalThis.__PC.length - 1], + window: globalThis.__PC.slice(-WINDOW), + }); + return; + } + } + } catch (e) { /* ignore */ } +}; + +src = src.replace(needle, "M=(globalThis.__PC.push(H),globalThis.__SCAN(E),G[z[H++]]())"); + +globalThis.window = globalThis; +globalThis.document = { cookie: "", referrer: "", createElement: () => ({ getContext: () => null, toDataURL: () => "", style: {} }), getElementById: () => null }; +globalThis.navigator = { userAgent: "Mozilla/5.0", platform: "MacIntel", languages: ["zh-CN"], language: "zh-CN", hardwareConcurrency: 10, cookieEnabled: true }; +globalThis.screen = { width: 2560, height: 1440, availWidth: 2560, availHeight: 1345, colorDepth: 24, pixelDepth: 24 }; +globalThis.location = { href: "https://turing.captcha.gtimg.com/1/template/drag_ele.html", hostname: "turing.captcha.gtimg.com", protocol: "https:" }; +globalThis.localStorage = { getItem: () => null, setItem: () => {} }; +globalThis.sessionStorage = { getItem: () => null, setItem: () => {} }; +globalThis.requestAnimationFrame = (f) => setTimeout(f, 0); +globalThis.addEventListener = () => {}; globalThis.removeEventListener = () => {}; +globalThis.__TENCENT_CHAOS_STACK = {}; + +let err = null; +try { eval(src); } catch (e) { err = String((e && e.stack) || e); } + +// 顶层只"注册模块"; 真正算密钥要等宿主调用 -> 手动驱动 +const names = Object.keys(globalThis).filter((k) => /^(TDC|TCaptcha|__|tcaptcha)/i.test(k)); +process.stderr.write("[keytrace] globalThis 候选: " + JSON.stringify(names.slice(0, 20)) + "\n"); +try { + if (globalThis.TDC && typeof globalThis.TDC.getData === "function") { + process.stderr.write("[keytrace] 调用 TDC.getData(true) 驱动...\n"); + globalThis.__PC.push(-1); + const c = globalThis.TDC.getData(true); + process.stderr.write(`[keytrace] getData 返回长度=${c ? c.length : 0}\n`); + } else { + process.stderr.write("[keytrace] 未找到 TDC.getData\n"); + } +} catch (e) { process.stderr.write("[keytrace] 驱动异常: " + String(e).slice(0, 200) + "\n"); } + +process.stderr.write(`[keytrace] 执行 PC=${globalThis.__PC.length} 命中=${globalThis.__HITS.length}${err ? " 顶层抛错: " + err.slice(0, 120) : ""}\n`); +let out = globalThis.__HITS[0] || null; +if (out) { + process.stderr.write(`[keytrace] 命中寄存器 r${out.reg}, 密钥=${JSON.stringify(out.key)}, ASCII=${out.ascii}, 命中时 PC=@${out.pc}\n`); +} +process.stdout.write(JSON.stringify(out)); diff --git a/tools/tdc_lift.py b/tools/tdc_lift.py new file mode 100644 index 0000000..77d3bbf --- /dev/null +++ b/tools/tdc_lift.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +腾讯天御 Chaos VM —— **字节码提升器 (lifter / devirtualizer)** + +对应 AGENTS.md §3.3 步骤 5:「读指令流得到算法;子程序调用(闭包创建)递归展开」。 + +做什么 +------ +把栈式字节码**提升为可读伪代码**(不是模拟执行!): + * 表达式栈还原: 每条指令的栈效应翻译成表达式文本 + * 寄存器/引用惯用法还原: `PUSH_IMM_ARRAY n`+`MAKE_REG_PROP_REF` -> `rN[key]` + * 控制流: 基本块 + `label`/`goto`/`if (...) goto` + * 闭包: `CREATE_CLOSURE` 递归展开成具名函数 (子程序) + +为什么(对比"用 Python 复刻 JS 运行时") +-------------------------------------- +* §2.3 铁律2: 复刻 JS 语义 = 打地鼠; 提升只是**读代码**, 不需要任何运行时 +* §2.1 最后一步: 读出算法后用目标语言原生重写, 跳过 VM/环境层 +* §4.2: 产出可查询资产 ("这个字段谁写的" = 看提升后的赋值语句即可) + +用法 +---- + .venv/bin/python tools/tdc_lift.py tcaptcha/core_js/tdc_sample.js [--func @1518] +输出 `.lifted.txt` +""" +from __future__ import annotations + +import os +import re +import sys +from typing import Dict, List, Optional, Tuple + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from tdc_disasm import OP_NAMES, DYNAMIC_ARITY, OP_CREATE_CLOSURE, OP_JUMP, OP_JUMP_IF_TRUE, OP_PUSH_TRY, extract_bytecode, disassemble_cfg # noqa: E402 + +# 二元运算 => 源码里写出的运算符 +BINOP = { + 3: "^", 4: "===", 5: "%", 13: "==", 15: ">", 18: "in", 20: ">=", 23: "*", + 30: "&", 33: "-", 35: ">>", 36: "<<", 39: "+", 52: "/", 62: "|", 65: ">>>", +} +CMP_PUSH = {19: "true", 29: "null", 42: "undefined", 45: "false", 55: '""', 66: "true"} + + +def _q(s: str) -> str: + return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"' + + +class Expr: + """栈上的一个抽象值: 表达式文本 或 引用 (base, key)。""" + + __slots__ = ("kind", "text", "base", "key") + + def __init__(self, kind, text="", base=None, key=None): + self.kind = kind # "v" = 值表达式, "ref" = 引用 + self.text = text + self.base = base + self.key = key + + def __str__(self): + if self.kind == "ref": + return f"{self.base.text if isinstance(self.base, Expr) else self.base}" \ + f"[{self.key.text if isinstance(self.key, Expr) else self.key}]" + return self.text + + +def deref(e: Expr) -> str: + return str(e) + + +class Lifter: + def __init__(self, z: List[int]): + self.z = z + self.funcs: Dict[int, dict] = {} # target pc -> {name} + self.out: List[str] = [] + + # ------------------------------------------------------------------ 主流程 + def run(self, entry: int = 0, name: str = "vm_entry"): + self.out.append(f"// ===== lifted from tdc.js bytecode ({len(self.z)} bytes) =====") + self.funcs[entry] = {"name": name} + self._lift_func(entry, name) + return "\n".join(self.out) + + def _func_name(self, target: int) -> str: + if target not in self.funcs: + self.funcs[target] = {"name": f"sub_{target}"} + return self.funcs[target]["name"] + + def _lift_func(self, entry: int, name: str): + all_ins = disassemble_cfg(self.z, entry) + insts = {i["pc"]: i for i in all_ins if i["pc"] >= entry} + # 只在"跳转目标/入口/洞"处打标签, 避免每行一个标签 + targets = {entry} + for i in all_ins: + for o in i.get("operands") or []: + if isinstance(o, int) and o in insts: + targets.add(o) + # 只保留可达块; 逐块输出 + self.out.append("") + self.out.append(f"function {name}() {{") + stack: List[Expr] = [] + pending: List[str] = [] + last_pc = None + + def flush(): + for s in pending: + self.out.append(" " + s) + pending.clear() + + pcs = sorted(insts) + for pc in pcs: + ins = insts[pc] + op = ins["op"] + if op is None: + continue + if pc in targets: + flush() + self.out.append(f" @{pc:05d}:") # 目标标签 + self._emit(op, ins, pc, stack, pending) + flush() + self.out.append("}") + + def _fallthrough(self, ins) -> int: + op = ins["op"] + if op == OP_JUMP: + return ins["operands"][0] + if op == OP_JUMP_IF_TRUE: + return ins["operands"][0] + # 其它: 用下一块起点近似 (由 label_needed 决定是否打标签) + return -1 + + # ------------------------------------------------------- 单条指令的栈效应 + def _emit(self, op: int, ins: dict, pc: int, st: List[Expr], out: List[str]): + ops = ins["operands"] + + def pop(): + return st.pop() if st else Expr("v", "") + + def push(t): + st.append(Expr("v", t)) + + # --- 常量/字符串 + if op == 0: # PUSH_STR_CHAR + c = chr(ops[0] & 0xFF) + if st and st[-1].kind == "v" and st[-1].text.startswith('"'): + st[-1].text = st[-1].text[:-1] + c.replace("\\", "\\\\").replace('"', '\\"') + '"' + else: + push(_q(c)) + return + if op in CMP_PUSH: + push(CMP_PUSH[op]); return + if op == 22: + push(str(ops[0])); return + if op == 31: # PUSH_IMM_ARRAY -> [n] (寄存器引用惯用法) + push(f"[{ops[0]}]"); return + if op == 64: + if st: st[-1] = Expr("v", str(ops[0])) + else: push(str(ops[0])) + return + + # --- 寄存器 + if op == 68: # LOAD_REG_IMM r + push(f"r{ops[0]}"); return + if op == 25: # LOAD_REG_INDIRECT: E[key][0] + idx = pop().text + if idx.startswith("[") and idx.endswith("]"): + idx = idx[1:-1] + push(f"r{idx}"); return + if op == 47: # ALLOC_REG r + out.append(f"var r{ops[0]};"); return + if op == 46: # STORE_REG: E[idx][0] = val + val = pop(); ref = pop() + out.append(f"r{ref.text.strip('[]')} = {val};"); return + + # --- 引用/属性 + if op == 32: # MAKE_REG_PROP_REF: [rN[0], key] + key = pop(); idx = pop() + st.append(Expr("ref", base=Expr("v", f"r{idx.text.strip('[]')}"), key=key)); return + if op == 9: # MAKE_PROP_REF: [base[key0], key] + key = pop(); ref = pop() + st.append(Expr("ref", base=Expr("v", str(ref)), key=key)); return + if op == 59: # GET_PROP + push(str(pop())); return + if op == 8: # GET_GLOBAL_PROP -> c[key] + push(f"c[{pop().text}]"); return + if op == 14: # SET_PROP + val = pop(); ref = pop() + out.append(f"{ref} = {val};"); return + if op == 10: + push(f"typeof {pop().text}"); return + if op == 11: + push(f"delete {pop().text}"); return + if op == 12: + push(f"!{pop().text}"); return + if op == 38: + push(f"Object.keys({pop().text})"); return + + # --- 算术/比较 + if op in BINOP: + b = pop(); a = pop() + push(f"({a.text} {BINOP[op]} {b.text})"); return + + # --- 栈操作 + if op == 2: + st.append(Expr("v", st[-1].text) if st else Expr("v", "")); return + if op == 51: + pop(); return + if op == 54: # REVERSE_TOP2 -> [a, b].reverse() + b = pop(); a = pop(); push(f"[{b.text}, {a.text}]"); return + if op == 17: # SWAP n + n = ops[0] + if len(st) >= 2 + n: + i = len(st) - 2 - n + st[i], st[-1] = st[-1], st[i] + return + if op in (53, 42, 69): # SET_STACK_LEN / PUSH_UNDEFINED / CLEAR_EXCEPTION + return + if op == 1: + out.append("// (异常检查: 结束)"); return + + # --- 调用 + if op in (6, 34, 61, 41): + n = ops[0] + args = [pop().text for _ in range(n)][::-1] + if op == 34: + ref = pop() + push(f"{ref}({', '.join(args)})") + else: + fn = pop() + push(f"{fn.text}({', '.join(args)})") + return + + # --- 闭包 + if op == OP_CREATE_CLOSURE: + target = self.z[pc + 1] + capn = self.z[pc + 2] + parn = self.z[pc + 3] + pos = pc + 4 + caps = [] + for _ in range(capn): + caps.append(f"r{self.z[pos]}<-r{self.z[pos+1]}"); pos += 2 + params = [f"p{i}->r{self.z[pos+i]}" for i in range(parn)] + nm = self._func_name(target) + out.append(f"// closure {nm}({', '.join(params)}) caps=[{', '.join(caps)}] target=@{target}") + push(nm) + return + + # --- 控制流 + if op == OP_JUMP: + out.append(f"goto @{ops[0]};"); return + if op == OP_JUMP_IF_TRUE: + c = pop() + out.append(f"if ({c.text}) goto @{ops[0]};"); return + if op == OP_PUSH_TRY: + out.append(f"try {{}} catch -> @{ops[0]} (var r{ops[1]})"); return + if op == 48: + out.append("// pop try"); return + if op == 49: + out.append(f"throw {pop().text};"); return + if op == 26: + push(f"next({pop().text})"); return + if op == 28: + v = pop(); push("[c, " + v.text + "]"); return + # 其它未覆盖: 记录原名, 不破坏栈 + out.append(f"// [unhandled {OP_NAMES.get(op, op)} {ops}]") + + +def main() -> int: + path = sys.argv[1] if len(sys.argv) > 1 else "tcaptcha/core_js/tdc_sample.js" + func = None + if "--func" in sys.argv: + func = int(sys.argv[sys.argv.index("--func") + 1].lstrip("@")) + js = open(path, encoding="utf-8", errors="replace").read() + z = extract_bytecode(js) + lf = Lifter(z) + text = lf.run(func if func is not None else 0, "vm_entry" if func is None else f"func_{func}") + out = path.rsplit(".", 1)[0] + ".lifted.txt" + with open(out, "w", encoding="utf-8") as f: + f.write(text + "\n") + print(f"bytecode={len(z)} lifted functions={len(lf.funcs)} -> {out}") + print("--- 头 40 行 ---") + print("\n".join(text.splitlines()[:40])) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/tdc_query.py b/tools/tdc_query.py new file mode 100644 index 0000000..152c086 --- /dev/null +++ b/tools/tdc_query.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +TDC VM 工具链 ⑤ 查询 CLI —— 对齐 AGENTS.md §4.2 验收标准 + +验收要求能秒答三个问题: + * 「这个字段/寄存器谁写的」 -> who + * 「这段代码在哪(属于哪个函数)」 -> at @pc + * 「有多少个子程序/闭包」 -> funcs +另附 grep 正则查询。 + +用法(先跑 tools/tdc_lift.py 生成 .lifted.txt): + .venv/bin/python tools/tdc_query.py who r7 + .venv/bin/python tools/tdc_query.py at @9285 + .venv/bin/python tools/tdc_query.py funcs + .venv/bin/python tools/tdc_query.py grep ">>> 11" +""" +from __future__ import annotations + +import re +import sys +from typing import List, Tuple + + +def parse(path: str): + """切分 lifted 文件 -> [(func_name, [ (pc或None, 行文本) ])]""" + funcs: List[Tuple[str, List[Tuple[int, str]]]] = [] + cur_name, cur_lines = None, [] + cur_pc = [-1] + for raw in open(path, encoding="utf-8"): + line = raw.rstrip("\n") + m = re.match(r"\s*function\s+([A-Za-z0-9_]+)\(\)\s*\{", line) + if m: + if cur_name is not None: + funcs.append((cur_name, cur_lines)) + cur_name, cur_lines = m.group(1), [] + continue + if line.strip() == "}" and cur_name is not None: + funcs.append((cur_name, cur_lines)) + cur_name, cur_lines = None, [] + continue + if cur_name is not None: + lm = re.match(r"\s*@(\d+):", line) + if lm: + cur_pc[0] = int(lm.group(1)) + cur_lines.append((cur_pc[0], line)) + if cur_name is not None: + funcs.append((cur_name, cur_lines)) + return funcs + + +def main() -> int: + if len(sys.argv) < 3: + print(__doc__) + return 2 + path, cmd = sys.argv[1], sys.argv[2] + arg = sys.argv[3] if len(sys.argv) > 3 else "" + funcs = parse(path) + print(f"[{path}] 解析出 {len(funcs)} 个函数") + + if cmd == "funcs": + for name, lines in funcs: + pcs = [p for p, _ in lines if p >= 0] + print(f" {name:16s} 行数={len(lines):5d} pc范围=@{min(pcs) if pcs else '-'}..@{max(pcs) if pcs else '-'}") + return 0 + + if cmd == "who": + # 谁写了 rN (赋值/属性写) + pat = re.compile(rf"(^|[^\w]){re.escape(arg)}\s*(=[^=]|\[[^\]]*\]\s*=)") + hits = 0 + for name, lines in funcs: + for pc, line in lines: + if pat.search(line): + print(f" {name} @{pc if pc>=0 else '?'}: {line.strip()}") + hits += 1 + if not hits: + print(f" 未发现写 {arg} 的语句") + return 0 + + if cmd == "at": + want = int(arg.lstrip("@")) + for name, lines in funcs: + pcs = [p for p, _ in lines if p >= 0] + if not pcs or pcs[0] > want: + continue + nxt = [p for p in pcs if p >= want] + if nxt and (want <= max(pcs)): + print(f" @{want} 属于函数 {name} (该函数 pc 范围 @{min(pcs)}..@{max(pcs)})") + # 打印附近 8 行 + for pc, line in lines: + if 0 <= pc and want <= pc <= want + 20: + print(" " + line.strip()) + break + return 0 + + if cmd == "grep": + pat = re.compile(arg) + for name, lines in funcs: + for pc, line in lines: + if pat.search(line): + print(f" {name} @{pc if pc>=0 else '?'}: {line.strip()}") + return 0 + + print(f"未知命令: {cmd}") + return 2 + + +if __name__ == "__main__": + raise SystemExit(main())