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
+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):