333 lines
11 KiB
JavaScript
333 lines
11 KiB
JavaScript
/**
|
|
* 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) {
|
|
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=2039519451',
|
|
protocol: 'https:',
|
|
host: 'turing.captcha.qcloud.com',
|
|
hostname: 'turing.captcha.qcloud.com',
|
|
pathname: '/cap_union_new_show',
|
|
search: '?aid=2039519451',
|
|
hash: ''
|
|
},
|
|
document: {
|
|
createElement: (tag) => {
|
|
if ((tag || '').toLowerCase() === 'canvas') return new CanvasElement();
|
|
return new Element(tag);
|
|
},
|
|
addEventListener: addL,
|
|
removeEventListener: () => {},
|
|
body: docBody,
|
|
documentElement: docElement,
|
|
referrer: 'https://app.rainyun.com/',
|
|
cookie: '',
|
|
getElementsByTagName: (t) => [t === 'body' ? docBody : docElement],
|
|
getElementById: () => null,
|
|
title: '安全验证',
|
|
head: new Element('HEAD')
|
|
},
|
|
navigator: {
|
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36',
|
|
platform: 'MacIntel',
|
|
languages: ['zh-CN', 'zh', 'en'],
|
|
language: 'zh-CN',
|
|
cookieEnabled: true,
|
|
plugins: [{ name: 'PDF Viewer' }],
|
|
mimeTypes: []
|
|
},
|
|
screen: {
|
|
width: 1920,
|
|
height: 1080,
|
|
availWidth: 1920,
|
|
availHeight: 1055,
|
|
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: 360,
|
|
innerHeight: 360,
|
|
outerWidth: 1920,
|
|
outerHeight: 1080,
|
|
devicePixelRatio: 2,
|
|
addEventListener: addL,
|
|
removeEventListener: () => {}
|
|
};
|
|
baseWindow.window = baseWindow;
|
|
baseWindow.self = baseWindow;
|
|
baseWindow.top = baseWindow;
|
|
baseWindow.parent = baseWindow;
|
|
|
|
// Execute TDC.js
|
|
vm.runInNewContext(rawCode, baseWindow);
|
|
|
|
// Map 672x480 coordinates to 360x360 iframe display space
|
|
const scale = 320.0 / 672.0;
|
|
const targetPoints = clickPoints.map(p => ({
|
|
x: Math.round(20 + p[0] * scale),
|
|
y: Math.round(60 + p[1] * scale)
|
|
}));
|
|
|
|
// Generate path: entrance -> P1 -> P2 -> P3
|
|
let currentPoint = {
|
|
x: Math.round(150 + (Math.random() - 0.5) * 60),
|
|
y: Math.round(320 + (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 : '';
|
|
|
|
return {
|
|
collect,
|
|
tlg: collect ? collect.length : 0,
|
|
eks
|
|
};
|
|
}
|
|
|
|
function runTdc(tdcFilePath, clickPoints) {
|
|
const rawCode = fs.readFileSync(tdcFilePath, 'utf8');
|
|
return runTdcCode(rawCode, clickPoints);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const tdcPath = process.argv[2];
|
|
const pointsArg = process.argv[3];
|
|
const points = pointsArg ? JSON.parse(pointsArg) : [[626, 253], [120, 120], [438, 33]];
|
|
|
|
if (tdcPath === '-') {
|
|
let input = '';
|
|
process.stdin.setEncoding('utf8');
|
|
process.stdin.on('data', chunk => { input += chunk; });
|
|
process.stdin.on('end', () => {
|
|
const result = runTdcCode(input, points);
|
|
console.log(JSON.stringify(result));
|
|
});
|
|
} else {
|
|
const result = runTdc(tdcPath, points);
|
|
console.log(JSON.stringify(result));
|
|
}
|
|
}
|
|
|
|
module.exports = { runTdc, runTdcCode };
|
|
|