Files
hello_js_reverse_skill/tcaptcha/probe_captcha.js
T
yml2213 1ef819b48d feat(纯算): 以成功样本明文为模板的纯 Python 出包管线 + 证据账本 v2
做了什么
- 从「签到成功-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,
  对比返回是否多出鉴权字段 —— 这是唯一未做且成本低的关键实验
2026-10-11 19:12:23 +08:00

135 lines
5.8 KiB
JavaScript

#!/usr/bin/env node
/**
* 在真实浏览器里用目标 aid 实例化验证码, 抓出它自己发出的真实请求。
*
* 与 tdc_worker / 纯协议实现的区别: 这里 Origin 是真业务域, SDK 是腾讯当前版本,
* 请求参数完全由官方前端构造 —— 用来和我们的协议实现做逐字段对照。
*
* 用法: node probe_captcha.js <originUrl> <aid> [waitMs]
*/
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';
const ORIGIN = process.argv[2] || 'https://1ms.run/';
const AID = process.argv[3] || '195672344';
const WAIT = parseInt(process.argv[4] || '12000', 10);
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function launchChrome() {
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'probe-cap-'));
const proc = spawn(CHROME, [
'--headless=new', '--remote-debugging-port=0', '--no-first-run',
'--no-default-browser-check', '--disable-gpu', '--disable-dev-shm-usage',
`--user-data-dir=${profile}`, '--window-size=1280,900', 'about:blank',
], { stdio: ['ignore', 'pipe', 'pipe'] });
const portFile = path.join(profile, 'DevToolsActivePort');
let port = null;
for (let i = 0; i < 150; 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(); this.on = () => {}; }
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);
} else if (m.method) c.on(m);
};
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 { proc, port, profile } = await launchChrome();
const captured = [];
let sessionId = null;
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 });
({ sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true }));
cdp.on = m => {
if (m.method === 'Network.requestWillBeSent') {
const r = m.params.request;
if (/turing\.captcha|cap_union/.test(r.url)) {
captured.push({ url: r.url, method: r.method, headers: r.headers,
postData: (r.postData || '').slice(0, 4000) });
}
} else if (m.method === 'Runtime.consoleAPICalled') {
const t = (m.params.args || []).map(a => a.value).join(' ');
if (t) process.stderr.write('[console] ' + t.slice(0, 300) + '\n');
}
};
await cdp.send('Network.enable', {}, sessionId);
await cdp.send('Page.enable', {}, sessionId);
await cdp.send('Runtime.enable', {}, sessionId);
await sleep(3000);
const loc = await cdp.send('Runtime.evaluate',
{ expression: 'location.href', returnByValue: true }, sessionId);
process.stderr.write('[page] ' + loc.result.value + '\n');
// 注入官方 SDK 并实例化目标 aid 的验证码
const inject = `(async () => {
window.__capcb = null;
await new Promise((res, rej) => {
const s = document.createElement('script');
s.src = 'https://turing.captcha.qcloud.com/TJCaptcha.js';
s.onload = res; s.onerror = () => rej(new Error('SDK 加载失败'));
document.head.appendChild(s);
});
if (typeof TencentCaptcha !== 'function')
return 'TencentCaptcha 未定义: ' + Object.keys(window).filter(k=>/captcha/i.test(k)).join(',');
window.__cap = new TencentCaptcha(${JSON.stringify(AID)}, function (res) {
window.__capcb = JSON.stringify(res);
});
window.__cap.show();
return 'shown';
})()`;
const r = await cdp.send('Runtime.evaluate',
{ expression: inject, awaitPromise: true, returnByValue: true }, sessionId);
process.stderr.write('[inject] ' + JSON.stringify(r.result.value || r.exceptionDetails) + '\n');
await sleep(WAIT);
// 如果验证码已经自己完成 (拖动/点选由我们后续处理), 这里先看回调
const cb = await cdp.send('Runtime.evaluate',
{ expression: 'window.__capcb || ""', returnByValue: true }, sessionId);
if (cb.result.value) process.stderr.write('[callback] ' + cb.result.value + '\n');
console.log(JSON.stringify({ captured }, null, 1));
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); });