Files
hello_js_reverse_skill/tcaptcha/tdc_browser.js
T
yml2213 fbcf2d448a 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 个住宅代理)、短时频控、静默窗口
2026-10-11 18:07:09 +08:00

244 lines
9.9 KiB
JavaScript

/**
* 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 };