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:
@@ -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"
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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))
|
||||
@@ -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())
|
||||
@@ -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")
|
||||
|
||||
@@ -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 "仍不可分")
|
||||
@@ -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;②③④应明显更低 => 归一化未过拟合,配对有效")
|
||||
@@ -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()))))
|
||||
@@ -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:
|
||||
|
||||
@@ -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 "配对有误,需检查")
|
||||
@@ -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()
|
||||
@@ -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:])
|
||||
@@ -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:])
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user