做了什么: - 新增 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
126 lines
5.8 KiB
JavaScript
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));
|