Files
hello_js_reverse_skill/tools/cdp_module_probe.js
T
yml2213 51dff870cf feat(collect): 纯离线生成打通 1ms 签到文字点选 —— errorCode=0 收口
做了什么:
- 新增 tcaptcha/tdc_values.py: 37 个语义名的取值生成器(静态画像 / 会话量 /
  每次采集量三层)+ build_plain 四段组装 + generate_collect 纯离线出包
- crypto: 新增 generate_telemetry(mode="python_values");轨迹生成统一到
  trajectory_points()(首点绝对坐标 + 增量 + 固定收尾 [1,1,12])
- crypto.split_collect_segments 重写: pad 只有 1 个空格时旧 ` {2,}` 判据会漏段,
  且 UA 串里的普通空格会假阳性 —— 改为「模 24 算术 + 不落在 JSON 字符串内部 + 结构校验」
- tools/tdc_modules: trajectory 指纹原先指向只做 JSON.stringify 的模块,改为
  mouseMoveEvent/touchStartEvent(真轨迹模块),37/37 不变
- client 默认 telemetry_mode 改为 python_values
- 新增 scripts/verify/{align_session,audit_names,verify_values,verify_offline,send_test}.py
  与 tools/{cdp_collect,cdp_dump_scripts,tdc_profile}.py

为什么:
- 账本 #40「谁在发 HTTP 是 12 的剩余自变量」被推翻。同 aid/同客户端/同求解器、
  答案都对,只换 collect 构造方式: 本次会话构建生成 -> 0 (4/4),
  旧模板回填 -> 12 (2/2)。真因是 collect 必须与本次会话构建同源。

怎么验证:
- .venv/bin/python -m tcaptcha.selftest  全绿(含新增四段切分与纯离线闭环回归)
- scripts/verify/verify_values.py        4 份真机明文 152/152 逐槽全等
- scripts/verify/verify_offline.py       逐槽全等 + 分段几何一致
- scripts/verify/send_test.py --mode python_values  1ms 签到页文字点选 errorCode=0 x4
2026-10-12 03:38:18 +08:00

179 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* cdp_module_probe.js —— 在真 Chrome 里**抓到每个采集器的真实取值**(不需要手工配平)。
*
* 背景(2026-10-12):
* 腾讯验证码 iframe (captcha.gtimg.com) 与页面 (1ms.run) **不同站**,Chrome 站点隔离下
* 它是独立 target —— 直接在页面里包 Function.prototype.apply **进不去**(实测:
* 钩子装上时模块系统已经初始化完,__mods 恒为空)。
*
* 解法(CDP 标准做法):
* Target.setAutoAttach(autoAttach, waitForDebuggerOnStart, flatten)
* -> 新的 iframe target 一创建就**暂停在任何脚本执行之前**;
* -> 此时 Page.addScriptToEvaluateOnNewDocument 注册钩子,再 runIfWaitingForDebugger 放行;
* -> tdc.js 执行时钩子已就位,模块工厂的 require/get 全被记账。
*
* 产出: {url, mods: [[module_id, get()返回值], ...], reqs: [module_id,...]}
* 配合同一会话的 tdc.js 静态分析(tdc_modules.analyze)即可得到「语义名 -> 真实取值」,
* 无需任何人工对齐。
*
* 用法:
* node tools/cdp_module_probe.js --aid 195672344 --out /tmp/1ms_bridge/modvals.json
*/
const fs = require('fs');
function arg(name, def) {
const i = process.argv.indexOf('--' + name);
if (i < 0) return def;
const v = process.argv[i + 1];
return (v === undefined || v.startsWith('--')) ? true : v;
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
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) { for (const h of c.handlers) h(m); }
};
return c;
}
send(method, params = {}, sessionId) {
const id = ++this.id;
const msg = { id, method, params };
if (sessionId) msg.sessionId = sessionId;
this.ws.send(JSON.stringify(msg));
return new Promise((res, rej) => this.waits.set(id, { res, rej }));
}
on(fn) { this.handlers.push(fn); }
close() { try { this.ws.close(); } catch (e) {} }
}
// 在任何页面脚本之前注入的钩子:记账「模块工厂 require 了谁」+「每个模块 .get() 返回什么」
const HOOK = `(() => {
if (window.__mp) return;
window.__mp = true;
window.__mods = [];
window.__reqs = [];
const oa = Function.prototype.apply;
Function.prototype.apply = function (t, a) {
try {
if (a && a.length >= 3 && typeof a[a.length - 1] === 'function') {
const req = a[a.length - 1];
if (typeof req === 'function' && !req.__w) {
const w = function (id) {
try { window.__reqs.push(String(id)); } catch (e) {}
const m = req.apply(this, arguments);
try {
if (m && typeof m.get === 'function' && !m.__g) {
m.__g = true;
const og = m.get;
m.get = function () {
let v;
try { v = og.apply(this, arguments); } catch (e) { v = '__ERR__:' + e; }
try { window.__mods.push([String(id), v]); } catch (e) {}
return v;
};
}
} catch (e) {}
return m;
};
w.__w = true;
const na = Array.prototype.slice.call(a);
na[na.length - 1] = w;
return oa.call(this, t, na);
}
}
} catch (e) {}
return oa.call(this, t, a);
};
})();`;
const SHOW = aid => `(async () => {
const load = (src) => new Promise((res, rej) => {
const s = document.createElement('script');
s.src = src; s.onload = () => res(true); s.onerror = () => rej(new Error('fail'));
document.head.appendChild(s);
});
if (typeof window.TencentCaptcha !== 'function') await load('https://ssl.captcha.qq.com/TCaptcha.js');
window.__capres = null;
window.__cap = new window.TencentCaptcha('${aid}', function (r) { window.__capres = r; }, {});
window.__cap.show();
return {ok: true};
})()`;
(async () => {
const port = parseInt(arg('port', '9222'), 10);
const aid = arg('aid', '195672344');
const out = arg('out', '/tmp/1ms_bridge/modvals.json');
const waitMs = parseInt(arg('wait', '9000'), 10);
const list = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
const page = list.find(t => t.type === 'page' && !(t.url || '').startsWith('chrome'));
if (!page) throw new Error('没有可用 page target');
const pageCdp = await Cdp.connect(page.webSocketDebuggerUrl);
await pageCdp.send('Page.enable');
await pageCdp.send('Runtime.enable');
const ver = await (await fetch(`http://127.0.0.1:${port}/json/version`)).json();
const brCdp = await Cdp.connect(ver.webSocketDebuggerUrl);
const sessions = [];
brCdp.on(async m => {
if (m.method === 'Target.attachedToTarget') {
const { sessionId, targetInfo } = m.params;
sessions.push({ sessionId, targetInfo });
console.error(`[attach] ${targetInfo.type} ${(targetInfo.url || '').slice(0, 80)}`);
try {
await brCdp.send('Page.enable', {}, sessionId);
await brCdp.send('Page.addScriptToEvaluateOnNewDocument', { source: HOOK }, sessionId);
console.error(' -> hook registered');
} catch (e) { console.error(' -> hook fail', e.message); }
try { await brCdp.send('Runtime.runIfWaitingForDebugger', {}, sessionId); }
catch (e) {}
}
});
await brCdp.send('Target.setAutoAttach', {
autoAttach: true, waitForDebuggerOnStart: true, flatten: true,
});
console.error('[1] autoAttach 已开(新 target 会先暂停,等我们注册钩子)');
console.error('[2] 导航到 1ms.run 并拉起验证码');
await pageCdp.send('Page.navigate', { url: 'https://1ms.run/' });
await sleep(4000);
await pageCdp.send('Runtime.evaluate', {
expression: SHOW(aid), awaitPromise: true, returnByValue: true,
});
await sleep(waitMs);
console.error('[3] 在各 session 里读取记账结果');
const results = [];
for (const s of sessions) {
try {
const r = await brCdp.send('Runtime.evaluate', {
expression: `(() => { try { return {u: location.href, n: (window.__mods||[]).length,
mods: window.__mods||[], reqs: window.__reqs||[]}; } catch(e) { return {err:String(e)}; } })()`,
returnByValue: true,
}, s.sessionId);
const v = r.result && r.result.value;
if (v && v.mods && v.mods.length) { results.push(v); }
else if (v) console.error(` ${(v.u || '?').slice(0, 60)}: mods=${v.n || 0}`);
} catch (e) { console.error(' read fail', e.message); }
}
fs.writeFileSync(out, JSON.stringify(results, null, 1));
console.error(`[4] -> ${out} (命中 ${results.length} 个 session 有记账)`);
for (const r of results) {
console.error(` ${(r.u || '').slice(0, 70)} mods=${r.mods.length} reqs=${r.reqs.length}`);
}
brCdp.close(); pageCdp.close();
})().catch(e => { console.error('ERR', e.message); process.exit(1); });