做的什么:
1. 判决并修正 collect 结构: 不是「整体一次加密」, 而是
chunk1 / trajectory / chunk2 / sd **四段独立加密**, 前 3 段
空格 pad 到 24 字节对齐、sd 不 pad, 4 段 base64 直接拼接。
2. 按案例 §5.3 实现字符串+结构指纹的模块识别器, 把 37 个采集器
全部识别且跨构建唯一。
3. 修 reorder 的恒等映射 bug, 并补强断言。
4. 在线抓取全新样本作为唯一数据源, 清理探索性脚本。
为什么:
* 旧「整体加密 + \x00 补齐」与真机数据矛盾 —— 导致多轮实验
建立在结构错误的 collect 上(这正是此前反复"乱碰"的根因)。
* 模块 id 每份构建重排, 不能当跨构建锚点; 但模块的**代码**
(字符串集合、结构)不变 —— 这是案例指明的不变量。
* reorder 旧实现用「目标槽下标」切「源数组」, 顺序不同即全盘错位;
原测试只断言"是置换", 恒等函数也满足, 所以 bug 被放过了。
怎么验证:
* 四段结构(3 条独立证据, 其中一条不需密钥):
- 真机明文 3 处连续空格串 [+5,+10,+24], 且 pad == 24-len%24 全中
- 密文长度算术 480+96+416+56 = 1048 == 真实 1048 字符
- '=' 只出现在 1046/1048, 对应 sd 段 33B→补 7→40B
* 指纹: 4 份全新在线构建 37/37 识别、0 unknown、跨构建语义名集合
完全一致、单构建内 0 重名
* 配对正确性: 独立证据 37/37 一致; 负对照(清单下标 / 模块 id /
随机配对)均只有 2-3/37 —— 证明归一化没有过拟合
* 端到端重排 3/3 正确且非恒等
* selftest 全绿; 强断言自查: 旧实现过不了新断言
新增:
tools/tdc_modules.py 字符串+结构指纹识别(案例 §5.3)
scripts/fetch_online_samples.py 在线抓取全新 tdc.js
scripts/verify/*.py 7 个可复现的验证脚本
docs/tdc-案例.md 案例原文(看雪, 作者执着的猫)
修改:
tcaptcha/crypto.py +build_collect / split_collect_segments(四段)
tools/tdc_collectgen.py reorder 改为跨构建锚点对齐 + spec_from_seq
tools/tdc_layout.py +static_fingerprint / opcode_histogram
tools/tdc_program.py 修符号求值插值类型崩溃
tcaptcha/tests/test_collectgen.py 补「非恒等」强断言 + 在线新样本端到端
tcaptcha/client.py debug 目录改到系统临时目录(不再写仓库内 scratch/)
har/1ms_dianxuan/PROGRESS.md 新增已知 #41~#49, 显式修正被推翻的旧结论
.gitignore +scratch/ +har/online/
删除:
scratch/(含 10-11 遗留的一次性脚本, 已确认不可恢复后清理)
注意:
* 账本记录了一次自我纠错: 曾据案例 §5.5 误判 kind=2「不进 cd」,
实测(cd 38 槽、轨迹在第 10 槽)否定后已撤回。
* 仍未打通: 纯离线生成需要每个语义名的取值来源(已知 #38)。
455 lines
20 KiB
Python
455 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
tdc_modules.py —— **按案例 §5.3 的字符串指纹识别 37 个采集器模块**。
|
||
|
||
> "从 v1 的全部错位到指纹方案的全部正确,关键不是更复杂的分析,
|
||
> 而是换了一个更好的问题 —— 不再问『这个模块返回什么值』,
|
||
> 而是问『这个模块的代码长什么样』。" —— docs/tdc-案例.md §5.3
|
||
|
||
为什么必须做这件事(2026-10-12 实测,推翻项目早期假设):
|
||
* **模块 id 每份构建重新分配**。两份构建清单交集的 28 个模块里,
|
||
`id -> 语义` 完全相同的只有 **1/11**(模块 8 在 A 是 `navigator.platform`,
|
||
在 B 是 canvas 指纹)。按 id 对齐必然错位。
|
||
* 但**模块代码里的字符串集合跨构建完全不变**:采集 userAgent 的模块
|
||
一定含 `'userAgent'`;采集时区的一定含 `'getTimezoneOffset'`。这就是不变量。
|
||
|
||
本模块做三件事:
|
||
1. `module_strings()` —— 取一个模块的**可达**字符串集合(含内部闭包,递归委派)
|
||
2. `resolve_delegate()` —— 解析**委派链**(见下)
|
||
3. `identify()` —— 用指纹规则表把模块判成语义名
|
||
|
||
委派链(本项目新发现,案例未提但实测必需):
|
||
清单里有 6 个模块**自身不采集任何东西**,身体只是 `exports.get = require(N)`:
|
||
项8 模块21 -> require(22)
|
||
项9 模块24 -> require(25)
|
||
项27 模块49 -> require(31)
|
||
项31 模块53 -> require(0)
|
||
它们的字符串集合只有 {'Array','Object','exports','get'} —— **零区分性**,
|
||
必须顺着委派链跟到真正的采集器才能识别。这 6 个就是案例 §5.2 说的
|
||
"6 个无 .get 模块"的来源之一。
|
||
|
||
用法:
|
||
from tdc_modules import analyze
|
||
info = analyze("har/1ms_dianxuan/tdc.js")
|
||
for row in info:
|
||
print(row["item"], row["module"], row["name"], row["strings"][:4])
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
import sys
|
||
from collections import Counter, defaultdict
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Sequence, Set, Tuple
|
||
|
||
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_layout # noqa: E402
|
||
import tdc_program as tp # noqa: E402
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 通用字符串(每个模块都有,无区分性,进指纹前必须剔除) #
|
||
# --------------------------------------------------------------------------- #
|
||
NOISE: Set[str] = {
|
||
"Array", "Object", "exports", "get", "Math", "RegExp", "String", "Number",
|
||
"JSON", "set", "reset", "on", "length", "slice", "prototype", "constructor",
|
||
"push", "apply", "call", "bind", "toString", "valueOf", "hasOwnProperty",
|
||
}
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 指纹规则表(案例 §5.3)—— 每个语义名对应一组「必须出现的字符串」 #
|
||
# --------------------------------------------------------------------------- #
|
||
# 形式: 语义名 -> (必需串集合, 权重提示)
|
||
# 判定: 模块的区分串集合 **包含** 某个规则的必需串 -> 命中该规则
|
||
# 形式: (语义名, 必需串, 禁用串)
|
||
# 判定: required ⊆ strings 且 forbidden ∩ strings == ∅
|
||
#
|
||
# ⚠️ 规则必须**按 specificity 从高到低排**(必需串多的在前)。
|
||
# 早期版本只按列表顺序取第一个命中,结果 webdriver_detect 被误判成 user_agent
|
||
# —— 两者都只要求 1 个串,而 user_agent 恰好排在前面。
|
||
# 给特征明显的规则补上更多必需串(如 webdriver 要求 2 个自动化标记),
|
||
# 它们就会天然压过 user_agent 这类单串规则。
|
||
#
|
||
# fp_salt 之所以需要 forbidden: 它和 iframe_detect 的串集合几乎相同
|
||
# (都是 appendChild/body/createElement/document),唯一区别是 iframe_detect
|
||
# 多一个 contentWindow。用禁用串把两者分开。
|
||
FINGERPRINTS: List[Tuple[str, Set[str], Set[str]]] = [
|
||
# ---- 复合指纹(多必需串,天然压过单串规则)----
|
||
("screen_fingerprint", {"screen", "width", "height", "availHeight", "colorDepth"}, set()),
|
||
("webrtc_ip", {"RTCPeerConnection", "createDataChannel", "createOffer"}, set()),
|
||
("webgl_renderer", {"UNMASKED_RENDERER_WEBGL", "WEBGL_debug_renderer_info"}, set()),
|
||
("webgl_vendor", {"UNMASKED_VENDOR_WEBGL", "WEBGL_debug_renderer_info"}, set()),
|
||
("webdriver_detect", {"$cdc_asdjflasutopfhvcZLmcfl_", "__webdriver_script_fn"}, set()),
|
||
("canvas_fingerprint", {"ClientJS,org <canvas> 1.0", "#069"}, set()),
|
||
("dom_compliance", {"::marker", "CSS"}, set()),
|
||
("template_url", {"?rand=*", "URL"}, set()),
|
||
("charset", {"characterSet", "charset"}, set()),
|
||
("viewport_size", {"clientWidth", "clientHeight"}, set()),
|
||
("time_since_load", {"getTime", "load"}, set()),
|
||
# ---- 单串规则(放最后,避免压过上面的复合规则)----
|
||
("user_agent", {"userAgent"}, set()),
|
||
("platform", {"platform"}, set()),
|
||
("languages", {"languages"}, set()),
|
||
("hardware_concurrency", {"hardwareConcurrency"}, set()),
|
||
("cookie_enabled", {"cookieEnabled"}, set()),
|
||
("timezone_offset", {"getTimezoneOffset"}, set()),
|
||
("itoken", {"TDC_itoken"}, set()),
|
||
("guid_token", {r"^\d+:\d+$"}, set()),
|
||
("referrer", {"TCaptchaReferrer"}, set()),
|
||
("location_href", {"href"}, {"?rand=*", "TCaptchaReferrer"}),
|
||
("iframe_detect", {"contentWindow"}, set()),
|
||
("console_detect", {"console"}, set()),
|
||
("touch_detect", {"ontouchstart"}, set()),
|
||
("window_time", {"getTime"}, set()),
|
||
("constant_98k", {"98k"}, set()),
|
||
("trajectory", {"stringify"}, set()),
|
||
("screen_width", {"screen", "width"}, set()),
|
||
("screen_height", {"screen", "height"}, set()),
|
||
("screen_color_depth", {"colorDepth"}, set()),
|
||
]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 结构兜底规则(案例 §5.3: "对于没有区分性字符串的通用模块,
|
||
# 用 .get 函数的指令数 + entry 函数的指令数 + 特定指令来区分")
|
||
# --------------------------------------------------------------------------- #
|
||
# 形式: (语义名, 判定函数) —— 只在**字符串规则全落空**时启用。
|
||
#
|
||
# ⚠️ 命名纪律:下面这些名字**只描述「观测到的结构分组」,不猜测业务语义**。
|
||
# 早期版本把 entry48/get9/call1 那组臆断成 "itoken_relay" —— 那是没有证据的猜测。
|
||
# 现在统一用 "struct_<entry>_<get>[_call]" 这种**纯结构标签**:
|
||
# 它诚实地表达「我们只知道它长这样,还不知道它采集什么」,
|
||
# 同时又因为跨构建稳定,仍然可以当**对齐锚点**用。
|
||
#
|
||
# 实测(4 份全新在线构建 har/online/20261012-01*,2026-10-12):
|
||
# entry135/get121/… -> 每份构建恰好 1 个 => fp_salt(随机串每次都变,只能靠结构)
|
||
# entry46/get9/… -> 每份构建恰好 1 个 => 稳定分组
|
||
# entry48/get9/call1 -> 每份构建恰好 1 个 => 稳定分组
|
||
# entry40/get9/… -> 每份构建 **2 个** => 组内无法再分,需运行时取样才能定
|
||
STRUCT_RULES = [
|
||
("fp_salt", lambda s: s["entry_instr"] >= 120 and s["get_instr"] >= 100),
|
||
("struct_46_9", lambda s: s["entry_instr"] == 46 and s["get_instr"] == 9),
|
||
("struct_48_9_call", lambda s: s["entry_instr"] == 48 and s["get_instr"] == 9
|
||
and s["call_func"]),
|
||
("struct_40_9", lambda s: s["entry_instr"] == 40 and s["get_instr"] == 9),
|
||
]
|
||
|
||
|
||
def struct_signature(p: tp.Program, reg: Dict[int, int], mid: int) -> Dict:
|
||
"""取一个模块的**结构签名**(案例 §5.3 兜底用)。
|
||
|
||
entry_instr = 模块工厂函数体的指令数
|
||
get_instr = `.get` 闭包(JUMP 目标)函数体的指令数
|
||
"""
|
||
entry = reg.get(mid)
|
||
if entry is None:
|
||
return {"entry_instr": -1, "get_instr": -1, "call_func": False,
|
||
"closure_kind": ""}
|
||
code = p.function_code(entry)
|
||
ops = Counter(inst["name"] for inst in code)
|
||
get_n = -1
|
||
for inst in code:
|
||
if inst["name"] == "JUMP" and inst["operands"]:
|
||
try:
|
||
get_n = len(p.function_code(inst["operands"][0]))
|
||
break
|
||
except Exception:
|
||
continue
|
||
return {
|
||
"entry_instr": len(code),
|
||
"get_instr": get_n,
|
||
"call_func": ops.get("CALL_FUNC", 0) > 0,
|
||
"closure_kind": _closure_kind(p, entry),
|
||
}
|
||
|
||
|
||
def _closure_kind(p: tp.Program, entry: int) -> str:
|
||
"""模块唯一闭包**最后写入什么** —— 用于拆开结构完全相同的模块。
|
||
|
||
背景(2026-10-12,4 份全新在线构建):
|
||
有 2 个模块 entry 都是 40 条、.get 都是 9 条、指令逐条相同,
|
||
只差一处 JUMP 绝对地址(每构建不同,不能当特征)。
|
||
真正的区别在**闭包体**(CREATE_CLOSURE 的 target,不是 JUMP 目标):
|
||
|
||
A: new Array()[0] = "" (PUSH_EMPTY_STR; SET_PROP)
|
||
B: new Array()[0] = 1558325408 (PUSH_IMM <随机int32>; SET_PROP)
|
||
|
||
实测每份构建恰好 empty×1 + rand×1 —— 稳定、可静态消歧。
|
||
|
||
返回: "empty" | "rand" | "const" | ""(无闭包)
|
||
"""
|
||
cls = [c for c in p.closures if p.owner(c.pc) == entry]
|
||
if not cls:
|
||
return ""
|
||
try:
|
||
body = p.function_code(cls[0].target)
|
||
except Exception:
|
||
return ""
|
||
imms = [inst["operands"][0] for inst in body
|
||
if inst["name"] == "PUSH_IMM" and inst["operands"]]
|
||
if any(v > 1_000_000 for v in imms):
|
||
return "rand"
|
||
if any(inst["name"] == "PUSH_EMPTY_STR" for inst in body):
|
||
return "empty"
|
||
return "const" if imms else ""
|
||
|
||
|
||
def _identify_struct(p: tp.Program, reg: Dict[int, int], mid: int) -> Optional[str]:
|
||
"""字符串规则落空时的兜底识别。"""
|
||
sig = struct_signature(p, reg, mid)
|
||
for name, pred in STRUCT_RULES:
|
||
try:
|
||
if pred(sig):
|
||
return name
|
||
except Exception:
|
||
continue
|
||
return None
|
||
|
||
|
||
def _clean(s: str) -> str:
|
||
return s.strip()
|
||
|
||
|
||
def module_strings(p: tp.Program, reg: Dict[int, int], mid: int,
|
||
depth: int = 3) -> Set[str]:
|
||
"""取一个模块的**可达**字符串集合(模块自身 + 内部闭包,递归 depth 层)。"""
|
||
entry = reg.get(mid)
|
||
if entry is None:
|
||
return set()
|
||
by_owner: Dict[int, Set[str]] = defaultdict(set)
|
||
for pc, s in p.strings:
|
||
by_owner[p.owner(pc)].add(s)
|
||
|
||
seen: Set[int] = set()
|
||
frontier = {entry}
|
||
out: Set[str] = set()
|
||
for _ in range(depth):
|
||
nxt: Set[int] = set()
|
||
for e in frontier:
|
||
if e in seen:
|
||
continue
|
||
seen.add(e)
|
||
out |= by_owner.get(e, set())
|
||
for c in p.closures:
|
||
if p.owner(c.pc) == e and c.target not in seen:
|
||
nxt.add(c.target)
|
||
frontier = nxt
|
||
if not frontier:
|
||
break
|
||
return out
|
||
|
||
|
||
def resolve_delegate(p: tp.Program, reg: Dict[int, int], mid: int,
|
||
max_hops: int = 6) -> List[int]:
|
||
"""解析模块 m 的委派链: `exports.X = require(N)` 里的 N。
|
||
|
||
返回 [m, N1, N2, ...](含自身)。取不到委派时返回 [m]。
|
||
|
||
形状(实测): 模块体里出现 `LOAD_REG_IMM; PUSH_IMM N; CALL_FUNC` 且 N 在
|
||
registry 里 -> 该模块委派给 N。有些模块 require 的是自己,要防环。
|
||
"""
|
||
chain: List[int] = [mid]
|
||
seen = {mid}
|
||
cur = mid
|
||
for _ in range(max_hops):
|
||
entry = reg.get(cur)
|
||
if entry is None:
|
||
break
|
||
code = p.function_code(entry)
|
||
nxt: Optional[int] = None
|
||
for k, inst in enumerate(code):
|
||
if inst["name"] != "PUSH_IMM" or not inst["operands"]:
|
||
continue
|
||
if k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM" \
|
||
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC":
|
||
cand = int(inst["operands"][0])
|
||
if cand in reg and cand not in seen:
|
||
nxt = cand
|
||
break
|
||
if nxt is None:
|
||
break
|
||
chain.append(nxt)
|
||
seen.add(nxt)
|
||
cur = nxt
|
||
return chain
|
||
|
||
|
||
def _has(strings: Set[str], token: str) -> bool:
|
||
"""判断 token 是否出现在字符串集合里。
|
||
|
||
两种模式:
|
||
* 普通 token -> **精确**相等
|
||
* `前缀*` 结尾 -> **子串/前缀**匹配
|
||
|
||
为什么需要后者(踩过的坑): `?rand=` 在真机里是 `?rand=1519713624347`
|
||
(带会话随机数),精确匹配永远命中不了,导致 template_url 被误判成
|
||
location_href。凡是"带运行时随机后缀"的串都必须用前缀匹配。
|
||
"""
|
||
if token in strings:
|
||
return True
|
||
if token.endswith("*"):
|
||
stem = token[:-1]
|
||
return any(stem in s for s in strings)
|
||
return False
|
||
|
||
|
||
def _matches(strings: Set[str]) -> List[Tuple[str, int, Set[str]]]:
|
||
"""列出所有命中的规则,按 specificity(必需串个数)降序。"""
|
||
hits: List[Tuple[str, int, Set[str]]] = []
|
||
for name, required, forbidden in FINGERPRINTS:
|
||
if not all(_has(strings, t) for t in required):
|
||
continue
|
||
if any(_has(strings, t) for t in forbidden):
|
||
continue
|
||
hits.append((name, len(required), required))
|
||
hits.sort(key=lambda x: -x[1])
|
||
return hits
|
||
|
||
|
||
def _identify(strings: Set[str]) -> Tuple[str, Set[str]]:
|
||
"""用指纹规则判定语义名。返回 (名字, 命中的必需串)。
|
||
|
||
取**必需串最多**的那条规则(specificity 优先),而不是列表里第一条 ——
|
||
这样 `webdriver_detect`(2 个必需串)不会被 `user_agent`(1 个)抢走。
|
||
"""
|
||
hits = _matches(strings)
|
||
if not hits:
|
||
return "unknown", set()
|
||
return hits[0][0], hits[0][2]
|
||
|
||
|
||
def disambiguate(rows: List[Dict]) -> None:
|
||
"""给**重名**的项加稳定后缀,使 37 个锚点尽可能唯一(原地修改 name)。
|
||
|
||
为什么需要这一步(2026-10-12 实测,只看 har/online/ 全新样本):
|
||
字符串规则会把不同采集器判成同一个名字。跨 4 份新构建统计,重名是**稳定的**:
|
||
|
||
user_agent x2 -> entry48(委派链) / entry68(直判) 每份各 1 个 ✓ 可消歧
|
||
window_time x3 -> entry279 / entry40 / entry48(委派链) 每份各 1 个 ✓ 可消歧
|
||
struct_40_9 x2 -> 结构完全相同 每份各 2 个 ✗ 静态不可分
|
||
|
||
消歧后缀用**结构签名**(entry/get 指令数 + 是否含 CALL_FUNC),因为它是
|
||
跨构建稳定的实测分组依据,而不是猜的语义。
|
||
|
||
⚠️ struct_40_9 那组每份构建固定 2 个、结构完全一致 —— 这是**静态识别的能力上限**。
|
||
要靠运行时取样(TDC.getData 对照)才能分开。本函数会如实保留重名,
|
||
并在 row["ambiguous_static"] 上标记,调用方据此决定是否需要运行时标定。
|
||
"""
|
||
groups: Dict[str, List[Dict]] = defaultdict(list)
|
||
for r in rows:
|
||
groups[r["name"]].append(r)
|
||
|
||
for name, members in groups.items():
|
||
if len(members) <= 1:
|
||
continue
|
||
for r in members:
|
||
s = r.get("struct") or {}
|
||
tag = "e%s_g%s%s%s" % (s.get("entry_instr", "-"),
|
||
s.get("get_instr", "-"),
|
||
"_call" if s.get("call_func") else "",
|
||
# 结构完全相同时,用"闭包最后写入什么"再拆一层
|
||
("_" + s["closure_kind"]) if s.get("closure_kind") else "")
|
||
# 同组内的 tag 是否唯一?唯一才能消歧
|
||
r["_tag"] = tag
|
||
tags = [r["_tag"] for r in members]
|
||
can_split = len(set(tags)) == len(tags)
|
||
for r in members:
|
||
r["ambiguous_static"] = not can_split
|
||
r["name_unique"] = "%s#%s" % (name, r["_tag"]) if can_split else name
|
||
if can_split:
|
||
for r in members:
|
||
r["name"] = r["name_unique"]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 主入口 #
|
||
# --------------------------------------------------------------------------- #
|
||
def analyze_prog(p: tp.Program, depth: int = 3) -> List[Dict]:
|
||
"""分析一个**已加载**的 Program(供调用方复用,避免重复解析 tdc.js)。"""
|
||
reg = dict(p.module_registry(0))
|
||
_mid, _entry, seq = tdc_layout.find_manifest_module(p)
|
||
|
||
rows: List[Dict] = []
|
||
for i, m in enumerate(seq):
|
||
own = module_strings(p, reg, m, depth=depth)
|
||
chain = resolve_delegate(p, reg, m)
|
||
# 顺着委派链往下找,直到能识别出语义(或链走完)
|
||
name, hit = _identify(own - NOISE)
|
||
used_chain = False
|
||
if name == "unknown" and len(chain) > 1:
|
||
for deeper in chain[1:]:
|
||
s = module_strings(p, reg, deeper, depth=depth)
|
||
n2, h2 = _identify(s - NOISE)
|
||
if n2 != "unknown":
|
||
name, hit = n2, h2
|
||
used_chain = True
|
||
break
|
||
# 字符串规则全落空 -> 启用案例 §5.3 的结构兜底(按指令数识别)
|
||
via_struct = False
|
||
if name == "unknown":
|
||
n3 = _identify_struct(p, reg, m)
|
||
if n3:
|
||
name, hit = n3, set()
|
||
via_struct = True
|
||
sig = sorted((own - NOISE))
|
||
rows.append({
|
||
"item": i,
|
||
"module": m,
|
||
"name": name,
|
||
"hit": sorted(hit),
|
||
"delegate": chain if len(chain) > 1 else [],
|
||
"via_delegate": used_chain,
|
||
"via_struct": via_struct,
|
||
"struct": struct_signature(p, reg, m),
|
||
"ambiguous_static": False,
|
||
"strings": sig,
|
||
})
|
||
disambiguate(rows)
|
||
return rows
|
||
|
||
|
||
def analyze(path: str, depth: int = 3) -> List[Dict]:
|
||
"""分析一份 tdc.js,返回清单每一项的识别结果(按清单顺序)。"""
|
||
return analyze_prog(tp.load(path), depth=depth)
|
||
|
||
|
||
def main() -> int:
|
||
import argparse
|
||
import json
|
||
|
||
ap = argparse.ArgumentParser(description="按字符串指纹识别 37 个采集器模块")
|
||
ap.add_argument("path")
|
||
ap.add_argument("--json", default=None, help="把结果写成 JSON")
|
||
ap.add_argument("--only-unknown", action="store_true", help="只列出未识别的")
|
||
args = ap.parse_args()
|
||
|
||
rows = analyze(args.path)
|
||
unknown = [r for r in rows if r["name"] == "unknown"]
|
||
print("文件 : %s" % args.path)
|
||
print("清单项 : %d 已识别 %d 未识别 %d"
|
||
% (len(rows), len(rows) - len(unknown), len(unknown)))
|
||
print()
|
||
print("项 模块 语义名 委派链 区分串")
|
||
print("-" * 108)
|
||
for r in rows:
|
||
if args.only_unknown and r["name"] != "unknown":
|
||
continue
|
||
dele = "->".join(str(x) for x in r["delegate"]) if r["delegate"] else ""
|
||
mark = " *" if r["via_delegate"] else " "
|
||
print("%2d %-5d %s%-21s %-17s %s"
|
||
% (r["item"], r["module"], mark, r["name"], dele,
|
||
r["strings"][:5]))
|
||
if args.json:
|
||
Path(args.json).write_text(
|
||
json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
|
||
print("\n结果 -> %s" % args.json)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|