Files
hello_js_reverse_skill/tools/tdc_keytrace.js
T
yml2213 2e7aaff9ec feat(vm): 自上而下重构工具链 —— 新增提升器(lift)与查询CLI(query)
做了什么:
- 新增 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
2026-10-11 22:15:43 +08:00

126 lines
5.8 KiB
JavaScript

#!/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 <tdc.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));