diff --git a/tools/tdc_trace.js b/tools/tdc_trace.js new file mode 100644 index 0000000..c6b2317 --- /dev/null +++ b/tools/tdc_trace.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +/** + * 真值 PC 轨迹器 —— 给 tdc.js 的分发器打补丁, dump 真实执行的 PC 序列。 + * + * 补丁点(源码实测): `for(var M=!1;!M;)M=G[z[H++]]();` + * -> 记录 H(即将执行的 PC), 再正常分发。 + * + * 用途: 与 tools/tdc_vm_run.py (纯 Python 解释器) 的 PC 序列**逐条对拍**, + * 第一个不同的 PC 即执行流分歧点。 + * + * 用法: node tools/tdc_trace.js [最大记录数] > /tmp/real_pcs.json + */ +const fs = require("fs"); + +const file = process.argv[2] || "tcaptcha/core_js/tdc_sample.js"; +const limit = parseInt(process.argv[3] || "200000", 10); + +let src = fs.readFileSync(file, "utf8"); +const needle = "M=G[z[H++]]()"; +if (!src.includes(needle)) { + console.error("[!] 未找到分发器文本: " + needle); + process.exit(2); +} +src = src.replace(needle, "M=(globalThis.__PC.push(H),G[z[H++]]())"); + +globalThis.__PC = []; +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", "zh"], 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 pcs = globalThis.__PC.slice(0, limit); +process.stderr.write(`[trace] 执行 PC 数=${globalThis.__PC.length}${err ? " 顶层抛错: " + err.slice(0, 160) : ""}\n`); +process.stdout.write(JSON.stringify(pcs)); diff --git a/tools/tdc_vm_run.py b/tools/tdc_vm_run.py index 2fb190d..215a34e 100644 --- a/tools/tdc_vm_run.py +++ b/tools/tdc_vm_run.py @@ -226,6 +226,8 @@ class VM: self.ret = None self.key_hits: List[Any] = [] self.trace = bool(os.environ.get("TRACE")) + self.trace_full = bool(os.environ.get("TRACE_FULL")) + self.pcs: List[int] = [] # 全量 PC 轨迹 (TRACE_FULL 时记录) self.events: List[Any] = [] # 记录调用/闭包/返回等关键事件 # ---- 工具 @@ -250,6 +252,8 @@ class VM: if self.steps > self.max_steps: raise RuntimeError("step limit exceeded") op = z[self.H]; self.H += 1 + if self.trace_full: + self.pcs.append(self.H - 1) if self.trace and op in (1, 6, 19, 34, 41, 49, 57, 61): self.events.append((self.H - 1, op)) h = self.OP.get(op) @@ -313,7 +317,9 @@ class VM: args = E[-n:] if n else [] del E[len(E) - n:] fn = E.pop() - E.append(self.call(fn, args, None)) + if os.environ.get("DBGCALL"): + print(f"[CALL_FUNC @{self.H-2}] n={n} callee={type(fn).__name__} {str(fn)[:70]}", file=sys.stderr) + E.append(self.call(fn, args, self.c)) # 真实语义: fn.apply(c, B) -> this = c @op(8) # GET_GLOBAL_PROP def _8(): @@ -393,6 +399,10 @@ class VM: args = E[-n:] if n else [] del E[len(E) - n:] ref = E.pop() + if os.environ.get("DBGCALL"): + callee = getprop(ref[0], ref[1]) + print(f"[CALL_METHOD @{self.H-2}] n={n} ref={str(ref)[:90]} arity/type={type(ref).__name__}", file=sys.stderr) + print(f" stack tail: {[str(x)[:36] for x in E[-6:]]}", file=sys.stderr) E.append(self.call(getprop(ref[0], ref[1]), args, ref[0])) @op(37) # JUMP_IF_TRUE @@ -536,6 +546,10 @@ class VM: if isinstance(fn, tuple) and fn and fn[0] == "closure": sub = VM(self.z, self.c, self.max_steps - self.steps) sub.key_hits = self.key_hits # 子 VM 共享命中列表 + sub.pcs = self.pcs # 共享 PC 轨迹 (轨迹是全局的) + sub.events = self.events # 共享关键事件 + sub.trace = self.trace + sub.trace_full = self.trace_full sub.E = [JArr() for _ in range(32)] sub.E[0] = JArr([this]); sub.E[1] = JArr([JArr(args)]); sub.E[2] = JArr([fn]) for (q, r) in fn[2]: @@ -547,7 +561,11 @@ class VM: v = sub._run() self.steps += sub.steps return v - except Exception: + except Exception as exc: + if os.environ.get("DBGCALL"): + import traceback + print(f"[closure @{fn[1]} 执行异常] {type(exc).__name__}: {exc}", file=sys.stderr) + traceback.print_exc(limit=3) return JS_UNDEF if isinstance(fn, HostFn): return fn(*args)