做了什么
- 从「签到成功-1.har」提取该次会话的 tdc.js, 提取密钥解密出
errorCode=0 那次 collect 的完整明文 (1360 字节), 存为
tcaptcha/models/success_collect_template.b64
- 新增 generate_collect_success(): 以成功样本为结构模板, Python 只回填
轨迹/当前时间戳/随机盐, 其余环境字段 (canvas/WebGL/屏幕/语言/sd.ft) 原样保留
- 新增 _success_style_trajectory(): 按成功样本实测统计生成轨迹
(61 点, dt 均值 30.6ms, 50% 在 0~16ms, 途经 3 个目标, 驻留抖动)
—— 修正了统计正则不允许负增量的 bug (真实 61 点, 此前误数成 18)
- 发现成功样本用 \x00 填充 (不是空格), sd 多一个 ft 字段
- telemetry_mode=python_keyed 默认走该路径; Node 仅提取每构建 XTEA 密钥
- ANALYSIS.md 第七节重写为 v2 证据账本, 显式撤回 4 个无证据结论
验证
- 纯 Python 密文 == 真实 tdc.js 密文 (逐字节)
- 回归: test_xtea_key 11 项、test_glyph_solver 8/8
- 端到端 1ms 实测: 仍 errorCode=12 (诚实记录, 未解决)
结论(见 ANALYSIS.md 7.5-7.7)
- 12 不是"检测到机器人": 同 IP 有成功样本、参数逐字段一致、
真 Chrome 网络栈/真人点击/CDP 均排除
- 剩余假设: H1 需已登录业务会话(未用 auth_token 调 captcha/params 复测)
H3 需有效 qcloud.com cookie(过期 skey 复测不能定论)
- 下一步: 用登录态 auth_token 调 /api/v1/captcha/params,
对比返回是否多出鉴权字段 —— 这是唯一未做且成本低的关键实验
110 lines
4.6 KiB
JavaScript
110 lines
4.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 用真实 Chrome 的网络栈发一个请求 (TLS/HTTP2/header 顺序全是浏览器的)
|
|
*
|
|
* 用途: 验证服务端风控是否在按「客户端网络指纹」判定。
|
|
* 从真实验证码域 https://turing.captcha.gtimg.com/ 发起 fetch,
|
|
* Origin/Referer/Sec-Fetch-* 都由浏览器自己填。
|
|
*
|
|
* 用法:
|
|
* echo '<json body>' | node chrome_fetch.js <url> [originUrl]
|
|
*/
|
|
const { spawn } = require('child_process');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const CHROME = process.env.REA_BROWSER_EXECUTABLE ||
|
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
|
|
|
|
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
|
|
|
async function launchChrome() {
|
|
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'chrome-fetch-'));
|
|
const proc = spawn(CHROME, [
|
|
'--headless=new', '--remote-debugging-port=0', '--no-first-run',
|
|
'--no-default-browser-check', '--disable-gpu', '--disable-dev-shm-usage',
|
|
'--disable-web-security', '--disable-features=IsolateOrigins,site-per-process',
|
|
`--user-data-dir=${profile}`, '--window-size=1280,800', 'about:blank',
|
|
], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
const portFile = path.join(profile, 'DevToolsActivePort');
|
|
let port = null;
|
|
for (let i = 0; i < 120; i++) {
|
|
if (fs.existsSync(portFile)) {
|
|
port = parseInt(fs.readFileSync(portFile, 'utf8').split('\n')[0], 10);
|
|
if (port) break;
|
|
}
|
|
await sleep(100);
|
|
}
|
|
if (!port) { proc.kill('SIGKILL'); throw new Error('Chrome 未就绪'); }
|
|
return { proc, port, profile };
|
|
}
|
|
|
|
class Cdp {
|
|
constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); }
|
|
static async connect(url) {
|
|
const ws = new WebSocket(url);
|
|
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = rej; });
|
|
const c = new Cdp(ws);
|
|
ws.onmessage = ev => {
|
|
const m = JSON.parse(ev.data);
|
|
if (m.id && c.waits.has(m.id)) {
|
|
const { res, rej } = c.waits.get(m.id);
|
|
c.waits.delete(m.id);
|
|
m.error ? rej(new Error(JSON.stringify(m.error))) : res(m.result);
|
|
}
|
|
};
|
|
return c;
|
|
}
|
|
send(method, params = {}, sessionId) {
|
|
const id = ++this.id;
|
|
return new Promise((res, rej) => {
|
|
this.waits.set(id, { res, rej });
|
|
this.ws.send(JSON.stringify({ id, method, params, sessionId }));
|
|
});
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const url = process.argv[2];
|
|
const origin = process.argv[3] || 'https://turing.captcha.gtimg.com/';
|
|
const body = fs.readFileSync(0, 'utf8').trim();
|
|
const { proc, port, profile } = await launchChrome();
|
|
try {
|
|
const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
|
|
const cdp = await Cdp.connect(ver.webSocketDebuggerUrl);
|
|
const { targetId } = await cdp.send('Target.createTarget', { url: origin });
|
|
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true });
|
|
await cdp.send('Page.enable', {}, sessionId);
|
|
await sleep(2500); // 等页面(真实验证码域)加载完
|
|
|
|
const loc = await cdp.send('Runtime.evaluate', { expression: 'location.href', returnByValue: true }, sessionId);
|
|
process.stderr.write('[page] ' + loc.result.value + '\n');
|
|
const expr = `(async () => {
|
|
const r = await fetch(${JSON.stringify(url)}, ${JSON.stringify(body)}
|
|
? { method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
|
|
body: ${JSON.stringify(body)}, credentials: 'include' }
|
|
: { method: 'GET', credentials: 'include' });
|
|
const t = await r.text();
|
|
return JSON.stringify({ status: r.status, text: t });
|
|
})()`;
|
|
const out = await cdp.send('Runtime.evaluate',
|
|
{ expression: expr, awaitPromise: true, returnByValue: true }, sessionId);
|
|
if (out.exceptionDetails) {
|
|
console.log(JSON.stringify({ _exception: out.exceptionDetails.text,
|
|
desc: (out.exceptionDetails.exception || {}).description }));
|
|
} else if (out.result && out.result.value !== undefined) {
|
|
console.log(out.result.value);
|
|
} else {
|
|
console.log(JSON.stringify({ _raw: out }));
|
|
}
|
|
cdp.ws.close();
|
|
} finally {
|
|
proc.kill('SIGKILL');
|
|
try { fs.rmSync(profile, { recursive: true, force: true }); } catch (_) {}
|
|
}
|
|
}
|
|
|
|
main().catch(e => { console.error('ERR', e.message); process.exit(1); });
|