Files
hello_js_reverse_skill/tcaptcha/telemetry/values.py
T
yml2213 344eea2194 refactor(结构): 分层重组 + 清除全部废弃路线
做了什么:
- 包改为分层: tcaptcha/{protocol,telemetry,solvers,ocr,adapters,models,tests}
  * protocol/  client.py  transport.py  proxy.py
  * telemetry/ crypto.py  values.py  key.py  opcodes.py
               disasm.py  program.py  layout.py  modules.py
  * solvers/   text_click.py  icon_click.py  icon_click_rotate.py  slider.py
  * ocr/       engines.py  slicer.py
  * adapters/  one_ms.py
- tools/ 里原本是**包运行时依赖**的 tdc_{disasm,program,layout,modules}.py 并入 telemetry/
  (放在 tools/ 属于分层倒置),tools/ 只留 CLI: tdc_key_pure.py / tdc_profile.py / cdp_*.js
- crypto.py 529 -> 340 行: 切除「成功样本模板回填」与「Node 插桩兜底」两条老路;
  generate_telemetry 只剩 python_values 一种模式,传别的会直接报错并说明原因
- 求解器改名分清: solver.py -> solvers/icon_click.py(client 实际在用的 YOLO+ResNet18 ONNX),
  icon_click_solver.py -> solvers/icon_click_rotate.py(仅测试在用,留作对照)
- pyproject: 去掉 ultralytics/onnx(训练栈已删),补上真正在用的 onnxruntime

删除了什么:
- 雨云整条线 + dist/rainyun_qinglong(104M 旧快照)
- 模板回填: models/success_collect_template.b64、tdc_worker.js、analyze_collect_layout.py
- 「补环境」时代浏览器 JS: chrome_fetch/manual_captcha/probe_captcha/solve_in_browser/tdc_browser
- 旧 VM 探索工具 10 个(tdc_collectgen/tdc_slotmap/tdc_lift/tdc_vm_run/tdc_query/tdc_probe 等)
  + 8 个基于它们的旧 verify 脚本
- 训练/标注/评估: train_classifier/train_detector/prepare_yolo/web_labeler/collector/
  siamese/evaluate/benchmark_verify
- tcaptcha/core_js/(60 个分析产物)、TOOLCHAIN_REPORT.md(整篇在描述已删工具)
- 3 个已无任何引用的 PyTorch 权重:icon_classifier.pt 43M / yolo_detector.pt 5.9M /
  siamese_matcher.pt 1.6M —— 线上只跑 onnx,tcaptcha/ 117M -> 66M

为什么:
- 项目后面要专门承载「滑块 / 文字点选 / 图标点选」三种求解器;扁平包 + 运行时依赖混在
  tools/ + 两条已废弃的 collect 生成路线,会让后续扩展持续踩坑

怎么验证:
- .venv/bin/python -m tcaptcha.selftest                    全绿
- scripts/verify/verify_offline.py                         逐槽全等 + 分段几何一致
- scripts/verify/verify_values.py                          4 份真机明文 152/152 逐槽全等
- scripts/verify/send_test.py --mode python_values         1ms 签到页文字点选 errorCode=0
- 回滚点: tag backup/pre-refactor-20261012
2026-10-12 12:34:57 +08:00

390 lines
16 KiB
Python
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.
# -*- coding: utf-8 -*-
"""tcaptcha/tdc_values.py —— 为每个采集器**语义名**提供取值生成器(纯离线)。
背景(账本 §五 第 0 项,har/1ms_dianxuan/PROGRESS.md):
「纯离线生成 collect」的唯一阻塞点 = 不知道每个语义名该填什么值。
本文件把 2026-10-12 实测到的**同会话真值**固化成生成规则。
真值来源(不是猜的):
真 Chrome + 真 1ms 页面 -> Debugger 取**同会话** tdc.js(77,954 字节,
含 __TENCENT_CHAOS_VM)-> iframe 内 TDC.getData(true) -> 同会话 XTEA 密钥解密
-> 按「require 序 == 槽位序」对齐,38/38 槽全覆盖。
脚本: scripts/verify/align_session.py、scripts/verify/audit_names.py
会话 vs 每次采集(连续 4 次 getData 实测,这一条决定了生成器怎么分层):
| 语义名 | 第1次 | 第2次 | 第3次 | 第4次 | 结论 |
|---------------------------|-------------|-------------|-------------|-------------|---------------|
| itoken | 641547489 | 641547489 | 641547489 | 641547489 | **会话内固定** |
| struct_40_9 (#.._rand) | 1591912482 | 1591912482 | 1591912482 | 1591912482 | **会话内固定** |
| window_time (#e48 委派) | 1791745224 | 1791745224 | 1791745224 | 1791745224 | **会话内固定** |
| window_time (#e40) | 1791745331 | 1791745548 | 1791745813 | 1791745814 | 每次刷新 |
| struct_46_9 | 1 | 2 | 3 | 4 | **每次 +1 的计数器** |
| trajectory 点数 | 1 | 61 | 1 | 1 | 取完即清空 |
三类取值:
* 静态画像 DEVICE_PROFILE —— UA / 屏幕 / WebGL / Canvas / 语言 / 时区 / 核数 …
(已用浏览器真值交叉校验: screen.width/height/availHeight/colorDepth、
hardwareConcurrency、timezoneOffset、languages 全部一一吻合)
* 会话量 Session —— itoken / 会话随机盐 / 页面加载时刻
* 每次采集 Session.next_call() —— 当前时刻、采集计数器、轨迹
"""
from __future__ import annotations
import random
import time
from typing import Any, Dict, List, Optional, Sequence
# --------------------------------------------------------------------------- #
# 设备画像:与真机逐项核对过(见模块 docstring 的交叉校验说明) #
# --------------------------------------------------------------------------- #
DEVICE_PROFILE: Dict[str, Any] = {
"name": "Mac / Chrome 155 / 1440x2560",
"user_agent": ("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"),
"platform": "MacIntel",
"languages": ["zh-CN", "zh"],
"timezone_offset": "+08", # 真机 Date.getTimezoneOffset() == -480
"charset": "UTF-8",
"hardware_concurrency": 10, # 真机 navigator.hardwareConcurrency
# ⚠️ 方向别弄反:真机 screen.width == 1440、screen.height == 2560
# (1440x2560 竖屏;浏览器实测 screen.availHeight == 2530)
"screen_width": 1440,
"screen_height": 2560,
"screen_color_depth": 24,
# 真机 screen_fingerprint 串;字段序 = 宽-高-可用高-色深-*-*-|-*
"screen_fingerprint": "1440-2560-2530-24-*-*-|-*",
"webgl_vendor": "Google Inc. (Apple)",
"webgl_renderer": "ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)",
"canvas_fingerprint": ("GgoAAAANSUhEUgAAASwAAACWCAYAAATf3v8bh+ryWcfSBSLkmN3N1"
"PQinPL7Bi3TG0qE2CogAAAABJRU5ErkJggg=="),
"viewport_size": [360, 360], # 验证码 iframe 的可视区
"cookie_enabled": 1,
"touch_detect": 2, # 真机实测值(该机 maxTouchPoints == 0)
"iframe_detect": "iframe",
"webrtc_ip": "", # 未取到候选时为空串
"dom_compliance": 1023,
"constant_98k": "98k",
"template_url": ("https://turing.captcha.gtimg.com/1/template/"
"drag_ele.html?rand=1519713624347"),
}
# 真机里恒为这些常量的槽(连续 4 次抽样一致)
CONSTANTS: Dict[str, Any] = {
"time_since_load": 0,
"webdriver_detect": 0,
"console_detect": 0,
"fp_salt": 0,
"struct_48_9_call": 0,
"location_href": "",
"struct_40_9#empty": "",
}
# 真机里是空数组的槽(与轨迹区分:轨迹至少含一个点)
EMPTY_LIST_NAME = "struct_76_33"
DEFAULT_REFERRER = "https://1ms.run/console/personal/benefits/check-in"
def _rand_int32() -> int:
"""[0, 2^31) 的随机整数 —— 真机观测值 1591912482 / 699129805 / 641547489 都落在这里。"""
return random.randint(0, (1 << 31) - 1)
def default_trajectory() -> List[List[int]]:
"""无鼠标输入时的轨迹(真机实测就是 [[1,1,12]],看着像个"无位移"哨兵点)。"""
return [[1, 1, 12]]
def trajectory_points(click_points: Sequence[Sequence[int]],
img_w: int = 672) -> List[List[int]]:
"""按点击点生成鼠标轨迹:首点绝对坐标 + 增量点 + 固定收尾 `[1,1,12]`。
形状与真机一致(3 份样本逐字核对):
* 首点是**绝对坐标**(含 300~600ms 初始停顿)
* 之后全是**增量** `[dx,dy,dt]`
* **最后一点固定是 `[1,1,12]`** —— 真机成功样本与"没动鼠标"样本都这么收尾
(没动鼠标时整条轨迹就是 `[[1,1,12]]`)
dt 分布按成功样本实测拟合(旧 crypto._success_style_trajectory 的统计)。
"""
scale = 320.0 / img_w
targets = [(int(round(20 + x * scale)), int(round(60 + y * scale)))
for x, y in click_points]
cur = [random.randint(140, 220), random.randint(300, 340)]
events: List[List[int]] = [[cur[0], cur[1], random.randint(300, 600)]]
def dt() -> int:
r = random.random()
if r < 0.50:
return random.randint(0, 16)
if r < 0.75:
return random.randint(17, 24)
if r < 0.90:
return random.randint(25, 60)
if r < 0.98:
return random.randint(61, 160)
return random.randint(161, 400)
for tx, ty in targets:
seg = random.randint(14, 20)
for i in range(1, seg + 1):
nx = cur[0] + (tx - cur[0]) * i / seg + random.randint(-1, 1)
ny = cur[1] + (ty - cur[1]) * i / seg + random.randint(-1, 1)
events.append([int(nx) - cur[0], int(ny) - cur[1], dt()])
cur = [int(nx), int(ny)]
for _ in range(random.randint(2, 5)): # 命中后短暂驻留
events.append([random.randint(-1, 1), random.randint(-1, 1), dt()])
events.append([1, 1, 12]) # 固定收尾(真机如此)
return events
def rows_from_source(js_text: str) -> List[Dict]:
"""分析 tdc.js **源码**(tdc_modules 的入口收的是路径,这里落临时文件再分析)。"""
import os
import tempfile
fd, path = tempfile.mkstemp(suffix=".js", prefix="tdc_")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(js_text)
return _analyze(path)
finally:
try:
os.unlink(path)
except OSError:
pass
def _analyze(path: str) -> List[Dict]:
"""惰性导入 tools/tdc_modules(它不在包里,且只做静态分析)。"""
import sys
from pathlib import Path as _P
tools = str(_P(__file__).resolve().parents[1] / "tools")
if tools not in sys.path:
sys.path.insert(0, tools)
from tcaptcha.telemetry import modules as tm
return tm.analyze(path)
def generate_collect(tdc_js_code: str,
click_points: Optional[Sequence[Sequence[int]]] = None,
*,
key: Optional[Sequence[int]] = None,
sd: Optional[Dict] = None,
session: Optional[Session] = None,
img_w: int = 672,
referrer: Optional[str] = None) -> Dict[str, Any]:
"""**纯离线**生成一颗 collect:只吃 tdc.js 源码,不需要任何真机明文。
流程: tdc.js -> 37 个语义名(清单序)-> 38 槽取值 -> 4 段明文 -> 4 段加密拼接。
密钥默认用纯 Python 静态提取(`tcaptcha.telemetry.key`),零 Node。
"""
from tcaptcha.telemetry.crypto import build_collect, extract_eks # 局部导入避免环形依赖
from tcaptcha.telemetry.key import extract_xtea_key_pure
rows = rows_from_source(tdc_js_code)
if key is None:
key = extract_xtea_key_pure(tdc_js_code)["key"]
sess = session or Session(referrer=referrer or DEFAULT_REFERRER)
if click_points:
sess.set_trajectory(trajectory_points(click_points, img_w=img_w))
cd = generate_cd(rows, sess)
plain = build_plain(cd, rows, sd)
collect = build_collect(plain["chunk1"], plain["trajectory"],
plain["chunk2"], plain["sd"], list(key))
return {
"collect": collect,
"tlg": str(len(collect)),
"eks": extract_eks(tdc_js_code),
"key": list(key),
"source": "python_values",
"cd": cd,
"segments": {k: len(v) for k, v in plain.items()},
}
class Session:
"""一次验证码会话的状态。
会话内固定的量(itoken / 随机盐 / 页面加载时刻)在 __init__ 里定下来,
每次采集会变的量(当前时刻 / 计数器 / 轨迹)由 next_call() 推进。
"""
def __init__(self, now: Optional[float] = None, entry_age: int = 60,
referrer: str = DEFAULT_REFERRER, profile: Optional[Dict] = None):
self.t0 = int(now if now is not None else time.time())
# 页面加载时刻先于会话建立(真机两次采样差 ~107 秒)
self.loaded_at = self.t0 - entry_age
self.itoken = [_rand_int32(), self.loaded_at] # 会话内固定,实测 4 次完全一致
self.session_rand = _rand_int32() # struct_40_9 的"随机"槽
self.referrer = referrer
self.profile = dict(profile or DEVICE_PROFILE)
self.calls = 0
self.now = self.t0
self.trajectory: List[List[int]] = default_trajectory()
def next_call(self, now: Optional[float] = None) -> "Session":
"""进入下一次 TDC.getData():计数器 +1、刷新当前时刻、轨迹复位由调用方决定。"""
self.calls += 1
self.now = int(now if now is not None else time.time())
return self
def set_trajectory(self, points: Sequence[Sequence[int]]) -> "Session":
self.trajectory = [list(p) for p in points]
return self
# --------------------------------------------------------------------------- #
# 语义名 -> 取值生成器 #
# --------------------------------------------------------------------------- #
def value_for(base: str, row: Dict, sess: Session) -> Any:
"""按**基础语义名**(去掉 #结构后缀)产出一个槽的值。
同一基础名在一个构建里可能出现多次(user_agent×2 / window_time×2 /
struct_40_9×2),这时用行上的**结构证据**分流,而不是靠出现次序 ——
次序会随构建变,结构不会(见 tdc_modules.STRUCT_RULES)。
"""
st = row.get("struct") or {}
p = sess.profile
# ---- 同组多成员:用结构 / 委派证据分流 ----
if base == "user_agent":
# 直采的那个返回 UA 串;走委派链的那个返回 0(真机实测)
return 0 if row.get("via_delegate") else p["user_agent"]
if base == "struct_40_9":
# 闭包最后写入 "" vs 随机 int32(账本 #47 的静态消歧依据)
return "" if st.get("closure_kind") == "empty" else sess.session_rand
if base == "window_time":
# 委派的那个是页面加载时刻(会话内固定),直采的那个是当前时刻(每次变)
return sess.loaded_at if row.get("via_delegate") else sess.now
# ---- 会话量 / 每次采集量 ----
if base == "itoken":
return None # 2 槽,见 generate_cd
if base == "trajectory":
return sess.trajectory
if base == "struct_46_9":
return sess.calls # 实测 1,2,3,4 —— 每次采集 +1
if base == "referrer":
return sess.referrer
# ---- 常量 / 画像 ----
if base in CONSTANTS:
return CONSTANTS[base]
if base == EMPTY_LIST_NAME:
return []
if base in p:
return p[base]
raise KeyError("没有 %r 的取值规则(新语义名? 需要补规则)" % base)
def generate_cd(rows: Sequence[Dict], sess: Optional[Session] = None,
now: Optional[float] = None) -> List[Any]:
"""按**清单顺序**生成 cd 数组(37 项 -> 38 槽,itoken 占 2 槽)。
rows 来自 tools.tdc_modules.analyze()(每项含 name / struct / via_delegate)。
每次调用会推进 sess.next_call()(模拟一次 TDC.getData)。
"""
s = sess or Session(now=now)
if now is not None:
s.next_call(now)
elif s.calls == 0:
s.next_call(s.t0)
out: List[Any] = []
for r in rows:
base = str(r["name"]).split("#")[0]
if base == "itoken":
out.extend(s.itoken) # 真机就是 2 个连续槽
continue
out.append(value_for(base, r, s))
return out
def trajectory_index(rows: Sequence[Dict]) -> int:
"""轨迹在 cd 中的下标 —— 它就是 4 段加密里第 2 段的分界点。"""
k = 0
for r in rows:
base = str(r["name"]).split("#")[0]
if base == "trajectory":
return k
k += 2 if base == "itoken" else 1
raise KeyError("清单里没有 trajectory 项")
def build_plain(cd: Sequence[Any], rows: Sequence[Dict],
sd: Optional[Dict] = None) -> Dict[str, str]:
"""把 cd 组装成 4 段明文(chunk1 / trajectory / chunk2 / sd)。
与真机**逐字节一致**的分段写法(3 份样本核对,2026-10-12):
chunk1 = '{"cd":[' + 轨迹之前的槽(逗号连接)+ ','
traj = '<轨迹槽的 JSON>'
chunk2 = ',' + 轨迹之后的槽(逗号连接)+ '],'
sd = '"sd":' + json(sd) + '}'
分隔符一律是**无空格的逗号**(真机 JSON.stringify 风格)。
"""
import json
def js(v: Any) -> str:
return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
k = trajectory_index(rows)
head = ",".join(js(v) for v in cd[:k])
tail = ",".join(js(v) for v in cd[k + 1:])
return {
"chunk1": '{"cd":[' + head + ",",
"trajectory": js(cd[k]),
"chunk2": "," + tail + "],",
"sd": '"sd":' + js(sd if sd is not None else {"od": "C"}) + "}",
}
def main() -> int:
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))
ap = argparse.ArgumentParser(description="按语义名生成 cd 取值(纯离线)")
ap.add_argument("tdc_js", help="一份 tdc.js(用来取清单顺序与语义名)")
ap.add_argument("--json", default=None)
args = ap.parse_args()
from tcaptcha.telemetry import modules as tm
rows = tm.analyze(args.tdc_js)
sess = Session()
cd = generate_cd(rows, sess)
print("项 语义名 取值")
print("-" * 78)
k = 0
for r in rows:
base = str(r["name"]).split("#")[0]
n = 2 if base == "itoken" else 1
val = cd[k:k + n] if n > 1 else cd[k]
k += n
print("%-3d %-36s %s" % (r["item"], r["name"],
json.dumps(val, ensure_ascii=False)[:34]))
print()
print("槽数 %d / %d 项" % (len(cd), len(rows)))
plain = build_plain(cd, rows)
print("分段: chunk1=%d traj=%d chunk2=%d sd=%d"
% (len(plain["chunk1"]), len(plain["trajectory"]),
len(plain["chunk2"]), len(plain["sd"])))
if args.json:
Path(args.json).write_text(json.dumps(cd, ensure_ascii=False, indent=1),
encoding="utf-8")
print(">>> %s" % args.json)
return 0
if __name__ == "__main__":
raise SystemExit(main())