做了什么
- 从「签到成功-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,
对比返回是否多出鉴权字段 —— 这是唯一未做且成本低的关键实验
172 lines
7.8 KiB
JavaScript
172 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 纯监听模式: 只打开真实验证码页面, **不派发任何鼠标事件**。
|
|
* 由人工手动点选, 脚本在后台记录官方 SDK 自己发出的 verify 请求与响应,
|
|
* 用来判定「人手动过验证码是否也会被 errorCode=12 拒绝」。
|
|
*
|
|
* 用法: node manual_captcha.js <originUrl> <aid> [--proxy=http://ip:port] [--minutes=20]
|
|
*/
|
|
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 PROXY_ARG = process.argv.find(a => a.startsWith('--proxy='));
|
|
const PROXY = PROXY_ARG ? PROXY_ARG.slice(8) : null;
|
|
const MIN_ARG = process.argv.find(a => a.startsWith('--minutes='));
|
|
const MINUTES = MIN_ARG ? parseInt(MIN_ARG.slice(10), 10) : 20;
|
|
const MONITOR_ONLY = process.argv.includes('--monitor-only');
|
|
const OUT = '/tmp/1ms';
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
const LOG = fs.createWriteStream(path.join(OUT, 'manual.log'), { flags: 'a' });
|
|
const say = m => { const line = `[${new Date().toISOString().slice(11, 19)}] ${m}`;
|
|
process.stderr.write(line + '\n'); LOG.write(line + '\n'); };
|
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
|
|
async function launchChrome(profile) {
|
|
const args = ['--remote-debugging-port=0', '--no-first-run', '--no-default-browser-check',
|
|
`--user-data-dir=${profile}`, '--window-size=1100,900', '--window-position=80,40'];
|
|
if (PROXY) args.push(`--proxy-server=${PROXY}`);
|
|
args.push('about:blank');
|
|
const proc = spawn(CHROME, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
const portFile = path.join(profile, 'DevToolsActivePort');
|
|
for (let i = 0; i < 150; i++) {
|
|
if (fs.existsSync(portFile)) {
|
|
const p = parseInt(fs.readFileSync(portFile, 'utf8').split('\n')[0], 10);
|
|
if (p) return { proc, port: p, profile };
|
|
}
|
|
await sleep(100);
|
|
}
|
|
proc.kill('SIGKILL');
|
|
throw new Error('Chrome 未就绪');
|
|
}
|
|
|
|
class Cdp {
|
|
constructor(ws) { this.ws = ws; this.id = 0; this.waits = new Map(); this.handlers = []; }
|
|
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.handlers.forEach(h => h(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 eval(expr, sessionId) {
|
|
const r = await this.send('Runtime.evaluate',
|
|
{ expression: expr, awaitPromise: true, returnByValue: true }, sessionId);
|
|
return r.exceptionDetails ? null : r.result.value;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const profile = fs.mkdtempSync(path.join(os.tmpdir(), 'manual-cap-'));
|
|
const { proc, port } = await launchChrome(profile);
|
|
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 cdp.send('Runtime.enable', {}, sessionId);
|
|
await cdp.send('Network.enable', {}, sessionId);
|
|
await sleep(3000);
|
|
|
|
const pending = new Set();
|
|
let seq = 0;
|
|
cdp.handlers.push(m => {
|
|
if (m.method === 'Network.requestWillBeSent' &&
|
|
/cap_union_new_verify/.test(m.params.request.url)) {
|
|
const n = ++seq;
|
|
pending.add(m.params.requestId);
|
|
fs.writeFileSync(path.join(OUT, `manual_verify_req_${n}.txt`),
|
|
m.params.request.postData || '');
|
|
say(`官方 verify #${n} 已发出 (body ${(m.params.request.postData || '').length} 字节)`);
|
|
}
|
|
if (m.method === 'Network.loadingFinished' && pending.has(m.params.requestId)) {
|
|
const id = m.params.requestId;
|
|
pending.delete(id);
|
|
(async () => {
|
|
try {
|
|
const rb = await cdp.send('Network.getResponseBody', { requestId: id }, sessionId);
|
|
const txt = rb.base64Encoded
|
|
? Buffer.from(rb.body, 'base64').toString('utf8') : rb.body;
|
|
let code = '?';
|
|
try { code = JSON.parse(txt).errorCode; } catch (_) {}
|
|
const rec = { errorCode: code, response: txt, at: new Date().toISOString() };
|
|
fs.writeFileSync(path.join(OUT, 'manual_result.json'),
|
|
JSON.stringify(rec, null, 2));
|
|
say(`★★★★ verify 响应 errorCode=${code} 已写入 ${OUT}/manual_result.json`);
|
|
} catch (e) { say('取响应体失败: ' + e.message); }
|
|
})();
|
|
}
|
|
});
|
|
|
|
if (MONITOR_ONLY) {
|
|
say('====== [仅监听] 不做任何注入, 请在窗口里登录并走真实签到流程 ======');
|
|
say('====== 真实验证码出现后请手动完成, 我会记录 verify 结果 ======');
|
|
const until2 = Date.now() + MINUTES * 60000;
|
|
while (Date.now() < until2) {
|
|
await sleep(3000);
|
|
const cb = await cdp.eval('window.__capcb || ""', sessionId).catch(() => '');
|
|
if (cb) say('SDK 回调: ' + cb);
|
|
}
|
|
return;
|
|
}
|
|
await cdp.eval(`(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);
|
|
});
|
|
new TencentCaptcha(${JSON.stringify(AID)}, r => {
|
|
window.__capcb = JSON.stringify(r);
|
|
document.title = 'CAPCB:' + JSON.stringify(r).slice(0, 80);
|
|
}).show();
|
|
return 'ok';
|
|
})()`, sessionId);
|
|
|
|
say('====== 页面已就绪, 验证码已弹出 ======');
|
|
say('====== 请手动点选汉字 -> 点「确定」, 我不做任何点击 ======');
|
|
say(`====== 代理: ${PROXY || '直连'} 监听 ${MINUTES} 分钟 ======`);
|
|
|
|
const until = Date.now() + MINUTES * 60000;
|
|
let lastCb = '';
|
|
while (Date.now() < until) {
|
|
await sleep(3000);
|
|
const cb = await cdp.eval('window.__capcb || ""', sessionId);
|
|
if (cb && cb !== lastCb) {
|
|
lastCb = cb;
|
|
say('★★★ SDK 回调 (成功才会触发): ' + cb);
|
|
try {
|
|
fs.writeFileSync(path.join(OUT, 'manual_result.json'),
|
|
JSON.stringify({ callback: JSON.parse(cb) }, null, 2));
|
|
} catch (_) {}
|
|
}
|
|
}
|
|
say('监听结束, 浏览器保持打开');
|
|
} catch (e) {
|
|
say('异常: ' + e.message);
|
|
}
|
|
}
|
|
|
|
main().catch(e => { console.error('ERR', e.message); process.exit(1); });
|