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
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""把真机明文的值,与「语义名」精确配对。
|
||||
|
||||
难点:真机明文的槽顺序 = **它自己那次构建**的 require 顺序,
|
||||
而我手上只有 har/online/ 等若干构建;不知道明文属于哪一份。
|
||||
|
||||
解法(自洽性打分):把明文的值**按每个候选构建的语义名顺序**标注一遍,
|
||||
再看标注结果是否语义自洽(platform 的位置该是 "MacIntel"、user_agent 该含
|
||||
"Mozilla"、charset 该是 "UTF-8" …)。自洽度最高者即为它所属的构建;
|
||||
若都不匹配,也能从"哪些语义名被正确喂到"反推出每个名字的真实取值。
|
||||
|
||||
用法:
|
||||
.venv/bin/python scripts/verify/label_plaintext.py <明文文件> <若干 tdc.js>
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import tdc_modules as tm # noqa: E402
|
||||
|
||||
# ---- 语义名 -> 期望值特征(这就是"画像"的定义雏形)----
|
||||
def likes(name: str, v) -> str:
|
||||
"""值是否与语义名相容。返回 OK / NO / ??(?? = 该名字无法从值判断)"""
|
||||
s = json.dumps(v, ensure_ascii=False)
|
||||
if name.startswith("user_agent"):
|
||||
return "OK" if isinstance(v, str) and "Mozilla" in v else "NO"
|
||||
if name == "platform":
|
||||
return "OK" if isinstance(v, str) and v and " " not in v else "NO"
|
||||
if name == "languages":
|
||||
return "OK" if isinstance(v, list) and v and isinstance(v[0], str) else "NO"
|
||||
if name == "hardware_concurrency":
|
||||
return "OK" if isinstance(v, int) and 2 <= v <= 64 else "NO"
|
||||
if name == "screen_width":
|
||||
return "OK" if isinstance(v, int) and v > 500 else "NO"
|
||||
if name == "screen_height":
|
||||
return "OK" if isinstance(v, int) and v > 300 else "NO"
|
||||
if name == "screen_color_depth":
|
||||
return "OK" if isinstance(v, int) and 16 <= v <= 48 else "NO"
|
||||
if name == "screen_fingerprint":
|
||||
return "OK" if isinstance(v, str) and "-" in v and "*" in v else "NO"
|
||||
if name == "timezone_offset":
|
||||
return "OK" if isinstance(v, str) and re.fullmatch(r"[+-]\d\d", v) else "NO"
|
||||
if name == "charset":
|
||||
return "OK" if isinstance(v, str) and "UTF" in v.upper() else "NO"
|
||||
if name == "webgl_renderer":
|
||||
return "OK" if isinstance(v, str) and ("ANGLE" in v or "Renderer" in v) else "NO"
|
||||
if name == "webgl_vendor":
|
||||
return "OK" if isinstance(v, str) and ("Inc." in v or "Google" in v) else "NO"
|
||||
if name == "canvas_fingerprint":
|
||||
return "OK" if isinstance(v, str) and len(v) > 40 else "NO"
|
||||
if name == "constant_98k":
|
||||
return "OK" if v == "98k" else "NO"
|
||||
if name == "template_url":
|
||||
return "OK" if isinstance(v, str) and "captcha" in v and "rand=" in v else "NO"
|
||||
if name in ("location_href", "referrer"):
|
||||
return "OK" if isinstance(v, str) and v.startswith("http") else "??"
|
||||
if name == "trajectory":
|
||||
return "OK" if (isinstance(v, list) and v
|
||||
and all(isinstance(x, list) and len(x) == 3 for x in v)) else "NO"
|
||||
return "??"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) < 3:
|
||||
print(__doc__)
|
||||
return 2
|
||||
plain = open(sys.argv[1], encoding="latin1").read().rstrip("\x00")
|
||||
logical = re.sub(r" {2,}", "", plain)
|
||||
cd = json.loads(logical)["cd"]
|
||||
print("明文 cd 槽数: %d" % len(cd))
|
||||
print()
|
||||
|
||||
cands = []
|
||||
for a in sys.argv[2:]:
|
||||
cands += sorted(glob.glob(a))
|
||||
if not cands:
|
||||
print("没有候选构建")
|
||||
return 1
|
||||
|
||||
def assign(names, nslots):
|
||||
"""按语义名顺序把 nslots 个槽分配给各名字(交错推进)。"""
|
||||
out = {}
|
||||
k = 0
|
||||
for nm in names:
|
||||
if k >= nslots:
|
||||
break
|
||||
out.setdefault(nm, []).append(cd[k])
|
||||
k += 1
|
||||
return out
|
||||
|
||||
best = None
|
||||
for f in cands:
|
||||
rows = tm.analyze(f)
|
||||
names = [r["name"] for r in rows]
|
||||
# 假设除 itoken(占 2 槽)外都是 1 槽
|
||||
itok = [i for i, n in enumerate(names) if n.startswith("itoken")]
|
||||
n_extra = 1 if itok else 0
|
||||
# 逐个候选:把 cd 的槽按名字顺序切(itoken 那两个槽合并看第一个)
|
||||
mapping = {}
|
||||
k = 0
|
||||
for i, nm in enumerate(names):
|
||||
if k >= len(cd):
|
||||
break
|
||||
if i in itok:
|
||||
mapping[nm] = cd[k:k + 2]
|
||||
k += 2
|
||||
else:
|
||||
mapping[nm] = [cd[k]]
|
||||
k += 1
|
||||
ok = no = unk = 0
|
||||
for nm, vals in mapping.items():
|
||||
st = likes(nm, vals[0])
|
||||
ok += st == "OK"
|
||||
no += st == "NO"
|
||||
unk += st == "??"
|
||||
score = ok - no
|
||||
print(" %-46s OK=%2d NO=%2d ??=%2d 分=%d"
|
||||
% (Path(f).name, ok, no, unk, score))
|
||||
if best is None or score > best[0]:
|
||||
best = (score, f, mapping, ok, no)
|
||||
|
||||
print()
|
||||
print(">>> 最自洽的构建: %s (OK=%d NO=%d)" % (Path(best[1]).name, best[3], best[4]))
|
||||
print()
|
||||
print("该假设下的 name -> 值 标注:")
|
||||
for nm, vals in best[2].items():
|
||||
st = likes(nm, vals[0])
|
||||
mark = {"OK": " ", "NO": "!", "??": "?"}[st]
|
||||
print(" %s %-40s %s" % (mark, nm, json.dumps(vals, ensure_ascii=False)[:70]))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user