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
This commit is contained in:
yml2213
2026-10-12 12:34:57 +08:00
parent 51dff870cf
commit 344eea2194
169 changed files with 599 additions and 83331 deletions
-182
View File
@@ -1,182 +0,0 @@
# -*- coding: utf-8 -*-
"""
collect 明文「字段布局」分析器 —— 回答一个关键问题:
`collect` 解密后的 `cd` 数组, 槽位顺序到底是"按 tdc.js 版本固定"还是"按会话/构建随机"?
为什么要问这个 (2026-10-11 排查 errorCode=12):
tcaptcha/models/success_collect_template.b64 存的是**明文**模板 (base64 封装),
generate_collect_success() 只回填 轨迹 / entry_url / 几个时间戳, 其余字段原样保留。
真机成功样本解密后发现: **同一批值, 顺序完全不同** —— 若服务端按"本次构建的槽位表"
解释 cd, 那么我们用旧槽位序发出的 collect 会被读成"自相矛盾的环境" -> 12。
本脚本把多份样本 (HAR + CDP 抓包 + 仓库模板) 的 cd 槽位打成类型指纹并对比顺序。
用法:
.venv/bin/python scripts/analyze_collect_layout.py # 默认样本集
.venv/bin/python scripts/analyze_collect_layout.py --json /tmp/cap2.json
"""
from __future__ import annotations
import argparse
import base64
import json
import os
import re
import sys
import urllib.parse
from pathlib import Path
from typing import Dict, List, Optional, Tuple
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tcaptcha.crypto import xtea_decrypt_bytes # noqa: E402
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
HARS = [
("har/原始抓包/签到成功-1.har", "签到成功-1"),
("har/原始抓包/点选.har", "点选"),
("har/login_checkin.har", "login_checkin"),
]
def _decrypt_collect(collect_b64: str, tdc_js: str) -> Optional[dict]:
key = extract_xtea_key_pure(tdc_js)["key"]
pt = xtea_decrypt_bytes(base64.b64decode(collect_b64), key)
text = pt.decode("latin1").rstrip("\x00").strip()
try:
return json.loads(text)
except Exception:
return None
def from_har(path: str) -> List[Tuple[str, dict]]:
if not os.path.exists(path):
return []
data = json.load(open(path, encoding="utf-8"))
tdcs: Dict[str, str] = {}
sess2sid: Dict[str, str] = {}
verifies: List[dict] = []
for e in data["log"]["entries"]:
url = e["request"]["url"]
content = (e["response"].get("content") or {}).get("text") or ""
if "tdc.js" in url and content:
q = urllib.parse.parse_qs(urllib.parse.urlsplit(url).query)
tdcs[(q.get("app_data") or [""])[0]] = content
elif "cap_union_prehandle" in url and content:
m = re.search(r"^[^(]*\((.*)\)\s*$", content, re.S)
if m:
try:
j = json.loads(m.group(1))
sess2sid[j.get("sess", "")] = str(j.get("sid", ""))
except Exception:
pass
elif "cap_union_new_verify" in url:
pd = (e["request"].get("postData") or {}).get("text") or ""
if pd:
verifies.append(dict(urllib.parse.parse_qsl(pd, keep_blank_values=True)))
out = []
for i, v in enumerate(verifies):
js = tdcs.get(sess2sid.get(v.get("sess", ""), ""))
if not js:
continue
j = _decrypt_collect(v["collect"], js)
if j is not None:
out.append((f"{Path(path).name}#{i}", j))
return out
def from_cdp_capture(path: str) -> List[Tuple[str, dict]]:
if not os.path.exists(path):
return []
d = json.load(open(path, encoding="utf-8"))
recs = d.get("records") or []
tdcs = [(r["url"], r.get("body") or "") for r in recs if "tdc.js" in r["url"] and r.get("body")]
vers = [r for r in recs if "verify" in r["url"] and r.get("postData")]
out = []
for i, v in enumerate(vers):
form = dict(urllib.parse.parse_qsl(v["postData"], keep_blank_values=True))
sess = form.get("sess", "")
# sess 前 2 段与 tdc.js 的 app_data(sid) 无直接映射, 这里按时间就近取**最后一份**构建
js = tdcs[-1][1] if tdcs else ""
if not js:
continue
j = _decrypt_collect(form["collect"], js)
if j is not None:
out.append((f"{Path(path).name}#{i}", j))
return out
def slot_tags(cd: List) -> List[str]:
tags = []
for v in cd:
if isinstance(v, str):
if v.startswith("http"):
tags.append("url")
elif "Mozilla" in v:
tags.append("UA")
elif v.startswith("GgoAAA"):
tags.append("png")
elif re.fullmatch(r"[-+]\d\d", v):
tags.append("tz")
elif v == "":
tags.append("empty")
elif len(v) <= 6 and v.isupper():
tags.append("enc:" + v)
else:
tags.append("str:" + v[:10])
elif isinstance(v, list):
if len(v) == 2 and all(isinstance(x, int) for x in v):
tags.append("vec2")
elif v and isinstance(v[0], list):
tags.append("traj")
else:
tags.append("list%d" % len(v))
elif isinstance(v, bool):
tags.append("bool")
else:
tags.append("num")
return tags
def main() -> int:
ap = argparse.ArgumentParser(description="collect 明文字段布局分析")
ap.add_argument("--json", action="append", default=[], help="额外加入的 cdp_capture JSON")
args = ap.parse_args()
samples: List[Tuple[str, dict]] = []
for p in args.json:
samples += from_cdp_capture(p)
for p, tag in HARS:
for name, j in from_har(str(ROOT / p)):
samples.append((f"{tag}#{name.split('#')[-1]}", j))
tpl_path = ROOT / "tcaptcha" / "models" / "success_collect_template.b64"
if tpl_path.exists():
tpl = json.loads(base64.b64decode(tpl_path.read_text(encoding="ascii"))
.decode("latin1").rstrip("\x00").strip())
samples.append(("TEMPLATE(仓库在用)", tpl))
if not samples:
print("没有可用样本")
return 1
for name, j in samples:
tags = slot_tags(j["cd"])
print(f"\n[{name}] len(cd)={len(j['cd'])} sd={j.get('sd')}")
print(" " + " ".join(f"{i}:{t}" for i, t in enumerate(tags)))
print("\n=== 槽位指纹两两对比 (看顺序是否随会话变化) ===")
sigs = [(n, slot_tags(j["cd"])) for n, j in samples]
base_name, base = sigs[0]
print(f" 基准 = {base_name}")
for n, s in sigs[1:]:
diff = [i for i in range(max(len(base), len(s)))
if (base[i] if i < len(base) else None) != (s[i] if i < len(s) else None)]
print(f" {n:22s} 顺序相同={s == base} 不同槽位数={len(diff)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+5 -5
View File
@@ -10,8 +10,8 @@
1. 用 CDP 在真实页面里刷新腾讯验证码 widget (`__cap.refresh()`);
2. 读 widget 内的题型/题面/几何 (滑块: slideBg + 拼图块 + 拖动柄; 点选: 提示图 + 背景图);
3. 求解:
* 滑块 -> tcaptcha.slider_solver.SliderSolver (ZNCC 找真缺口)
* 图文点选 -> tcaptcha.icon_click_solver.IconClickSolver (形状模板 + 旋转 IoU)
* 滑块 -> tcaptcha.solvers.slider.SliderSolver (ZNCC 找真缺口)
* 图文点选 -> tcaptcha.solvers.icon_click.IconClickSolver (形状模板 + 旋转 IoU)
4. 用 CDP `Input.dispatchMouseEvent` 派发**真实(trusted)鼠标事件** (点击 / 拖动);
5. 读回 widget 回调的 {ret, ticket, randstr}。
@@ -38,9 +38,9 @@ if str(ROOT) not in sys.path:
import cv2 # noqa: E402
from tcaptcha.glyph_solver import TextClickSolver # noqa: E402
from tcaptcha.icon_click_solver import IconClickSolver # noqa: E402
from tcaptcha.slider_solver import SliderSolver, sprite_region # noqa: E402
from tcaptcha.solvers.text_click import TextClickSolver # noqa: E402
from tcaptcha.solvers.icon_click_rotate import IconClickSolver # noqa: E402
from tcaptcha.solvers.slider import SliderSolver, sprite_region # noqa: E402
NODE = "node"
CDP_EVAL = str(ROOT / "tools" / "cdp_eval.js")
-67
View File
@@ -1,67 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
真机 aid 冒烟测试 —— 用**可反复测**的真实 aid 回归"纯算链路是否完好"。
背景 (har/1ms_dianxuan/PROGRESS.md 已知 #26/#27):
aid 195672344 (1ms.run) 对本客户端恒返回 errorCode=12 (aid 级风控, 与请求侧无关),
且 1ms 签到每天只能试一次 —— 不适合做回归。
而抓包 `har/原始抓包/登录抓包-点选.har` 里的 aid **190758559** (mliev.login.wang 短信登录)
可反复测: 本客户端 (纯算, 零 Node) 稳定拿到 errorCode=0 + 真 ticket。
用法:
.venv/bin/python scripts/check_real_tencent_aid.py # 默认 190758559
.venv/bin/python scripts/check_real_tencent_aid.py --aid 195672344 # 对照: 1ms 会得到 12
注意: 只调用腾讯天御的验证码接口 (prehandle/tdc.js/getcapbysig/verify),
**不会**触发任何短信/签到等业务副作用。
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
# (aid, entry_url, referer, wb)
TARGETS = {
"190758559": ("https://mliev.login.wang/auth/#/oidc/authorize", "https://mliev.login.wang/", "1"),
"195672344": ("https://1ms.run/console/personal/benefits/check-in", "https://1ms.run/", "1"),
}
def main() -> int:
ap = argparse.ArgumentParser(description="真机 aid 冒烟: 纯算链路端到端")
ap.add_argument("--aid", default="190758559", choices=sorted(TARGETS))
ap.add_argument("--transport", default="httpx", choices=["httpx", "curl"])
ap.add_argument("--refreshes", type=int, default=25)
args = ap.parse_args()
entry_url, referer, wb = TARGETS[args.aid]
client = TencentCaptchaClient(aid=args.aid, entry_url=entry_url, referer=referer, wb=wb,
accept_kinds=(KIND_TEXT_CLICK,), telemetry_mode="python_keyed",
transport=args.transport)
try:
result = client.solve(max_retries=2, max_refreshes=args.refreshes)
finally:
client.close()
raw = result.get("raw_response") or {}
print("\n=== 结果 ===")
print("aid :", args.aid)
print("transport :", args.transport)
print("success :", result.get("success"))
print("errorCode :", raw.get("errorCode"))
print("ticket :", (raw.get("ticket") or "")[:32])
print("randstr :", raw.get("randstr"))
print("telemetry :", result.get("raw_response", {}).get("sess") and "python_keyed(纯算)")
return 0 if result.get("success") else 1
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -35,7 +35,7 @@ ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
def run_once(tag: str, cookies, args) -> dict:
-153
View File
@@ -1,153 +0,0 @@
# -*- coding: utf-8 -*-
"""
实验 C —— 用「按本次构建生成的 collect」打一次完整 verify,检验 12 是否消失。
背景(PROGRESS.md #31/#32/#34/#35/#36/#37):
槽位序按构建随机化;仓库里的模板是**某一次**的顺序;
`tools/tdc_collectgen.py` 已能按本次构建重排并加密(回环自洽)。
这一发要回答:
* 用「本次构建的槽位序」发出的 collect,aid 195672344 是否 **12 -> 0**?
- 若 0 -> H-LAYOUT 成立(12 的根因就是槽位序),这条链彻底闭合
- 若仍 12 -> 槽位序**不是** 12 的自变量,H-LAYOUT 需要改,
但我们已经知道该查什么(转去查请求方/会话绑定,即实验 A)
⚠️ 样本要求(#31): 重排需要一份**同机**的真机明文(取值集合相同、只有顺序不同)。
本脚本的 --sample 就是它;本次样本恰好与本次构建同序,因此这一发是"原序复现"。
用法:
.venv/bin/python scripts/experiment_layout_verify.py \\
--build /tmp/build.js --sample /tmp/sample_cd.json --kinds /tmp/items3.json
"""
from __future__ import annotations
import argparse
import json
import random
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
if str(ROOT / "tools") not in sys.path:
sys.path.insert(0, str(ROOT / "tools"))
import tdc_collectgen as cg # noqa: E402
import tdc_layout # noqa: E402
import tdc_program as tp # noqa: E402
from tcaptcha.client import TencentCaptchaClient # noqa: E402
from tcaptcha.crypto import extract_eks, extract_xtea_key # noqa: E402
def one_verify(client, ch, tkid, collect: str, eks: str, pts) -> dict:
ans = json.dumps([{"elem_id": i + 1, "type": "DynAnswerType_POS",
"data": f"{p[0]},{p[1]}"} for i, p in enumerate(pts)],
separators=(",", ":"))
body = {"ans": ans, "collect": collect, "tlg": str(len(collect)),
"eks": eks, "sess": ch.sess, "tkid": tkid,
"pow_answer": "", "pow_calc_time": "1"}
r = client.http_client.post(
"/cap_union_new_verify", data=body,
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Origin": "https://turing.captcha.gtimg.com",
"Referer": "https://turing.captcha.gtimg.com/"})
return r.json()
def main() -> int:
ap = argparse.ArgumentParser(description="用按构建生成的 collect 打 verify(带对照组)")
ap.add_argument("--build", required=True, help="本次会话的 tdc.js 路径")
ap.add_argument("--sample", required=True, help="同机的真机 cd(json: {cd:[...]} 或 [...])")
ap.add_argument("--kinds", default=None, help="运行时取样的 items json(提供 kind/v0)")
ap.add_argument("--aid", default="195672344")
ap.add_argument("--entry-url", default="https://1ms.run/console/personal/benefits/check-in")
ap.add_argument("--n", type=int, default=12, help="每种 collect 各打几次(统计用)")
ap.add_argument("--solve", action="store_true",
help="真解题: 按题型用本地求解器算答案(12 只在答案正确时才判, 见说明)")
args = ap.parse_args()
js = open(args.build, encoding="utf-8", errors="replace").read()
prog = tp.load(args.build)
_mid, _entry, seq = tdc_layout.find_manifest_module(prog)
key = extract_xtea_key(js)
eks = extract_eks(js)
kinds, v0s = cg.load_shape(args.kinds, seq) if args.kinds else (None, None)
spec = cg.SlotSpec(seq, kinds, v0s)
sample = cg.load_sample_cd(args.sample)
cd = cg.reorder(sample, spec)
collect = cg.encrypt_plain(cg.build_plain(cd), key)
print(f"[1] 构建 : {args.build}")
print(f" 清单模块 : registry[{_mid}] 项 {len(seq)}")
print(f" 槽位规格 : {spec.describe()}")
print(f" 密钥 : {key}")
print(f" eks : {len(eks)} 字符")
print(f"[2] collect : {len(collect)} 字符 (tlg={len(collect)}) 槽数 {len(cd)}")
# 走一次完整协议: prehandle -> tdc(已在手) -> getcapbysig -> 解 -> verify, 但**替换 collect**
tkid = str(random.randint(100000000, 999999999))
client = TencentCaptchaClient(aid=args.aid, entry_url=args.entry_url,
referer="https://1ms.run/")
# ⚠️ 12 是"答案正确之后"的风控判定, 答错一律 50/9, 两组就没区分度。
# 所以 --solve 时按题型真解; 只统计**可解**的轮次。
def solve_points(ch):
if not args.solve:
return [(120, 90), (180, 150), (240, 210)]
if ch.kind in ("text", "text_click"):
res = client.text_solver.solve_ex(client.fetch_image(ch.img_url, 1),
ch.instruction)
return res.points if res.ok else None
if ch.kind in ("icon", "icon_click"):
bg = client.fetch_image(ch.img_url, 1)
pr = client.fetch_image(ch.img_url, 0)
if bg and pr:
return client.icon_solver.solve(bg, pr) or None
return None
return None # 滑块: 本脚本不处理(用 --no-solve 时才有意义)
# 对照组: 仓库旧模板(某一次的槽位序), 同一份构建/同一 session 流程
from tcaptcha.crypto import generate_telemetry
# 对照组只需"仓库旧模板的槽位序", 轨迹用固定点即可(不影响槽位序这一自变量)
tm = generate_telemetry(js, [(120, 90), (180, 150), (240, 210)],
aid=args.aid, user_agent=client.user_agent,
referrer=client.referer)
tally = {"new(按构建)": {}, "old(旧模板)": {}}
try:
solved = 0
for rnd in range(1, args.n + 1):
ch, _cfg, _sid = client.prehandle(tkid)
pts = solve_points(ch)
if not pts:
print(f" [{rnd:02d}] 跳过 (kind={ch.kind}, 本脚本不处理)")
continue
solved += 1
print(f" [{rnd:02d}] kind={ch.kind} pts={pts}")
for tag, collect in (("new(按构建)", collect), ("old(旧模板)", tm["collect"])):
v = one_verify(client, ch, tkid, collect, eks, pts)
code = str(v.get("errorCode"))
tally[tag][code] = tally[tag].get(code, 0) + 1
print(f" [{rnd:02d}] {tag:12s} -> {code}")
print(f"\n (有效轮次: {solved})")
for tag, d in tally.items():
print(f" {tag:12s} 分布: {d}")
new12 = tally["new(按构建)"].get("12", 0)
old12 = tally["old(旧模板)"].get("12", 0)
print()
if new12 == 0 and old12 > 0:
print(">>> ✅ 按构建生成后 12 消失(对照组仍有 12) -> H-LAYOUT 成立, 槽位序就是根因")
elif new12 and old12:
print(">>> ❌ 两组都有 12 -> 槽位序不是 12 的自变量, H-LAYOUT 需修正")
elif not new12 and not old12:
print(">>> ⚠️ 两组都没有 12(多为 50/9=答案未命中) -> 本轮题型/条件下风控未介入, "
"需要换成能答对的题型或加大样本再判")
return 0
finally:
client.close()
if __name__ == "__main__":
raise SystemExit(main())
+2 -2
View File
@@ -34,8 +34,8 @@ if str(ROOT) not in sys.path:
if str(ROOT / "tools") not in sys.path:
sys.path.insert(0, str(ROOT / "tools"))
import tdc_layout # noqa: E402
import tdc_program as tp # noqa: E402
from tcaptcha.telemetry import layout # noqa: E402
from tcaptcha.telemetry import program as tp # noqa: E402
TRAP_JS = r"""(() => {
if (window.__reqTrapInstalled) return {already: true};
+1 -1
View File
@@ -37,7 +37,7 @@ ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
TRANSPORTS = {"a": ("httpx", None), "b": ("curl", "chrome")}
+3 -3
View File
@@ -25,9 +25,9 @@ ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
from tcaptcha.client import TencentCaptchaClient # noqa: E402
from tcaptcha.crypto import extract_eks # noqa: E402
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
from tcaptcha.protocol.client import TencentCaptchaClient # noqa: E402
from tcaptcha.telemetry.crypto import extract_eks # noqa: E402
from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402
OUT = ROOT / "har" / "online"
-492
View File
@@ -1,492 +0,0 @@
# -*- coding: utf-8 -*-
"""
cron: 30 8 * * *
new Env('雨云每日签到');
雨云 (Rainyun) 自动登录与每日签到脚本 (适用于青龙面板)
账号配置规范:
环境变量 / 本地 .env 统一使用 RAINYUN_ACCOUNT="账号#密码"
多账号分割:
- 青龙环境变量: 使用 & 分割 (如 RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2")
- 本地 .env 文件: 支持 & 分割或换行分割
"""
import json
import logging
import os
import random
import re
import sys
import time
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
import httpx
# 将项目根目录加入 sys.path 以便加载本地模块
CURRENT_DIR = Path(__file__).resolve().parent
if str(CURRENT_DIR) not in sys.path:
sys.path.insert(0, str(CURRENT_DIR))
# 配置日志格式
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("rainyun_checkin")
# 青龙通知模块导入
try:
from sendNotify import send
except Exception:
try:
from notify import send
except Exception:
def send(title: str, content: str):
pass
# 默认配置
RAINYUN_API_BASE = "https://api.v2.rainyun.com"
TENCENT_AID = "2039519451"
DEFAULT_UA = (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
)
# 积分提现比例: 2000 积分 = 1 元现金 (支持直接提现)
POINTS_PER_CNY = 2000.0
def format_points(points: int) -> str:
"""格式化积分及等值提现金额 (2000积分 = 1元)"""
cny = points / POINTS_PER_CNY
return f"{points} 积分 (≈ {cny:.2f} 元)"
def mask_field(val: str) -> str:
"""脱敏账号信息"""
if not val:
return ""
if len(val) <= 4:
return "***"
if len(val) == 11 and val.isdigit():
return f"{val[:3]}****{val[7:]}"
return f"{val[:2]}****{val[-2:]}"
class CaptchaManager:
"""验证码求解调度器:支持本地 tcaptcha 模块或远程 HTTP 接口"""
def __init__(self, api_url: Optional[str] = None):
self.api_url = api_url or os.getenv("TCAPTCHA_API_URL", "").strip()
self.local_client = None
# 尝试初始化本地客户端
if not self.api_url:
try:
from tcaptcha.client import TencentCaptchaClient
self.local_client = TencentCaptchaClient(aid=TENCENT_AID)
logger.info("✓ 成功加载本地 tcaptcha 纯算法求解引擎")
except Exception as e:
logger.warning(f"未能加载本地 tcaptcha 求解引擎 ({e}),将尝试远程接口或环境变量配置")
def get_ticket(self) -> Tuple[Optional[str], Optional[str]]:
"""获取腾讯天御点选验证码票据 (ticket, randstr)"""
# 1. 优先使用远程 API (若已配置)
if self.api_url:
logger.info(f"正在调用远程验证码求解接口: {self.api_url} ...")
try:
with httpx.Client(timeout=30.0, trust_env=False) as client:
resp = client.post(self.api_url, json={"aid": TENCENT_AID})
if resp.status_code == 200:
data = resp.json()
# 兼容直接返回或嵌套在 data 中
res_data = data.get("data", data)
ticket = res_data.get("ticket")
randstr = res_data.get("randstr")
if ticket and randstr:
logger.info(f"✓ 远程验证码求解成功 (randstr: {randstr})")
return ticket, randstr
else:
logger.error(f"远程验证码接口未返回有效票据: {data}")
else:
logger.error(f"远程验证码接口响应异常: {resp.status_code} {resp.text}")
except Exception as e:
logger.error(f"调用远程验证码接口失败: {e}")
# 2. 本地求解引擎
if self.local_client:
logger.info("正在调用本地 tcaptcha AI 纯协议求解器...")
try:
res = self.local_client.solve()
if res.get("success"):
logger.info(f"✓ 本地求解成功 (耗时: {res.get('elapsed_ms')}ms, randstr: {res.get('randstr')})")
return res.get("ticket"), res.get("randstr")
else:
logger.error(f"本地求解失败: {res.get('error')}")
except Exception as e:
logger.error(f"本地求解器执行异常: {e}")
logger.error("未找到可用的验证码求解方案!请确认部署了 tcaptcha 本地模型或配置了 TCAPTCHA_API_URL 环境变量")
return None, None
class RainyunClient:
"""雨云 API 客户端"""
def __init__(
self,
user: str,
password: str,
captcha_mgr: Optional[CaptchaManager] = None,
):
self.user = user
self.password = password
self.captcha_mgr = captcha_mgr or CaptchaManager()
self.account_name = mask_field(user) if user else "未知账号"
self.points_before = 0
self.points_after = 0
# 初始化 HTTP 客户端
self.session = httpx.Client(
base_url=RAINYUN_API_BASE,
timeout=15.0,
follow_redirects=True,
trust_env=False, # 屏蔽环境代理干扰以兼容各类青龙网络
)
self._update_common_headers()
def _update_common_headers(self):
rys = f"{uuid.uuid4().hex}{time.time():.3f}"
self.session.headers.update({
"User-Agent": DEFAULT_UA,
"Origin": "https://app.rainyun.com",
"Referer": "https://app.rainyun.com/",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"rys": rys,
})
def login(self) -> bool:
"""执行登录并获取凭据"""
if not self.user or not self.password:
logger.error("缺少账号或密码!")
return False
logger.info(f"正在登录雨云账号: {self.account_name} ...")
self._update_common_headers()
payload = {
"field": self.user,
"password": self.password,
"verifyCode": "",
}
resp = self.session.post("/user/login", json=payload)
res_json = {}
try:
res_json = resp.json()
except Exception:
pass
# 检查是否需要验证码
if resp.status_code == 400 and res_json.get("code") in [10004, 10005]:
logger.info("雨云登录要求完成人机验证,正在调用验证码求解器...")
ticket, randstr = self.captcha_mgr.get_ticket()
if not ticket or not randstr:
logger.error("登录验证码获取失败,无法继续登录")
return False
payload["vticket"] = ticket
payload["vrandstr"] = randstr
resp = self.session.post("/user/login", json=payload)
try:
res_json = resp.json()
except Exception:
pass
if resp.status_code == 200 and res_json.get("code") == 200:
# 同步更新 CSRF Token
csrf = self.session.cookies.get("X-CSRF-Token")
if csrf:
self.session.headers["x-csrf-token"] = csrf
logger.info(f"✓ 账号 {self.account_name} 登录成功!")
return True
err_msg = res_json.get("message") or resp.text
logger.error(f"❌ 账号 {self.account_name} 登录失败: {err_msg}")
return False
def fetch_user_info(self) -> Optional[Dict[str, Any]]:
"""获取当前用户信息(积分、用户名等)"""
self._update_common_headers()
try:
resp = self.session.get("/user/?no_cache=true")
if resp.status_code == 200:
data = resp.json()
if data.get("code") == 200:
info = data.get("data", {})
self.account_name = info.get("Name") or self.account_name
self.points_before = info.get("Points", 0)
return info
except Exception as e:
logger.warning(f"获取用户信息异常: {e}")
return None
def fetch_tasks(self) -> List[Dict[str, Any]]:
"""获取任务列表"""
self._update_common_headers()
try:
resp = self.session.get("/user/reward/tasks")
if resp.status_code == 200:
data = resp.json()
if data.get("code") == 200:
return data.get("data", [])
except Exception as e:
logger.error(f"获取任务列表失败: {e}")
return []
def claim_daily_checkin(self) -> Dict[str, Any]:
"""
执行每日签到核心业务逻辑:
1. 检查任务列表与签到状态
2. 若 Status == 2: 今日已完成,直接跳过
3. 若 Status == 1: 待领取,获取验证码并提交
"""
user_info = self.fetch_user_info()
user_display = f"{self.account_name} (ID: {user_info.get('ID') if user_info else '未知'})"
tasks = self.fetch_tasks()
checkin_task = None
for t in tasks:
if t.get("Name") == "每日签到":
checkin_task = t
break
if not checkin_task:
return {
"user": user_display,
"status": "error",
"message": "未在任务列表中找到【每日签到】任务",
"points_before": self.points_before,
"points_after": self.points_before,
}
task_status = checkin_task.get("Status")
# Status 2: 今日已领取
if task_status == 2:
logger.info(f"✨ 账号【{user_display}】今日已完成每日签到,当前总资产: {format_points(self.points_before)}。")
return {
"user": user_display,
"status": "already_done",
"message": "今日已签到 (无需重复领取)",
"points_before": self.points_before,
"points_after": self.points_before,
}
# Status 1: 待签到
logger.info(f"🎯 账号【{user_display}】今日尚未签到,开始启动签到流程...")
# 1. 求解验证码
ticket, randstr = self.captcha_mgr.get_ticket()
if not ticket or not randstr:
return {
"user": user_display,
"status": "fail",
"message": "验证码求解失败,签到中止",
"points_before": self.points_before,
"points_after": self.points_before,
}
# 2. 提交签到请求
self._update_common_headers()
payload = {
"task_name": "每日签到",
"verifyCode": "",
"vticket": ticket,
"vrandstr": randstr,
}
resp = self.session.post("/user/reward/tasks", json=payload)
res_json = {}
try:
res_json = resp.json()
except Exception:
pass
if resp.status_code == 200 and res_json.get("code") == 200:
reward_data = res_json.get("data", {})
earned_points = reward_data.get("points", 500)
earned_cny = earned_points / POINTS_PER_CNY
# 刷新最新积分
latest_info = self.fetch_user_info()
self.points_after = latest_info.get("Points", self.points_before + earned_points) if latest_info else self.points_before + earned_points
msg = f"签到成功!获得 +{earned_points} 积分 (≈ {earned_cny:.2f} 元)"
logger.info(f"🎉 账号【{user_display}】{msg},当前总资产: {format_points(self.points_after)}")
return {
"user": user_display,
"status": "success",
"message": msg,
"earned_points": earned_points,
"points_before": self.points_before,
"points_after": self.points_after,
}
else:
err_msg = res_json.get("message") or resp.text
logger.error(f"❌ 账号【{user_display}】签到失败: {err_msg}")
return {
"user": user_display,
"status": "fail",
"message": f"签到请求失败: {err_msg}",
"points_before": self.points_before,
"points_after": self.points_before,
}
def load_dotenv(env_path: Optional[Path] = None):
"""自动加载本地 .env 文件到 os.environ,方便本地直接调试运行"""
search_paths = [
env_path,
CURRENT_DIR / ".env",
Path.cwd() / ".env",
]
for p in search_paths:
if p and p.is_file():
try:
with open(p, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
k = k.strip()
v = v.strip()
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
v = v[1:-1]
if k not in os.environ or not os.environ[k]:
os.environ[k] = v
logger.info(f"✓ 成功加载本地环境变量: {p}")
break
except Exception as e:
logger.warning(f"读取 .env 异常: {e}")
def parse_accounts() -> List[Dict[str, str]]:
"""
解析雨云账号配置 (优先加载本地 .env):
仅支持 RAINYUN_ACCOUNT="账号#密码" 格式
多账号使用 & 分割 (青龙面板推荐) 或 换行分割 (本地 .env 推荐)
例如:
RAINYUN_ACCOUNT="13754650804#pwd1&13800000000#pwd2"
"""
load_dotenv()
accounts = []
acc_env = os.getenv("RAINYUN_ACCOUNT", "").strip()
if not acc_env:
return []
# 去除外层引号
if (acc_env.startswith('"') and acc_env.endswith('"')) or (acc_env.startswith("'") and acc_env.endswith("'")):
acc_env = acc_env[1:-1].strip()
# 按照 & 或 换行 分割多账号
raw_list = re.split(r"[&\n]", acc_env)
for item in raw_list:
item = item.strip()
if not item or "#" not in item:
continue
u, p = item.split("#", 1)
u, p = u.strip(), p.strip()
if u and p and not any(a.get("user") == u for a in accounts):
accounts.append({"user": u, "password": p})
return accounts
def main():
print("=" * 60)
print(" 🚀 雨云 (Rainyun) 自动登录与每日签到任务启动")
print("=" * 60)
accounts = parse_accounts()
if not accounts:
msg = (
"未检测到雨云账号配置!请在青龙环境变量或本地 .env 中配置 RAINYUN_ACCOUNT\n"
"格式: 账号#密码 (多账号用 & 或换行分割)\n"
"例如: RAINYUN_ACCOUNT=\"13754650804#pwd1&13800000000#pwd2\"\n"
"如需远程验证码服务,可添加 TCAPTCHA_API_URL (如 http://x.x.x.x:8000/solve)"
)
logger.warning(msg)
send("雨云每日签到异常", msg)
return
logger.info(f"已成功加载 {len(accounts)} 个雨云账号配置")
captcha_mgr = CaptchaManager()
results = []
for idx, acc in enumerate(accounts, 1):
logger.info(f"\n--- [正在处理账号 {idx}/{len(accounts)}] ---")
client = RainyunClient(
user=acc["user"],
password=acc["password"],
captcha_mgr=captcha_mgr,
)
login_ok = client.login()
if not login_ok:
results.append({
"user": client.account_name,
"status": "login_failed",
"message": "登录失败",
"points_before": 0,
"points_after": 0,
})
continue
res = client.claim_daily_checkin()
results.append(res)
# 多账号之间随机休眠 2~4 秒
if idx < len(accounts):
sleep_sec = random.uniform(2.0, 4.0)
time.sleep(sleep_sec)
# 统计汇总
print("\n" + "=" * 60)
print(" 📊 任务执行结果统计报表")
print("=" * 60)
success_cnt = sum(1 for r in results if r["status"] in ["success", "already_done"])
summary_lines = []
total_points = 0
for idx, r in enumerate(results, 1):
status_icon = "✅" if r["status"] in ["success", "already_done"] else "❌"
pts = r.get("points_after", 0)
total_points += pts
cny_str = format_points(pts)
line = f"[{idx}] {status_icon} 账号: {r['user']} | 结果: {r['message']} | 当前账户: {cny_str}"
summary_lines.append(line)
print(line)
total_cny = total_points / POINTS_PER_CNY
total_summary = f"💰 全部账号总资产: {total_points} 积分 (可提现 ≈ {total_cny:.2f} 元)"
summary_lines.append(total_summary)
print(total_summary)
print("=" * 60)
# 推送青龙系统通知
notify_title = f"雨云签到: 成功 {success_cnt}/{len(results)} | 总资产 ≈ {total_cny:.2f}元"
notify_content = "\n".join(summary_lines)
send(notify_title, notify_content)
if __name__ == "__main__":
main()
+3 -3
View File
@@ -23,9 +23,9 @@ from urllib.parse import unquote
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
from tcaptcha.telemetry import modules as tm # noqa: E402
from tcaptcha.telemetry.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402
from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402
BUILD = "/tmp/1ms_bridge/session_build.js"
SESS = "/tmp/1ms_bridge/session_collect.json"
+4 -4
View File
@@ -30,10 +30,10 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
import tdc_program as tp # noqa: E402
from tcaptcha.crypto import xtea_decrypt_bytes # noqa: E402
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
from tcaptcha.telemetry import modules as tm # noqa: E402
from tcaptcha.telemetry import program as tp # noqa: E402
from tcaptcha.telemetry.crypto import xtea_decrypt_bytes # noqa: E402
from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402
# 用于「这个模块到底像在采什么」的旁证关键词
HINTS = {
-75
View File
@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
"""全量配平:37 个语义名 <-> 真机明文的 38 个槽。
约束(这些是硬证据,不是猜):
1. 值内容唯一确定语义名(MacIntel/UTF-8/ANGLE/GgoA.../98k/iframe...)
2. screen_fingerprint `"1440-2560-2530-24-*-*-|-*"` 反解出
screen_height=1440 / screen_width=2560 / screen_color_depth=24
3. itoken 有 `^\\d+:\\d+$` 特征 -> 占 **2 槽**(`641547489,1791732001`)
4. 37 项 -> 38 槽(仅 itoken 多占 1 槽),可与明文槽数对账
输出: 每个语义名的取值 + 置信度,供人工核对。
"""
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
RAW = "/tmp/1ms_bridge/real_collect_plain.txt"
raw = open(RAW, encoding="latin1").read().rstrip("\x00")
cd = json.loads(re.sub(r" {2,}", "", raw))["cd"]
print("明文 38 槽:")
for i, v in enumerate(cd):
print(" %2d %s" % (i, json.dumps(v, ensure_ascii=False)[:78]))
print()
rows = tm.analyze("har/1ms_dianxuan/tdc.js")
names = [r["name"] for r in rows]
print("语义名 %d 个" % len(names))
print()
# ---- 按内容给每个槽打标签(不依赖位置)----
def tags(v):
t = []
if isinstance(v, str):
if "Mozilla" in v: t.append("user_agent")
if v == "MacIntel": t.append("platform")
if re.fullmatch(r"[+-]\d\d", v): t.append("timezone_offset")
if v.upper() == "UTF-8": t.append("charset")
if "ANGLE" in v: t.append("webgl_renderer")
if "Inc." in v: t.append("webgl_vendor")
if v.startswith("GgoA"): t.append("canvas_fingerprint")
if "rand=" in v and "captcha" in v: t.append("template_url")
if v == "98k": t.append("constant_98k")
if v == "iframe": t.append("iframe_detect")
if "*" in v and "-" in v: t.append("screen_fingerprint")
if v.startswith("http") and "1ms.run" in v: t.append("location_href")
elif isinstance(v, list):
if v and all(isinstance(x, list) and len(x) == 3 for x in v): t.append("trajectory")
if v and all(isinstance(x, str) for x in v): t.append("languages")
elif isinstance(v, int) and not isinstance(v, bool):
if 1_600_000_000 <= v <= 2_100_000_000: t.append("timestamp?")
elif v > 1_000_000: t.append("bigint?")
else: t.append("smallint?")
elif isinstance(v, str) and v == "":
t.append("emptystr?")
return t
print("逐槽标签:")
for i, v in enumerate(cd):
print(" %2d %-22s %s" % (i, ",".join(tags(v)), json.dumps(v, ensure_ascii=False)[:50]))
print()
# ---- 反解 screen_fingerprint 的约束 ----
fp = next((v for v in cd if isinstance(v, str) and "*" in v and "-" in v), None)
if fp:
p = fp.split("-")
print("screen_fingerprint = %r -> 高%s 宽%s 色深%s" % (fp, p[0], p[1], p[3]))
for target, want in (("screen_height", int(p[0])), ("screen_width", int(p[1])),
("screen_color_depth", int(p[3]))):
idxs = [i for i, v in enumerate(cd) if v == want]
print(" %-20s 期望 %s -> 明文里出现在槽 %s" % (target, want, idxs))
-138
View File
@@ -1,138 +0,0 @@
# -*- 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())
+3 -3
View File
@@ -24,8 +24,8 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import tcaptcha.client as client_mod # noqa: E402
from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
import tcaptcha.protocol.client as client_mod # noqa: E402
from tcaptcha.protocol.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
AID = "195672344"
ENTRY = "https://1ms.run/console/personal/benefits/check-in"
@@ -56,7 +56,7 @@ def main() -> int:
# 段几何(新链路才有;旧模板回填没有 segments)
if out.get("source") == "python_values":
try:
from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes
from tcaptcha.telemetry.crypto import split_collect_segments, xtea_decrypt_bytes
import base64
plain = xtea_decrypt_bytes(base64.b64decode(out["collect"]),
out["key"]).decode("latin1").rstrip("\x00")
-64
View File
@@ -1,64 +0,0 @@
# -*- coding: utf-8 -*-
"""验证 struct_40_9 的消歧特征是否跨构建稳定。
判别:
A 组: 闭包体最后写入 **空串** (PUSH_EMPTY_STR; SET_PROP)
B 组: 闭包体最后写入 **随机 int32** (PUSH_IMM <big>; SET_PROP)
"""
import glob
import sys
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
import tdc_program as tp # noqa: E402
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
def closure_body(p, entry):
"""取模块唯一闭包的 target 体。"""
cls = [c for c in p.closures if p.owner(c.pc) == entry]
if not cls:
return None
return p.function_code(cls[0].target)
def disamb(p, entry):
"""返回 'empty' | 'rand' | None"""
body = closure_body(p, entry)
if body is None:
return None
imms = [x["operands"][0] for x in body
if x["name"] == "PUSH_IMM" and x["operands"]]
big = [v for v in imms if v > 1_000_000]
if big:
return "rand"
return "empty"
stats = defaultdict(lambda: defaultdict(list))
for f in BATCH:
p = tp.load(f)
reg = dict(p.module_registry(0))
rows = tm.analyze(f)
for r in rows:
if r["name"] != "struct_40_9":
continue
d = disamb(p, reg[r["module"]])
stats[f.split("/")[-1]][d or "?"].append((r["item"], r["module"]))
print("%-24s %-8s %s" % ("构建", "判别", "项(模块)"))
print("-" * 70)
ok = True
for f, groups in stats.items():
for d, members in sorted(groups.items()):
print("%-24s %-8s %s" % (f, d, members))
# 每份构建必须 empty 1 个 + rand 1 个
if len(groups.get("empty", [])) != 1 or len(groups.get("rand", [])) != 1:
ok = False
print()
print(">>> 每份构建都是 empty×1 + rand×1 :", ok)
print(">>> 结论:", "可静态消歧(写入空串 vs 写入随机 int32)" if ok else "仍不可分")
-94
View File
@@ -1,94 +0,0 @@
# -*- coding: utf-8 -*-
"""对照实验:证明「按语义名配对」不是归一化过拟合出来的。
风险: 归一化若抹掉太多,任何配对都会"全对",校验就失去意义。
做法(负对照,同一套归一化 + 同一套比对):
① 按语义名配对(实验组)
② 按清单下标配对(负对照:槽位序随机,必然错)
③ 按模块 id 配对(负对照:id 每次重排,必然错)
④ 随机配对(负对照基线)
若①接近 37/37 而 ②③④ 明显更低 => 归一化没有过拟合,配对确实有效。
"""
import glob
import random
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
import tdc_program as tp # noqa: E402
# 带"代码地址"的操作数:每份构建的布局不同,必须抹掉
ADDR_OPS = {"JUMP", "JUMP_IF_TRUE", "JUMP_IF_FALSE", "PUSH_TRY"}
def norm_code(p, entry, maxlen=400):
"""归一化指令序列(与 verify_pair_correct2.py 同一套规则,此处内联避免导入副作用)。
抹掉三类**会重排**的操作数,保留指令骨架与字符串的位置:
1. 代码地址(JUMP / JUMP_IF_* / PUSH_TRY)
2. require(N) 的模块号 N
3. PUSH_STR_CHAR 的具体字符值(fp_salt 随机盐串、混淆变量名每次都变)
"""
code = p.function_code(entry)
out = []
for k, inst in enumerate(code):
nm, ops = inst["name"], tuple(inst["operands"] or [])
if nm in ADDR_OPS:
ops = ("ADDR",) * len(ops)
elif nm == "PUSH_STR_CHAR":
ops = ("CH",)
elif nm == "PUSH_IMM" and ops:
if (k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM"
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC"):
ops = ("MOD",) * len(ops)
out.append((nm, ops))
return tuple(out[:maxlen])
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
def load(f):
p = tp.load(f)
rows = tm.analyze(f)
reg = dict(p.module_registry(0))
sig = {r["name"]: norm_code(p, reg[r["module"]]) for r in rows}
return rows, sig
A, fa = load(BATCH[0])
print("基准 %s (归一化:抹掉地址/模块号/字符值,保留指令骨架与串的位置)" % BATCH[0].split("/")[-1])
print()
print("%-22s %-22s %-8s %-8s" % ("对比构建", "配对方式", "一致", "不一致"))
print("-" * 66)
for f in BATCH[1:]:
B, fb = load(f)
names = sorted(set(fa) & set(fb))
def score(pairs):
return sum(1 for a, b in pairs if a == b)
# ① 实验组:语义名
p1 = [(fa[n], fb[n]) for n in names]
# ② 负对照:清单下标
p2 = [(fa[A[i]["name"]], fb[B[i]["name"]]) for i in range(min(len(A), len(B)))]
# ③ 负对照:模块 id(用 id 当 key 去查另一边同名集合的任意一项)
fb_by_id = {r["module"]: fb[r["name"]] for r in B}
p3 = [(fa[A[i]["name"]], fb_by_id.get(A[i]["module"], ())) for i in range(len(A))]
# ④ 负对照:随机配对
rnd = random.Random(7)
shuf = names[:]
rnd.shuffle(shuf)
p4 = [(fa[n], fb[m]) for n, m in zip(names, shuf)]
for label, pairs in (("①语义名(实验组)", p1), ("②清单下标(负对照)", p2),
("③模块id(负对照)", p3), ("④随机配对(负对照)", p4)):
s = score(pairs)
print("%-22s %-22s %-8d %-8d" % (f.split("/")[-1], label, s, len(pairs) - s))
print()
print(">>> 判读: ①应≈37/37;②③④应明显更低 => 归一化未过拟合,配对有效")
-67
View File
@@ -1,67 +0,0 @@
# -*- coding: utf-8 -*-
"""验证字符串+结构指纹方案的**跨构建一致性** —— 只基于 har/online/ 全新样本。
案例 §5.3 的验收标准: "3 个 tdc.js 版本、37 个模块、全部正确识别,无一 unknown"。
本脚本再进一步:不仅要求"无 unknown",还要求
① 每份构建内部 37 项都有名字
② 各构建之间的**语义名集合**一致(说明识别的是同一批采集器)
③ 语义名 -> 每项在 cd 里的**相对位置**可交叉核对
"""
import glob
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
print("全新在线样本 %d 份(只使用这批数据)" % len(BATCH))
print()
results = {}
for f in BATCH:
rows = tm.analyze(f)
names = [r["name"] for r in rows]
results[f] = rows
unk = sum(1 for n in names if n == "unknown")
via_struct = sum(1 for r in rows if r.get("via_struct"))
via_deleg = sum(1 for r in rows if r.get("via_delegate"))
print(" %-24s 项%2d unknown %d | 字符串直判 %2d 委派链 %d 结构兜底 %d"
% (f.split("/")[-1], len(rows), unk,
len(rows) - via_struct - via_deleg, via_deleg, via_struct))
print()
keys = list(results)
base = Counter(r["name"] for r in results[keys[0]])
print("以 %s 为基准:%d 个语义名(去重 %d)"
% (keys[0].split("/")[-1], len(base), len(set(base))))
print()
for f in keys[1:]:
c = Counter(r["name"] for r in results[f])
same = sum((base & c).values())
only_base = sum((base - c).values())
only_it = sum((c - base).values())
print(" vs %-22s 相同 %2d | 仅基准有 %2d | 仅有它有 %2d"
% (f.split("/")[-1], same, only_base, only_it))
print()
common = set(base)
for f in keys[1:]:
common &= set(Counter(r["name"] for r in results[f]))
allnames = set()
for f in keys:
allnames |= set(r["name"] for r in results[f])
print(">>> 所有构建**共同**识别出的语义名: %d 个" % len(common))
print(">>> 出现过的语义名总数 : %d 个" % len(allnames))
print(">>> 差异名:", sorted(allnames - common) if allnames - common else "无(完全一致)")
print()
# 唯一性检查:单构建内是否有重名(重名 = 锚点有歧义)
print("单构建内重名检查(重名说明该锚点有歧义):")
for f in keys:
c = Counter(r["name"] for r in results[f])
dup = {k: v for k, v in c.items() if v > 1}
print(" %-24s 重名 %d 组 %s"
% (f.split("/")[-1], len(dup), dict(sorted(dup.items()))))
+4 -4
View File
@@ -26,11 +26,11 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
from tcaptcha.crypto import (build_collect, split_collect_segments, # noqa: E402
from tcaptcha.telemetry import modules as tm # noqa: E402
from tcaptcha.telemetry.crypto import (build_collect, split_collect_segments, # noqa: E402
xtea_decrypt_bytes)
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
from tcaptcha.tdc_values import Session, build_plain, generate_cd, trajectory_index # noqa: E402
from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402
from tcaptcha.telemetry.values import Session, build_plain, generate_cd, trajectory_index # noqa: E402
def main() -> int:
-94
View File
@@ -1,94 +0,0 @@
# -*- coding: utf-8 -*-
"""判决「按语义名配对」是否**正确** —— 用独立于指纹的方法。
⚠️ 上一轮我用「模块 id 相等」做校验,那个假设是**错的**:
案例 §1.2 与账本 #29 都明确说「37 个模块的 ID 每次构建随机重新分配」。
registry 全集交集 62/62 只说明编号**范围**相同(都是 0..61),
不代表 id -> 语义 的映射相同。id 不一致是**预期行为**,不能用来否定配对。
本轮改用真正独立的证据:**函数体的归一化指令序列**。
指纹规则只用到「字符串集合」和「结构统计(指令数/直方图)」,
没有用到「指令的完整顺序 + 操作数」。所以拿它做校验是独立的。
归一化: 抹掉 JUMP 的绝对地址(每构建不同),保留其余操作数。
"""
import glob
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools"))
import tdc_layout # noqa: E402
import tdc_modules as tm # noqa: E402
import tdc_program as tp # noqa: E402
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
# 带"代码地址"的操作数:每份构建的布局不同,必须抹掉
ADDR_OPS = {"JUMP", "JUMP_IF_TRUE", "JUMP_IF_FALSE", "PUSH_TRY"}
def norm_code(p, entry, maxlen=400):
"""归一化指令序列。
必须抹掉两类**会重排**的操作数,否则校验必然失败:
1. 代码地址:JUMP / JUMP_IF_TRUE / JUMP_IF_FALSE / PUSH_TRY
2. require(N) 的模块号 N:`LOAD_REG_IMM; PUSH_IMM N; CALL_FUNC`
(案例 §1.2:37 个模块的 ID 每次构建随机重新分配)
其余操作数(字符串 charCode、小常量、寄存器号)跨构建稳定,保留。
"""
code = p.function_code(entry)
out = []
for k, inst in enumerate(code):
nm, ops = inst["name"], tuple(inst["operands"] or [])
if nm in ADDR_OPS:
ops = ("ADDR",) * len(ops)
elif nm == "PUSH_STR_CHAR":
# 只保留"这是一个字符"这件事,**不看具体是哪个字符**。
# 这样 fp_salt 的随机盐串、混淆变量名这些「会变的内容」就不会干扰校验,
# 而"字符串的长度/位置"仍然参与比对 —— 足够独立,且仍然很强。
ops = ("CH",)
elif nm == "PUSH_IMM" and ops:
# require(N) 形状:前一个 LOAD_REG_IMM,后一个 CALL_FUNC
if (k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM"
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC"):
ops = ("MOD",) * len(ops)
out.append((nm, ops))
return tuple(out[:maxlen])
print("构建 : 每个模块的函数体归一化指令序列 -> 作为独立指纹")
print()
A = tm.analyze(BATCH[0])
pa = tp.load(BATCH[0])
ra = dict(pa.module_registry(0))
fa = {r["name"]: norm_code(pa, ra[r["module"]]) for r in A}
print("基准 %s (%d 项)" % (BATCH[0].split("/")[-1], len(A)))
print()
print("%-24s %-8s %-8s %-8s %s" % ("对比构建", "配对", "序列一致", "不一致", "判决"))
print("-" * 66)
all_ok = True
for f in BATCH[1:]:
B = tm.analyze(f)
pb = tp.load(f)
rb = dict(pb.module_registry(0))
fb = {r["name"]: norm_code(pb, rb[r["module"]]) for r in B}
common = set(fa) & set(fb)
same = [n for n in common if fa[n] == fb[n]]
diff = [n for n in common if fa[n] != fb[n]]
all_ok &= not diff
print("%-24s %-8d %-8d %-8d %s"
% (f.split("/")[-1], len(common), len(same), len(diff),
"✓" if not diff else "✗"))
for n in sorted(diff)[:3]:
a, b = fa[n], fb[n]
hit = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), -1)
print(" %-26s 首个分歧@%d A=%s B=%s" % (n, hit,
a[hit] if hit >= 0 else "-", b[hit] if hit >= 0 else "-"))
print()
print(">>> 全部配对的函数体序列一致:", all_ok)
print(">>> 判决:", "语义名配对**正确**(独立证据)" if all_ok else "配对有误,需检查")
-61
View File
@@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
"""用**全新在线样本**做端到端重排测试(不需要真机明文,自证)。
做法:
给每个语义名编一个**假值** value_i,按构建 A 的清单顺序排成 cd_A。
再用锚点把它重排到构建 B 的顺序,得到 cd_B。
正确答案 = 按 B 的清单顺序、取各自语义名的假值。
这个测试能抓住 reorder 的核心 bug(恒等映射):
若 B 的顺序 ≠ A 的顺序,恒等映射必然错;只有真对齐才对。
"""
import glob
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools"))
import tdc_collectgen as cg # noqa: E402
import tdc_modules as tm # noqa: E402
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
print("新样本 %d 份" % len(BATCH))
print()
A = BATCH[0]
rowsA = tm.analyze(A)
namesA = [r["name"] for r in rowsA]
seqA = [r["module"] for r in rowsA]
n = len(namesA)
print("基准 %s 项 %d 唯一名 %d" % (A.split("/")[-1], n, len(set(namesA))))
# 假值:每项一个可区分的值
fake = {nm: "V<%s>" % nm for nm in namesA}
cd_A = [fake[nm] for nm in namesA]
src = cg.spec_from_seq(seqA, [None] * n, namesA, sample_cd=cd_A)
print()
for f in BATCH[1:]:
rowsB = tm.analyze(f)
namesB = [r["name"] for r in rowsB]
seqB = [r["module"] for r in rowsB]
dst = cg.spec_from_seq(seqB, [None] * len(rowsB), namesB)
# 正确答案:按 B 的顺序取各自语义名的假值
truth = [fake[nm] for nm in namesB]
try:
out = cg.reorder(cd_A, dst, src)
ok = out == truth
identical = out == cd_A
print(" vs %-24s" % f.split("/")[-1])
print(" 重排结果 == 正确答案 : %s" % ok)
print(" 重排结果 == 原样(说明没重排): %s" % identical)
if not ok:
bad = [i for i, (a, b) in enumerate(zip(out, truth)) if a != b]
print(" 错位 %d 处, 前5: %s" % (len(bad), bad[:5]))
except Exception as e:
print(" vs %-24s %s: %s" % (f.split("/")[-1], type(e).__name__, str(e)[:110]))
print()
-75
View File
@@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
"""用真机 collect 密文的**长度与填充**判决「四段加密」结构。
不需要密钥:段结构在 base64 的长度与 '=' 分布上留下指纹。
判决依据(docs/tdc-案例.md §5.1):
chunk1/trajectory/chunk2 各 pad 到 24 字节 -> 密文长度是 24 的倍数 -> base64 无 '='
sd 不 pad,只被 XTEA 补到 8 的倍数 -> 长度%3 != 0 -> base64 带 '='
用法:
.venv/bin/python scripts/verify/verify_seglen.py <collect 的 base64>
"""
import base64
import re
import sys
from pathlib import Path
if len(sys.argv) < 2:
print("用法: verify_seglen.py <collect 的 base64(文件路径或直接给串)>")
raise SystemExit(2)
_arg = sys.argv[1]
_p = Path(_arg)
b64 = (_p.read_text(encoding="ascii") if _p.exists() else _arg).strip()
print("真机 collect: %d 字符, 含 '=' : %s" % (len(b64), "=" in b64))
print()
raw = base64.b64decode(b64)
print("解码后密文: %d 字节" % len(raw))
print()
# 真机明文(另一份同会话样本)给出的各段长度
segs = [("chunk1", 355, 5), ("trajectory", 62, 10), ("chunk2", 288, 24), ("sd", 33, None)]
print("%-11s %8s %6s %10s %12s %10s" % ("段", "内容", "pad", "密文字节", "base64字符", "含'='"))
print("-" * 66)
total = 0
total_b64 = 0
for name, content, pad in segs:
if pad is not None:
n = content + pad # 空格 pad 到 24
assert n % 24 == 0
else:
n = content + ((8 - content % 8) % 8) # sd: 只被 XTEA 补到 8 的倍数
enc = base64.b64encode(b"\x00" * n).decode()
total += n
total_b64 += len(enc)
print("%-11s %8d %6s %10d %12d %10s"
% (name, content, pad if pad is not None else "(xtea)",
n, len(enc), "=" in enc))
print("-" * 66)
print("%-11s %8s %6s %10d %12d" % ("合计", "", "", total, total_b64))
print()
print(">>> 推算总密文字节 %d == 实际 %d : %s" % (total, len(raw), total == len(raw)))
print(">>> 推算 base64 字符 %d == 实际 %d : %s" % (total_b64, len(b64), total_b64 == len(b64)))
print()
# 反过来看:把整串按 24 字节切,前三段应能整除
print("[反向校验] 把密文按 24 字节切分:")
off = 0
for name, content, pad in segs:
if pad is None:
print(" %-11s 剩余 %3d 字节 (sd, 非 24 倍数)" % (name, len(raw) - off))
break
n = content + pad
print(" %-11s [%4d:%4d] %3d 字节 24倍数=%s"
% (name, off, off + n, n, n % 24 == 0))
off += n
print()
# 关键指纹: '=' 只应出现在最后(来自 sd 段)
idx = b64.find("=")
print("'=' 首次出现位置: %d / 总长 %d -> %s"
% (idx, len(b64), "在末尾区域(符合 sd 段不 pad)"
if idx >= len(b64) - 4 else "⚠️ 在中间(与四段结构矛盾)"))
if idx >= 0:
print(" 尾部填充: %r" % b64[idx:])
-89
View File
@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
"""按 docs/tdc-案例.md §5.1 验证 collect 的「四段加密」结构(真机明文判决)。
要判决的问题:
项目现有 crypto.encrypt_string / tdc_collectgen.encrypt_plain 都是
「整体 JSON -> \x00 补齐到 8 -> 一次加密 -> 一个 base64」。
案例说「4 段独立加密,前 3 段空格 pad 到 24 字节对齐,sd 不 pad」。
两者只能有一个对 —— 用真机明文里的**空格填充串**来判。
用法:
.venv/bin/python scripts/verify/verify_segment.py <已解密的 collect 明文文件>
"""
import json
import re
import sys
if len(sys.argv) < 2:
print("用法: verify_segment.py <已解密的 collect 明文文件>")
print(" (明文来源: 用同会话 tdc.js 的密钥解密 TDC.getData(true) 的产出)")
raise SystemExit(2)
RAW = sys.argv[1]
data = open(RAW, encoding="latin1").read()
print("原始(含尾部 \\x00):", len(data), "字节")
stripped = data.rstrip("\x00")
print("去掉尾部 \\x00 后 :", len(stripped), "字节 (尾部 \\x00 个数 = %d)" % (len(data) - len(stripped)))
print()
runs = [(m.start(), m.end()) for m in re.finditer(r" {2,}", stripped)]
print("[1] 连续空格串(只能来自「空格 pad」):")
for a, b in runs:
print(" 位置 %3d..%3d 长度 %2d" % (a, b, b - a))
print()
# 段边界 = 每个空格串的结束位置(若空格串在段尾)
bounds = [0] + [b for _a, b in runs[:-1]] if len(runs) > 1 else [0]
# 更稳: 段边界取每个空格串的"起点所在段"的末尾 -> 直接用空格串终点
b1, b2, b3 = runs[0][1], runs[1][1], runs[2][1]
segs = [("chunk1", 0, b1), ("trajectory", b1, b2), ("chunk2", b2, b3), ("sd", b3, len(stripped))]
print("[2] 按空格串切出的 4 段:")
for name, a, b in segs:
s = stripped[a:b]
print(" %-11s [%3d:%3d] len=%3d 24倍数=%-5s 8倍数=%-5s"
% (name, a, b, len(s), len(s) % 24 == 0, len(s) % 8 == 0))
print()
print("[3] 空格 pad 公式校验 (pad_len = 24 - len%24,总是补 1..24 个):")
for name, a, b in segs[:3]:
s = stripped[a:b]
content = s.rstrip(" ")
pad = len(s) - len(content)
print(" %-11s 内容 %3d + pad %2d = %3d 公式给出 %2d -> %s"
% (name, len(content), pad, len(s), 24 - len(content) % 24,
"OK" if pad == 24 - len(content) % 24 else "FAIL"))
print()
print("[4] sd 段不 pad(只被 XTEA 的 8 字节块 \x00 补齐):")
sd = stripped[b3:]
print(" sd = %r" % sd)
print(" sd 长度 %d (非 8 的倍数 -> 加密时被补 %d 个 \\x00)"
% (len(sd), (8 - len(sd) % 8) % 8))
print(" 实际尾部 \\x00 个数 = %d" % (len(data) - len(stripped)))
print()
print("[5] 逻辑整体是否是合法 JSON,以及轨迹在 cd 里的位置:")
obj = json.loads(stripped)
cd = obj["cd"]
traj_idx = [i for i, v in enumerate(cd)
if isinstance(v, list) and v and all(isinstance(x, list) and len(x) == 3 for x in v)]
print(" cd 槽数 = %d" % len(cd))
print(" 轨迹槽下标 = %s" % traj_idx)
if traj_idx:
print(" 轨迹内容前 3 点 = %s" % cd[traj_idx[0]][:3])
print()
print("[6] 去 pad 后 4 段拼接 == 逻辑明文(应能解析成同一对象):")
rebuilt = "".join(stripped[a:b].rstrip(" ") for _name, a, b in segs[:3]) + stripped[b3:]
obj2 = json.loads(rebuilt)
print(" 可解析为 JSON :", True)
print(" 与原对象逐项相同 :", obj2 == obj)
print(" 重建长度 %d (原文去尾 \\x00 后 %d, 差 = pad 总量 %d)"
% (len(rebuilt), len(stripped), len(stripped) - len(rebuilt)))
print()
print("[7] 三段之间的连接处(验证 chunk1 以逗号结尾 / chunk2 以 '],' 结尾):")
print(" chunk1 内容末尾 : %r" % stripped[0:b1].rstrip(" ")[-24:])
print(" trajectory 内容 : %r" % stripped[b1:b2].rstrip(" ")[:40])
print(" chunk2 内容开头 : %r" % stripped[b2:b3].rstrip(" ")[:24])
print(" chunk2 内容末尾 : %r" % stripped[b2:b3].rstrip(" ")[-24:])
print(" sd 内容 : %r" % stripped[b3:])
+4 -4
View File
@@ -27,10 +27,10 @@ ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
from tcaptcha.tdc_values import Session, generate_cd # noqa: E402
from tcaptcha.telemetry import modules as tm # noqa: E402
from tcaptcha.telemetry.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402
from tcaptcha.telemetry.key import extract_xtea_key_pure # noqa: E402
from tcaptcha.telemetry.values import Session, generate_cd # noqa: E402
def slot_names(rows):