/** * Tencent TDC.js Sandboxed Worker & Mouse Trajectory Generator * Runs TDC.js in an isolated mock DOM environment to generate authentic * encrypted telemetry (`collect`) and session encryption key (`eks`). */ const fs = require('fs'); const vm = require('vm'); class Element { constructor(tagName) { this.tagName = (tagName || 'div').toUpperCase(); this.children = []; this.style = {}; this.attributes = {}; } appendChild(child) { this.children.push(child); return child; } removeChild(child) { const idx = this.children.indexOf(child); if (idx !== -1) this.children.splice(idx, 1); return child; } insertBefore(newChild, refChild) { const idx = this.children.indexOf(refChild); if (idx !== -1) this.children.splice(idx, 0, newChild); else this.children.push(newChild); return newChild; } cloneNode(deep) { const el = new Element(this.tagName); el.style = { ...this.style }; return el; } setAttribute(k, v) { this.attributes[k] = String(v); } getAttribute(k) { return this.attributes[k] || null; } hasAttribute(k) { return k in this.attributes; } removeAttribute(k) { delete this.attributes[k]; } getBoundingClientRect() { return { top: 0, left: 0, width: 360, height: 360, bottom: 360, right: 360 }; } } class CanvasElement extends Element { constructor() { super('CANVAS'); this.width = 300; this.height = 150; } getContext(type) { if (type.includes('webgl')) { return { getSupportedExtensions: () => [ 'ANGLE_instanced_arrays', 'EXT_blend_minmax', 'EXT_texture_filter_anisotropic', 'OES_element_index_uint', 'OES_standard_derivatives', 'OES_texture_float', 'OES_texture_half_float', 'OES_vertex_array_object', 'WEBGL_compressed_texture_s3tc', 'WEBGL_debug_renderer_info', 'WEBGL_depth_texture', 'WEBGL_lose_context' ], getExtension: (ext) => { if (ext === 'WEBGL_debug_renderer_info') { return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 }; } return null; }, getParameter: (p) => { if (p === 37445) return 'Apple'; if (p === 37446) return 'Apple M1'; if (p === 7936) return 'WebKit'; if (p === 7937) return 'WebKit WebGL'; return 0; }, createBuffer: () => ({}), bindBuffer: () => {}, bufferData: () => {}, createProgram: () => ({}), createShader: () => ({}), shaderSource: () => {}, compileShader: () => {}, attachShader: () => {}, linkProgram: () => {}, useProgram: () => {}, drawArrays: () => {}, viewport: () => {}, clearColor: () => {}, clear: () => {} }; } return { fillRect: () => {}, fillText: () => {}, getImageData: () => ({ data: new Uint8ClampedArray(4) }), measureText: (txt) => ({ width: txt.length * 10 }), beginPath: () => {}, arc: () => {}, fill: () => {}, stroke: () => {} }; } toDataURL() { return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; } } /** * Generate human-like mouse movement trajectory using cubic Bézier curves and Gaussian tremor. */ function generateBezierTrajectory(p0, p1, steps = 25) { // Control points with natural curve deviation const dx = p1.x - p0.x; const dy = p1.y - p0.y; const dist = Math.hypot(dx, dy); // Lateral deviation perpendicular to direct vector const perpX = -dy / (dist || 1); const perpY = dx / (dist || 1); const curvature = (Math.random() - 0.5) * Math.min(60, dist * 0.4); const cp1 = { x: p0.x + dx * 0.3 + perpX * curvature + (Math.random() - 0.5) * 10, y: p0.y + dy * 0.3 + perpY * curvature + (Math.random() - 0.5) * 10, }; const cp2 = { x: p0.x + dx * 0.7 + perpX * curvature * 0.6 + (Math.random() - 0.5) * 10, y: p0.y + dy * 0.7 + perpY * curvature * 0.6 + (Math.random() - 0.5) * 10, }; const points = []; for (let i = 0; i <= steps; i++) { const t = i / steps; // Ease in-out parameterization const easeT = t * t * (3 - 2 * t); const invT = 1 - easeT; const bx = invT * invT * invT * p0.x + 3 * invT * invT * easeT * cp1.x + 3 * invT * easeT * easeT * cp2.x + easeT * easeT * easeT * p1.x; const by = invT * invT * invT * p0.y + 3 * invT * invT * easeT * cp1.y + 3 * invT * easeT * easeT * cp2.y + easeT * easeT * easeT * p1.y; // Subtle tremor (except start and end) const tremor = (i === 0 || i === steps) ? 0 : (Math.random() - 0.5) * 1.5; points.push({ x: Math.round(bx + tremor), y: Math.round(by + tremor) }); } return points; } function runTdcCode(rawCode, clickPoints, options) { const opt = Object.assign({ aid: '2039519451', referrer: 'https://app.rainyun.com/', 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', screenWidth: 1920, screenHeight: 1080, availHeight: 1055, devicePixelRatio: 2, // 验证码 iframe 在页面中的显示尺寸 (672x480 原图会被缩放到该尺寸) frameWidth: 360, frameHeight: 360, // 原图 -> 显示空间的映射 (默认按 320/672 缩放, 与雨云场景一致) displayScale: null, offsetX: 20, offsetY: 60 }, options || {}); const listeners = []; function addL(type, fn) { listeners.push({ type, fn }); } const docBody = new Element('BODY'); const docElement = new Element('HTML'); const baseWindow = { location: { href: `https://turing.captcha.qcloud.com/cap_union_new_show?aid=${opt.aid}`, protocol: 'https:', host: 'turing.captcha.qcloud.com', hostname: 'turing.captcha.qcloud.com', pathname: '/cap_union_new_show', search: `?aid=${opt.aid}`, hash: '' }, document: { createElement: (tag) => { if ((tag || '').toLowerCase() === 'canvas') return new CanvasElement(); return new Element(tag); }, addEventListener: addL, removeEventListener: () => {}, body: docBody, documentElement: docElement, referrer: opt.referrer, cookie: '', getElementsByTagName: (t) => [t === 'body' ? docBody : docElement], getElementById: () => null, title: '安全验证', head: new Element('HEAD') }, navigator: { userAgent: opt.userAgent, platform: opt.platform, languages: ['zh-CN', 'zh', 'en'], language: 'zh-CN', cookieEnabled: true, plugins: [{ name: 'PDF Viewer' }], mimeTypes: [] }, screen: { width: opt.screenWidth, height: opt.screenHeight, availWidth: opt.screenWidth, availHeight: opt.availHeight, colorDepth: 24, pixelDepth: 24 }, performance: { now: () => Date.now(), timing: { navigationStart: Date.now() - 2500, loadEventEnd: Date.now() - 800 } }, history: { length: 2 }, indexedDB: {}, sessionStorage: { getItem: () => null, setItem: () => {} }, localStorage: { getItem: () => null, setItem: () => {} }, Date: Date, Math: Math, setTimeout: setTimeout, clearTimeout: clearTimeout, setInterval: setInterval, clearInterval: clearInterval, console: { log: () => {}, error: () => {}, warn: () => {} }, innerWidth: opt.frameWidth, innerHeight: opt.frameHeight, outerWidth: opt.screenWidth, outerHeight: opt.screenHeight, devicePixelRatio: opt.devicePixelRatio, addEventListener: addL, removeEventListener: () => {} }; baseWindow.window = baseWindow; baseWindow.self = baseWindow; baseWindow.top = baseWindow; baseWindow.parent = baseWindow; // 注意: vm.runInNewContext 每次调用都会新建一个 context, 桩打不进去。 // 必须显式 createContext 一次, 之后 prelude 与 tdc.js 都跑在同一个 context 里。 const vmContext = vm.createContext(baseWindow); let instrumentedCode = rawCode; if (opt.captureKey) { // 密钥提取: 把 VM 的 XOR / ADD 两个 opcode 换成带日志的版本。 // 自定义 XTEA 每轮都要算 f(v) ^ (sum + K[j]), 其中 (sum + K[j]) 是 XOR 的操作数, // 记下它就能反推 K[j] = (operand - round*DELTA) mod 2^32。 // 用 Array.prototype.push 打桩是抓不到的 —— 这些 opcode 用的是 // `g[g.length-2]=...` 直接下标赋值。 vm.runInContext( `globalThis.__TDC_XOR=[];globalThis.__TDC_ADD=[];` + `globalThis.__TDC_LOGX=function(a,b){var s=globalThis.__TDC_XOR;` + `if(s.length<4000000){s.push(a>>>0,b>>>0);}return (a^b);};` + `globalThis.__TDC_LOGA=function(a,b){var s=globalThis.__TDC_ADD;` + `if(s.length<4000000){s.push(a>>>0,b>>>0);}return (a+b);};`, vmContext); // 注意: opcode 表里的 **栈变量名每份构建都不同** // (实测: 样本=g, 雨云实时=j, 1ms实时=M —— 见 AGENTS.md 附录 A#6 码表轮换), // 所以必须用反向引用自动识别变量名, 不能写死 'g'。 // 目标 opcode 形态: [.length-2]=[.length-2] .pop() const mkRe = op => new RegExp( '(\\w+)\\[(\\1\\.length-2)\\]=\\1\\[\\2\\]\\' + op + '\\1\\.pop\\(\\)', 'g'); const xorRe = mkRe('^'); const addRe = mkRe('+'); const nXor = (instrumentedCode.match(xorRe) || []).length; const nAdd = (instrumentedCode.match(addRe) || []).length; if (nXor !== 1 || nAdd !== 1) { // 不静默失败 (AGENTS.md 附录 A#8): 结构性变化必须显式暴露 return { error: `opcode 插桩失败: 期望 XOR/ADD 各 1 处, 实际 XOR=${nXor} ADD=${nAdd}. ` + `tdc.js 的 VM opcode 形态可能已变更, 需要更新插桩规则。` }; } instrumentedCode = instrumentedCode .replace(addRe, (m, v, idx) => `${v}[${idx}]=__TDC_LOGA(${v}[${idx}],${v}.pop())`) .replace(xorRe, (m, v, idx) => `${v}[${idx}]=__TDC_LOGX(${v}[${idx}],${v}.pop())`); if (opt.captureKeyVerbose) { process.stderr.write(`[captureKey] 插桩 XOR opcode x${nXor}, ADD opcode x${nAdd}\n`); } } // Execute TDC.js vm.runInContext(instrumentedCode, vmContext); // Map 672x480 coordinates to 360x360 iframe display space // 原图坐标 (672x480) -> 验证码 iframe 显示空间 const scale = opt.displayScale !== null && opt.displayScale !== undefined ? opt.displayScale : (320.0 / 672.0); const targetPoints = clickPoints.map(p => ({ x: Math.round(opt.offsetX + p[0] * scale), y: Math.round(opt.offsetY + p[1] * scale) })); // Generate path: entrance -> P1 -> P2 -> P3 let currentPoint = { x: Math.round(opt.frameWidth * 0.42 + (Math.random() - 0.5) * 60), y: Math.round(opt.frameHeight * 0.89 + (Math.random() - 0.5) * 20) }; const mmListener = listeners.find(l => l.type === 'mousemove'); let currentTime = Date.now() - 1500; if (mmListener) { for (const target of targetPoints) { // Leg trajectory const steps = Math.floor(18 + Math.random() * 12); const leg = generateBezierTrajectory(currentPoint, target, steps); for (const pt of leg) { currentTime += Math.floor(12 + Math.random() * 16); mmListener.fn({ clientX: pt.x, clientY: pt.y, pageX: pt.x, pageY: pt.y, screenX: pt.x + 300, screenY: pt.y + 200, timeStamp: currentTime, type: 'mousemove', isTrusted: true }); } currentPoint = target; // Dwell pause on icon (150ms ~ 350ms) const dwellSteps = Math.floor(4 + Math.random() * 5); for (let i = 0; i < dwellSteps; i++) { currentTime += Math.floor(25 + Math.random() * 30); mmListener.fn({ clientX: currentPoint.x + Math.round((Math.random() - 0.5) * 2), clientY: currentPoint.y + Math.round((Math.random() - 0.5) * 2), pageX: currentPoint.x, pageY: currentPoint.y, screenX: currentPoint.x + 300, screenY: currentPoint.y + 200, timeStamp: currentTime, type: 'mousemove', isTrusted: true }); } } } const collect = baseWindow.TDC.getData(); const info = baseWindow.TDC.getInfo(); const eks = info ? info.info : ''; const result = { collect, tlg: collect ? collect.length : 0, eks }; // 从记录的 ADD 操作数里反推 XTEA 密钥 (含索引) // // 字节码里 (sum + K[j]) 是一次 ADD, 两个操作数分别是 // sum = (r*DELTA) mod 2^32 与 K[j] 本身。 // 反过来查表: 找到 r 后, j 只可能是 r&3 (v0 分支) 或 ((r+1)*DELTA>>11)&3 (v1 分支)。 // 对同一个 K 的多次观测取交集, 就能唯一确定它的索引 —— 不需要 4! 排列搜索。 if (opt.captureKey) { const DELTA = 2654435769; const sumToR = new Map(); for (let r = 0; r <= 32; r++) sumToR.set(Math.imul(r, DELTA) >>> 0, r); const idxOf = r => [r & 3, (((r + 1) * DELTA) >>> 0 >>> 11) & 3]; const addLog = baseWindow.__TDC_ADD || []; const obs = new Map(); // K -> Map(r -> count) for (let i = 0; i + 1 < addLog.length; i += 2) { const a = addLog[i], b = addLog[i + 1]; let r = sumToR.get(a), k = b; if (r === undefined) { r = sumToR.get(b); k = a; } if (r === undefined || k === 0 || (k >>> 0) === DELTA) continue; k = k >>> 0; if (!obs.has(k)) obs.set(k, new Map()); const m = obs.get(k); m.set(r, (m.get(r) || 0) + 1); } const entries = []; for (const [k, rmap] of obs) { let total = 0, candidates = null; for (const [r, c] of rmap) { total += c; const s = new Set(idxOf(r)); candidates = candidates === null ? s : new Set([...candidates].filter(x => s.has(x))); } if (candidates && candidates.size === 1) { entries.push([k, total, [...candidates][0], rmap.size]); } else { entries.push([k, total, candidates && candidates.size ? [...candidates] : null, rmap.size]); } } entries.sort((x, y) => y[1] - x[1]); result.key_words = entries.slice(0, 24); result.add_operand_count = addLog.length; } return result; } function runTdc(tdcFilePath, clickPoints, options) { const rawCode = fs.readFileSync(tdcFilePath, 'utf8'); return runTdcCode(rawCode, clickPoints, options); } if (require.main === module) { const tdcPath = process.argv[2]; const pointsArg = process.argv[3]; const optionsArg = process.argv[4]; const points = pointsArg ? JSON.parse(pointsArg) : [[626, 253], [120, 120], [438, 33]]; const options = optionsArg ? JSON.parse(optionsArg) : {}; if (tdcPath === '-') { let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => { input += chunk; }); process.stdin.on('end', () => { try { const result = runTdcCode(input, points, options); console.log(JSON.stringify(result)); } catch (err) { console.log(JSON.stringify({ error: String(err && err.stack || err) })); process.exitCode = 1; } }); } else { try { const result = runTdc(tdcPath, points, options); console.log(JSON.stringify(result)); } catch (err) { console.log(JSON.stringify({ error: String(err && err.stack || err) })); process.exitCode = 1; } } } module.exports = { runTdc, runTdcCode };