做了什么:
- tools/tdc_probe.js 注入自包含钩子(window.__H): 记录 PC 轨迹, 并在指定 PC
dump 指定寄存器 (默认 9109 / E[7]), 通过 report.__xtea / report.__pc_len 回传
- tools/tdc_keytrace.js 同步支持 TARGET_PC 钩子
为什么:
- 需要"某个值在哪段字节码诞生"的运行时神谕(AGENTS.md §3.2 第3条: 与运行时观测对齐)
- 钩子自包含写在沙箱 window 上(沙箱内没有 globalThis)
怎么验证(本轮的关键结论):
- 命中: @9109 入口 E[7] = [[1817655617,1381395306,1245924197,1767660346]];
其后 PC 序列 tail = [...9021,9022,9023,9024,9026,9028,9030,9109]
-> 说明 XTEA 闭包是在 @9030 CALL_FUNC[2] 被调用的
- 交叉验证: 用 extract_xtea_key 的密钥解密 probe 的 collect -> {"cd":[0,10,"top",...
(真明文) => extract_xtea_key 正确;
而 E[7] 解不开 => E[7] 是"基字", 不是生效密钥
- 结构确认: real[0]-cap[0]=855311, real[3]-cap[3]=723726 (XTEA 轮里的可见立即数)
=> 有效密钥 = 脚本基字 + 两个轮加盐
364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
/**
|
|
* Tencent Turing Captcha TDC Deep Proxy Runtime Probe
|
|
* ---------------------------------------------------
|
|
* 依据 AGENTS.md §4.3 与 §2.3 规范,通过深度 Proxy 探针拦截并记录 TDC 执行时
|
|
* 实际读取的全部环境属性清单与调用时序,彻底告别盲目补环境。
|
|
*/
|
|
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const vm = require("vm");
|
|
|
|
function createDeepProxy(target, prefix, logSet) {
|
|
return new Proxy(target, {
|
|
get(obj, prop, receiver) {
|
|
if (typeof prop === "symbol" || prop === "inspect" || prop.startsWith("_")) {
|
|
return Reflect.get(obj, prop, receiver);
|
|
}
|
|
const BUILTIN_TYPES = new Set([
|
|
"String", "Number", "Boolean", "Array", "Object", "Function",
|
|
"RegExp", "Date", "Math", "JSON", "Error", "TypeError",
|
|
"Uint8Array", "Uint8ClampedArray", "parseInt", "parseFloat",
|
|
"encodeURIComponent", "decodeURIComponent", "setTimeout",
|
|
"clearTimeout", "setInterval", "clearInterval"
|
|
]);
|
|
if (BUILTIN_TYPES.has(String(prop))) {
|
|
return Reflect.get(obj, prop, receiver);
|
|
}
|
|
const fullName = `${prefix}.${String(prop)}`;
|
|
logSet.add(`GET: ${fullName}`);
|
|
|
|
const val = Reflect.get(obj, prop, receiver);
|
|
if (val !== null && typeof val === "object" && !(val instanceof RegExp)) {
|
|
return createDeepProxy(val, fullName, logSet);
|
|
}
|
|
if (typeof val === "function") {
|
|
const fnWrapper = function (...args) {
|
|
logSet.add(`CALL: ${fullName}(${args.map((a) => JSON.stringify(a)).join(", ")})`);
|
|
return val.apply(this === receiver ? obj : this, args);
|
|
};
|
|
fnWrapper.prototype = val.prototype;
|
|
return fnWrapper;
|
|
}
|
|
return val;
|
|
},
|
|
set(obj, prop, value, receiver) {
|
|
const fullName = `${prefix}.${String(prop)}`;
|
|
logSet.add(`SET: ${fullName}`);
|
|
return Reflect.set(obj, prop, value, receiver);
|
|
},
|
|
});
|
|
}
|
|
|
|
function createVirtualDate(initialTime) {
|
|
let currentTime = initialTime || (Date.now() - 3200);
|
|
const RealDate = Date;
|
|
|
|
function FakeDate(...args) {
|
|
if (!(this instanceof FakeDate)) {
|
|
return new RealDate(currentTime).toString();
|
|
}
|
|
if (args.length === 0) {
|
|
return new RealDate(currentTime);
|
|
}
|
|
return new RealDate(...args);
|
|
}
|
|
|
|
FakeDate.prototype = Object.create(RealDate.prototype);
|
|
FakeDate.now = () => currentTime;
|
|
FakeDate.parse = RealDate.parse;
|
|
FakeDate.UTC = RealDate.UTC;
|
|
|
|
return {
|
|
FakeDate,
|
|
advance: (ms) => {
|
|
currentTime += ms;
|
|
return currentTime;
|
|
},
|
|
now: () => currentTime,
|
|
};
|
|
}
|
|
|
|
function probeTdc(tdcFilePath, options = {}) {
|
|
let tdcCode = options.tdcCode || fs.readFileSync(tdcFilePath, "utf-8");
|
|
|
|
// ---- 神谕钩子(自包含, 写在沙箱内的 window 上): 记录 PC, 并在进入 XTEA 例程时 dump 密钥寄存器 ----
|
|
const TARGET_PC = parseInt(process.env.XTEA_PC || "9109", 10);
|
|
const HOOK = "window.__PC=window.__PC||[];window.__H=function(H,E){window.__PC.push(H);"
|
|
+ "if(H===" + TARGET_PC + "&&!window.__XT){var c=E[7];window.__XT={at:H,"
|
|
+ "r7:Array.isArray(c)?JSON.stringify(c).slice(0,300):String(c),prev:window.__PC.slice(-260)};}};";
|
|
const NEEDLE = "M=G[z[H++]]()";
|
|
if (tdcCode.includes(NEEDLE)) {
|
|
tdcCode = HOOK + tdcCode.replace(NEEDLE, "M=(window.__H(H,E),G[z[H++]]())");
|
|
}
|
|
const accesses = new Set();
|
|
let mouseHandler = null;
|
|
|
|
// Initialize Virtual Clock (defaults to ~3.2s before real Date.now())
|
|
const vClock = createVirtualDate(options.startTime);
|
|
|
|
// Mock Realistic Environment (based on macOS Chrome success capture)
|
|
const realScreen = {
|
|
width: options.screenWidth || 2560,
|
|
height: options.screenHeight || 1440,
|
|
availWidth: options.screenWidth || 2560,
|
|
availHeight: options.screenAvailHeight || 1345,
|
|
colorDepth: 24,
|
|
pixelDepth: 24,
|
|
};
|
|
|
|
const realNavigator = {
|
|
userAgent:
|
|
options.userAgent ||
|
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36",
|
|
platform: "MacIntel",
|
|
languages: ["zh-CN", "zh", "fr", "de", "en"],
|
|
language: "zh-CN",
|
|
hardwareConcurrency: 10,
|
|
cookieEnabled: true,
|
|
};
|
|
|
|
const fakeCanvas = {
|
|
getContext: (type) => {
|
|
if (type.includes("webgl")) {
|
|
return {
|
|
getExtension: (name) => {
|
|
if (name === "WEBGL_debug_renderer_info") {
|
|
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
|
|
}
|
|
return null;
|
|
},
|
|
getParameter: (param) => {
|
|
if (param === 37445) return "Google Inc. (Apple)";
|
|
if (param === 37446) {
|
|
return "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)";
|
|
}
|
|
return "";
|
|
},
|
|
getSupportedExtensions: () => ["WEBGL_debug_renderer_info"],
|
|
};
|
|
}
|
|
return {
|
|
getImageData: () => ({ data: new Uint8ClampedArray(16) }),
|
|
toDataURL: () => "data:image/png;base64,GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1PQinPL7Bi3TG0qE2CogAAAABJRU5ErkJggg==",
|
|
};
|
|
},
|
|
toDataURL: () => "data:image/png;base64,GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1PQinPL7Bi3TG0qE2CogAAAABJRU5ErkJggg==",
|
|
};
|
|
|
|
const fakeLoc = {
|
|
href: "https://turing.captcha.gtimg.com/1/template/drag_ele.html?rand=1519713624347",
|
|
hostname: "turing.captcha.gtimg.com",
|
|
protocol: "https:",
|
|
};
|
|
|
|
const fakeDoc = {
|
|
referrer: options.referrer || "https://1ms.run/console/personal/benefits/check-in",
|
|
characterSet: "UTF-8",
|
|
documentElement: { clientWidth: 360, clientHeight: 360 },
|
|
createElement: (tag) => {
|
|
if (String(tag).toLowerCase() === "canvas") return fakeCanvas;
|
|
return { appendChild: () => {}, style: {}, setAttribute: () => {} };
|
|
},
|
|
getElementById: () => null,
|
|
cookie: "",
|
|
};
|
|
|
|
Object.defineProperty(fakeDoc, "onmousemove", {
|
|
set(fn) {
|
|
mouseHandler = fn;
|
|
},
|
|
get() {
|
|
return mouseHandler;
|
|
},
|
|
});
|
|
|
|
const baseWindow = {
|
|
screen: realScreen,
|
|
navigator: realNavigator,
|
|
document: fakeDoc,
|
|
location: fakeLoc,
|
|
innerWidth: 360,
|
|
innerHeight: 360,
|
|
devicePixelRatio: 2,
|
|
Date: vClock.FakeDate,
|
|
Math: Math,
|
|
String: String,
|
|
Number: Number,
|
|
Boolean: Boolean,
|
|
Array: Array,
|
|
Object: Object,
|
|
Function: Function,
|
|
RegExp: RegExp,
|
|
JSON: JSON,
|
|
Error: Error,
|
|
TypeError: TypeError,
|
|
Uint8Array: Uint8Array,
|
|
Uint8ClampedArray: Uint8ClampedArray,
|
|
parseInt: parseInt,
|
|
parseFloat: parseFloat,
|
|
encodeURIComponent: encodeURIComponent,
|
|
decodeURIComponent: decodeURIComponent,
|
|
setTimeout: setTimeout,
|
|
clearTimeout: clearTimeout,
|
|
setInterval: setInterval,
|
|
clearInterval: clearInterval,
|
|
addEventListener: () => {},
|
|
removeEventListener: () => {},
|
|
};
|
|
|
|
// Attach window.top to simulate running in an iframe
|
|
baseWindow.top = {};
|
|
baseWindow.window = baseWindow;
|
|
baseWindow.self = baseWindow;
|
|
|
|
const proxiedWindow = createDeepProxy(baseWindow, "window", accesses);
|
|
|
|
// Execute in isolated V8 Context
|
|
const sandbox = vm.createContext(proxiedWindow);
|
|
|
|
if (options.verbose) console.log(`Running TDC inside Semantic Probe...`);
|
|
vm.runInContext(tdcCode, sandbox);
|
|
|
|
// Dispatch simulated human trajectory if clickPoints provided
|
|
const clickPoints = options.clickPoints || options.trajectory || [];
|
|
if (mouseHandler && clickPoints.length > 0) {
|
|
const scale = 320.0 / 672.0;
|
|
|
|
// 1. Initial human observation hesitation (400 ~ 650ms)
|
|
vClock.advance(Math.floor(420 + Math.random() * 200));
|
|
|
|
// Starting mouse position (natural bottom-ish area: x~140, y~300)
|
|
let curX = Math.round(130 + (Math.random() - 0.5) * 30);
|
|
let curY = Math.round(290 + (Math.random() - 0.5) * 20);
|
|
|
|
for (let pIdx = 0; pIdx < clickPoints.length; pIdx++) {
|
|
const pt = clickPoints[pIdx];
|
|
// Target coordinates in iframe frame (left offset ~20, top ~60)
|
|
const targetX = Math.round(20 + pt[0] * scale + (Math.random() - 0.5) * 4);
|
|
const targetY = Math.round(60 + pt[1] * scale + (Math.random() - 0.5) * 4);
|
|
|
|
const startStepX = curX;
|
|
const startStepY = curY;
|
|
// 15 ~ 22 movement steps per target point
|
|
const steps = Math.floor(15 + Math.random() * 6);
|
|
|
|
for (let s = 1; s <= steps; s++) {
|
|
const t = s / steps;
|
|
// Human kinematics: Ease-in-out cubic curve
|
|
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
const jitterX = s < steps ? (Math.random() - 0.5) * 1.6 : 0;
|
|
const jitterY = s < steps ? (Math.random() - 0.5) * 1.6 : 0;
|
|
|
|
curX = Math.round(startStepX + (targetX - startStepX) * ease + jitterX);
|
|
curY = Math.round(startStepY + (targetY - startStepY) * ease + jitterY);
|
|
|
|
// Advance 16 ~ 18ms per frame (60Hz vsync sampling)
|
|
vClock.advance(16 + (Math.random() < 0.25 ? 1 : 0));
|
|
|
|
mouseHandler({
|
|
type: "mousemove",
|
|
clientX: curX,
|
|
clientY: curY,
|
|
pageX: curX,
|
|
pageY: curY,
|
|
offsetX: curX,
|
|
offsetY: curY,
|
|
});
|
|
}
|
|
|
|
// Pause at target (click dwell time: 90 ~ 160ms)
|
|
const dwellMs = Math.floor(90 + Math.random() * 70);
|
|
vClock.advance(dwellMs);
|
|
mouseHandler({
|
|
type: "mousemove",
|
|
clientX: curX,
|
|
clientY: curY,
|
|
pageX: curX,
|
|
pageY: curY,
|
|
offsetX: curX,
|
|
offsetY: curY,
|
|
});
|
|
}
|
|
|
|
// Post-click think & submit delay (200 ~ 350ms)
|
|
vClock.advance(Math.floor(220 + Math.random() * 120));
|
|
}
|
|
|
|
if (options.verbose) console.log(`TDC loaded. Calling TDC.getData(!0)...`);
|
|
let collect = null;
|
|
let eks = null;
|
|
if (sandbox.TDC && sandbox.TDC.getData) {
|
|
collect = sandbox.TDC.getData(true);
|
|
if (sandbox.TDC.getInfo) {
|
|
const info = sandbox.TDC.getInfo();
|
|
eks = info ? info.info : null;
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
success: !!collect,
|
|
collect: collect,
|
|
eks: eks,
|
|
collectLen: collect ? collect.length : 0,
|
|
collectPreview: collect ? collect.slice(0, 80) : null,
|
|
eksPreview: eks ? eks.slice(0, 80) : null,
|
|
accessCount: accesses.size,
|
|
accesses: Array.from(accesses).sort(),
|
|
__pc_len: (sandbox.__PC || []).length,
|
|
__xtea: sandbox.__XT || null,
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const arg2 = process.argv[2];
|
|
if (arg2 === "-") {
|
|
// Pipeline mode for Python / external callers
|
|
let tdcCode = "";
|
|
process.stdin.setEncoding("utf-8");
|
|
process.stdin.on("data", (chunk) => {
|
|
tdcCode += chunk;
|
|
});
|
|
process.stdin.on("end", () => {
|
|
try {
|
|
const opts = process.argv[3] ? JSON.parse(process.argv[3]) : {};
|
|
// Temporary write to memory / execute directly
|
|
const report = probeTdc(null, { ...opts, tdcCode });
|
|
process.stdout.write(
|
|
JSON.stringify({
|
|
success: report.success,
|
|
collect: report.collect,
|
|
tlg: String(report.collectLen),
|
|
eks: report.eks,
|
|
source: "probe",
|
|
}) + "\n"
|
|
);
|
|
} catch (err) {
|
|
process.stdout.write(
|
|
JSON.stringify({ success: false, error: String(err) }) + "\n"
|
|
);
|
|
}
|
|
});
|
|
} else {
|
|
const tdcFile = arg2 || "tcaptcha/core_js/tdc_sample.js";
|
|
const report = probeTdc(tdcFile);
|
|
console.log("\n--- TDC Execution Result ---");
|
|
console.log(`Success: ${report.success}`);
|
|
console.log(`Collect Length: ${report.collectLen}`);
|
|
console.log(`Collect Preview: ${report.collectPreview}`);
|
|
console.log(`EKS Preview: ${report.eksPreview}`);
|
|
console.log(`Total Environment Accesses: ${report.accessCount}`);
|
|
|
|
console.log("\n--- Top 35 Environment Accesses Logged by Probe ---");
|
|
report.accesses.slice(0, 35).forEach((a) => console.log(a));
|
|
|
|
const outReport = tdcFile.replace(/\.js$/, ".probe_report.json");
|
|
fs.writeFileSync(outReport, JSON.stringify(report, null, 2), "utf-8");
|
|
console.log(`\nDetailed probe report written to ${outReport}`);
|
|
}
|
|
}
|
|
|
|
module.exports = { probeTdc };
|