feat(反VM): 纯 Python 还原 XTEA 算法 + 从 tdc.js 运行时提取每构建密钥
做了什么 - 确认 collect 的加密算法就是教科书 XTEA (32 轮, delta=0x9E3779B9), 并证明 crypto.py 原有的"带加盐"写法只是把两个密钥字折叠: K' = [k0, k1+salt1, k2+salt2, k3] - tdc_worker.js 新增 captureKey 模式: 正则改写 VM 的 ADD/XOR opcode 为带日志版本, 用反向引用自动识别栈变量名 (样本=g/雨云实时=j/1ms=M, 版本无关); 匹配数不为 1 时显式报错, 不静默失败 - 密钥反推: (sum + K[j]) 是一次 ADD, 两操作数必有一个等于 r*DELTA, 另一个就是密钥字; 再对 Top-N 候选做 4! 排列 + 明文内容特征验证 - 新增 recover_payload_template / generate_collect_from_template: 运行时标定一次明文模板, 之后轨迹生成/组包/加密/编码全部在 Python - 新增 tcaptcha/proxy.py 代理提取, client 支持 --proxy-api 遇 12 自动换 IP 为什么 - 服务端只认 collect 的解密结果, 而密钥是每构建随机的; 把算法做成纯 Python 后, 只有"取密钥"依赖 Node, 抗发版能力最强 怎么验证的 - 纯 Python XTEA 加密结果与真实 tdc.js 产物前 48 字节逐字节一致 - 密钥按构建固定的证据: 同一脚本 3 次运行密文首块均为 f6a24e3e4fb5dd2b - 三份脚本 (样本/雨云实时/1ms实时) 均成功复原密钥 - 雨云 aid 端到端: 纯 Python 出包 -> errorCode=0 + 真实 ticket - 回归测试: tcaptcha/tests/test_xtea_key.py 11 项断言全过, 文字点选求解 8/8; 新增 tcaptcha/selftest.py 一键复验 已知未解 - 1ms aid=195672344 在答案正确时仍返回 errorCode=12; 已排除密钥、 遥测引擎、出口 IP (4 个住宅代理)、短时频控、静默窗口
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* tdc_browser.js —— 在「真实 Chrome」中执行服务端下发的 tdc.js, 产出 collect / eks。
|
||||
*
|
||||
* 为什么需要它
|
||||
* ------------
|
||||
* `tdc_worker.js` 用 Node VM 伪造 DOM/BOM 环境, tdc.js 采集到的环境指纹不真实,
|
||||
* 服务端在答题正确后进入风控评估会返回 errorCode=12 (环境异常/自动化)。
|
||||
* 本脚本用 CDP 驱动本机真实 Chrome: 在验证码真实来源域下加载 tdc.js, 用
|
||||
* Input.dispatchMouseEvent 派发可信鼠标事件, 再读回 TDC.getData()/getInfo()。
|
||||
*
|
||||
* 零依赖: 只用 Node >= 22 内置的全局 WebSocket 与 child_process。
|
||||
*
|
||||
* 用法:
|
||||
* node tdc_browser.js <tdc.js路径|-> <points JSON> <options JSON>
|
||||
* 选项: { aid, referer, userAgent, chromePath, headless, origin, timeoutMs }
|
||||
* 输出: {"collect":..,"tlg":..,"eks":..,"source":"browser"} 或 {"error":..}
|
||||
*/
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_CHROME = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
||||
|
||||
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
|
||||
async function waitForFile(file, timeoutMs) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < timeoutMs) {
|
||||
if (fs.existsSync(file)) {
|
||||
const txt = fs.readFileSync(file, 'utf8').trim().split('\n');
|
||||
if (txt[0]) return txt[0].trim();
|
||||
}
|
||||
await sleep(120);
|
||||
}
|
||||
throw new Error(`等待 DevToolsActivePort 超时: ${file}`);
|
||||
}
|
||||
|
||||
class Cdp {
|
||||
constructor(ws) {
|
||||
this.ws = ws;
|
||||
this.id = 0;
|
||||
this.pending = new Map();
|
||||
ws.addEventListener('message', ev => {
|
||||
let msg;
|
||||
try { msg = JSON.parse(ev.data); } catch { return; }
|
||||
if (msg.id && this.pending.has(msg.id)) {
|
||||
const { resolve, reject } = this.pending.get(msg.id);
|
||||
this.pending.delete(msg.id);
|
||||
if (msg.error) reject(new Error(JSON.stringify(msg.error)));
|
||||
else resolve(msg.result);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
send(method, params = {}, sessionId) {
|
||||
const id = ++this.id;
|
||||
const payload = { id, method, params };
|
||||
if (sessionId) payload.sessionId = sessionId;
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pending.set(id, { resolve, reject });
|
||||
this.ws.send(JSON.stringify(payload));
|
||||
setTimeout(() => {
|
||||
if (this.pending.has(id)) {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`CDP 超时: ${method}`));
|
||||
}
|
||||
}, 45000);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function connect(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const ws = new WebSocket(url);
|
||||
ws.addEventListener('open', () => resolve(ws));
|
||||
ws.addEventListener('error', e => reject(new Error('WebSocket 连接失败: ' + (e.message || e.type))));
|
||||
});
|
||||
}
|
||||
|
||||
async function httpJson(url) {
|
||||
const res = await fetch(url);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成鼠标轨迹 (相对位移), 与 tdc_worker.js 保持同样的形态:
|
||||
* 起点在 iframe 底部中间, 依次移动到 3 个目标点, 每段用贝塞尔插值 + 抖动 + 停留。
|
||||
*/
|
||||
function buildTrajectory(points, opts) {
|
||||
const scale = opts.displayScale != null ? opts.displayScale : (320.0 / 672.0);
|
||||
const targets = points.map(p => ([
|
||||
Math.round((opts.offsetX || 0) + p[0] * scale),
|
||||
Math.round((opts.offsetY || 0) + p[1] * scale),
|
||||
]));
|
||||
const frameW = opts.frameWidth || 360;
|
||||
const frameH = opts.frameHeight || 360;
|
||||
let cur = [Math.round(frameW * 0.42 + (Math.random() - 0.5) * 60),
|
||||
Math.round(frameH * 0.89 + (Math.random() - 0.5) * 20)];
|
||||
const events = [cur.slice()];
|
||||
for (const t of targets) {
|
||||
const steps = 18 + Math.floor(Math.random() * 12);
|
||||
const dx = t[0] - cur[0], dy = t[1] - cur[1];
|
||||
const dist = Math.hypot(dx, dy) || 1;
|
||||
const cx = (Math.random() - 0.5) * Math.min(50, dist * 0.4);
|
||||
const cp1 = [cur[0] + dx * 0.3 - dy / dist * cx, cur[1] + dy * 0.3 + dx / dist * cx];
|
||||
const cp2 = [cur[0] + dx * 0.7 + dy / dist * cx * 0.5, cur[1] + dy * 0.7 - dx / dist * cx * 0.5];
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const u = i / steps, e = u * u * (3 - 2 * u), v = 1 - e;
|
||||
const x = Math.round(v ** 3 * cur[0] + 3 * v ** 2 * e * cp1[0] + 3 * v * e ** 2 * cp2[0] + e ** 3 * t[0]);
|
||||
const y = Math.round(v ** 3 * cur[1] + 3 * v ** 2 * e * cp1[1] + 3 * v * e ** 2 * cp2[1] + e ** 3 * t[1]);
|
||||
cur = [x, y];
|
||||
events.push(cur.slice());
|
||||
}
|
||||
for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) {
|
||||
events.push([cur[0] + Math.round((Math.random() - 0.5) * 2),
|
||||
cur[1] + Math.round((Math.random() - 0.5) * 2)]);
|
||||
}
|
||||
}
|
||||
return { events, targets };
|
||||
}
|
||||
|
||||
async function generate(rawCode, points, opts) {
|
||||
const opt = Object.assign({
|
||||
aid: '2039519451',
|
||||
referer: 'https://1ms.run/',
|
||||
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',
|
||||
chromePath: process.env.TDC_CHROME || DEFAULT_CHROME,
|
||||
headless: true,
|
||||
origin: 'https://turing.captcha.gtimg.com/',
|
||||
frameWidth: 360,
|
||||
frameHeight: 360,
|
||||
timeoutMs: 45000,
|
||||
}, opts || {});
|
||||
|
||||
if (!fs.existsSync(opt.chromePath)) throw new Error(`找不到 Chrome: ${opt.chromePath}`);
|
||||
|
||||
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'tdc-chrome-'));
|
||||
const args = [
|
||||
'--remote-debugging-port=0',
|
||||
`--user-data-dir=${profile}`,
|
||||
'--no-first-run',
|
||||
'--no-default-browser-check',
|
||||
'--disable-background-networking',
|
||||
'--disable-blink-features=AutomationControlled',
|
||||
'--disable-features=Translate,OptimizationHints',
|
||||
`--user-agent=${opt.userAgent}`,
|
||||
'--window-size=420,760',
|
||||
'about:blank',
|
||||
];
|
||||
if (opt.headless) args.unshift('--headless=new');
|
||||
|
||||
const chrome = spawn(opt.chromePath, args, { stdio: ['ignore', 'ignore', 'pipe'] });
|
||||
let stderr = '';
|
||||
chrome.stderr.on('data', d => { stderr += d.toString(); });
|
||||
|
||||
let ws = null;
|
||||
try {
|
||||
const port = await waitForFile(path.join(profile, 'DevToolsActivePort'), opt.timeoutMs);
|
||||
const version = await httpJson(`http://127.0.0.1:${port}/json/version`);
|
||||
ws = await connect(version.webSocketDebuggerUrl);
|
||||
const cdp = new Cdp(ws);
|
||||
|
||||
const { targetId } = await cdp.send('Target.createTarget', { url: 'about:blank' });
|
||||
const list = await httpJson(`http://127.0.0.1:${port}/json/list`);
|
||||
const page = list.find(t => t.id === targetId);
|
||||
if (!page) throw new Error('未找到新建的页面 target');
|
||||
|
||||
const pageWs = await connect(page.webSocketDebuggerUrl);
|
||||
const pcdp = new Cdp(pageWs);
|
||||
|
||||
await pcdp.send('Page.enable');
|
||||
await pcdp.send('Runtime.enable');
|
||||
await pcdp.send('Page.navigate', { url: opt.origin });
|
||||
await sleep(1200);
|
||||
|
||||
// 注入并执行真实 tdc.js
|
||||
const inject = await pcdp.send('Runtime.evaluate', {
|
||||
expression: `(function(){ ${rawCode}\n; return typeof TDC !== 'undefined' && !!TDC.getData; })()`,
|
||||
awaitPromise: false,
|
||||
returnByValue: true,
|
||||
});
|
||||
if (!inject.result || inject.result.value !== true) {
|
||||
throw new Error(`tdc.js 执行后未暴露 TDC.getData (result=${JSON.stringify(inject.result)})`);
|
||||
}
|
||||
|
||||
// 用 CDP Input 派发可信鼠标事件
|
||||
const { events } = buildTrajectory(points, opt);
|
||||
for (const [x, y] of events) {
|
||||
await pcdp.send('Input.dispatchMouseEvent', {
|
||||
type: 'mouseMoved', x, y, button: 'none', buttons: 0, clickCount: 0,
|
||||
});
|
||||
}
|
||||
// 三次真实点击
|
||||
for (const t of points.map(p => [
|
||||
Math.round((opt.offsetX || 0) + p[0] * (opt.displayScale != null ? opt.displayScale : 320 / 672)),
|
||||
Math.round((opt.offsetY || 0) + p[1] * (opt.displayScale != null ? opt.displayScale : 320 / 672)),
|
||||
])) {
|
||||
await pcdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x: t[0], y: t[1], button: 'left', buttons: 1, clickCount: 1 });
|
||||
await pcdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x: t[0], y: t[1], button: 'left', buttons: 0, clickCount: 1 });
|
||||
await sleep(120);
|
||||
}
|
||||
|
||||
const out = await pcdp.send('Runtime.evaluate', {
|
||||
expression: `JSON.stringify({collect: TDC.getData(), eks: (TDC.getInfo()||{}).info || ''})`,
|
||||
returnByValue: true,
|
||||
});
|
||||
const payload = JSON.parse(out.result.value);
|
||||
pageWs.close();
|
||||
if (!payload.collect) throw new Error('TDC.getData() 返回空');
|
||||
return { collect: payload.collect, tlg: payload.collect.length, eks: payload.eks, source: 'browser' };
|
||||
} finally {
|
||||
try { if (ws) ws.close(); } catch { /* ignore */ }
|
||||
try { chrome.kill('SIGKILL'); } catch { /* ignore */ }
|
||||
await sleep(150);
|
||||
try { fs.rmSync(profile, { recursive: true, force: true }); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
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) : [[162, 249], [104, 37], [561, 156]];
|
||||
const options = optionsArg ? JSON.parse(optionsArg) : {};
|
||||
|
||||
const run = code => generate(code, points, options).then(
|
||||
r => { console.log(JSON.stringify(r)); },
|
||||
e => { console.log(JSON.stringify({ error: String((e && e.stack) || e) })); process.exitCode = 1; }
|
||||
);
|
||||
|
||||
if (tdcPath === '-') {
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', c => { input += c; });
|
||||
process.stdin.on('end', () => run(input));
|
||||
} else {
|
||||
run(fs.readFileSync(tdcPath, 'utf8'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { generate };
|
||||
Reference in New Issue
Block a user