sniff_live_v3.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. """sniff_live_v3 — 引导序列抓包(2026-09-26, 解锁路线①)。
  2. 监听 996盒子 Mac 客户端 与 游戏网关(8001-8099)的 TCP 流, 逐帧落盘 JSONL。
  3. 目标: 抓真客户端从出生到"能走动"的完整引导序列(1011/3199 等), 供按序重放。
  4. 相对 sniff_live_fixed.py 升级:
  5. 1. frame_hex: 48B encode6头+body 的原始字节 → 离线可字节级重放(旧版只有解析字段)
  6. 2. body_utf8: body 尽力 UTF-8 解码 → @yindao_chiyao/@chiyao_kaiqi 等引导串肉眼可读
  7. 3. ts: 绝对 epoch 时间戳(对齐客户端启动/登录操作), t: 相对秒
  8. 4. flow: 四元组保留 → 验证"移动帧独立通道"假设(3011 与 3031 是否同流)
  9. 用法(root):
  10. sudo protocol_bot/.venv/bin/python protocol_bot/docs/sniff_live_v3.py \
  11. --out /tmp/cap_guide.jsonl --duration 2400
  12. 行缓冲实时落盘, 被杀也不丢已收帧。
  13. """
  14. from __future__ import annotations
  15. import argparse
  16. import json
  17. import struct
  18. import time
  19. from pathlib import Path
  20. from typing import TYPE_CHECKING, Final
  21. from pbot.codec.bitbuf import decode6
  22. if TYPE_CHECKING:
  23. from collections.abc import Callable
  24. from typing import TextIO
  25. PORT_LO: Final[int] = 8001
  26. PORT_HI: Final[int] = 8099
  27. HDR_LEN: Final[int] = 48
  28. MAX_BODY: Final[int] = 300000
  29. def split_frames(
  30. buf: bytes,
  31. ) -> tuple[list[tuple[list[int], bytes]], bytes]:
  32. """从流缓冲切出完整帧: 返回 ([(9int头, body)...], 剩余字节)。
  33. 头 48B encode6 编码; blen 非法或帧不完整即停(等待更多数据)。
  34. """
  35. out: list[tuple[list[int], bytes]] = []
  36. pos = 0
  37. while len(buf) - pos >= HDR_LEN:
  38. try:
  39. h = list(struct.unpack("<9i", decode6(buf[pos : pos + HDR_LEN])))
  40. except Exception: # noqa: BLE001 非法头=流边界错位, 截断等待重同步
  41. break
  42. blen = h[7]
  43. if blen < 0 or blen > MAX_BODY or len(buf) - pos < HDR_LEN + blen:
  44. break
  45. out.append((h, buf[pos + HDR_LEN : pos + HDR_LEN + blen]))
  46. pos += HDR_LEN + blen
  47. return out, buf[pos:]
  48. def body_utf8(body: bytes) -> str:
  49. """尽力解码引导串; 不可解码部分以 . 替换。"""
  50. return body.decode("utf-8", errors="replace").replace("\x00", "")
  51. def make_handler(
  52. fh: TextIO,
  53. t0: float | None = None,
  54. ) -> tuple[Callable[[object], None], dict[str, int]]:
  55. """构造 scapy prn 回调。fh 为逐行落盘函数(行缓冲)。返回 (handler, 计数器)。"""
  56. flows: dict[tuple[str, int, str, int], bytes] = {}
  57. stats = {"frames": 0, "pkts": 0}
  58. start = t0 if t0 is not None else time.time()
  59. def on_pkt(pkt: object) -> None:
  60. tcp = getattr(pkt, "TCP", None)
  61. ip = getattr(pkt, "IP", None)
  62. raw = getattr(pkt, "Raw", None)
  63. if tcp is None or ip is None or raw is None:
  64. return
  65. payload = bytes(getattr(raw, "load", b""))
  66. if not payload:
  67. return
  68. sport = int(tcp.sport)
  69. dport = int(tcp.dport)
  70. if PORT_LO <= dport <= PORT_HI:
  71. d = "CS" # 客户端→服务器(服务器端口在 8001-8099)
  72. elif PORT_LO <= sport <= PORT_HI:
  73. d = "SC"
  74. else:
  75. return
  76. stats["pkts"] += 1
  77. src = str(ip.src)
  78. dst = str(ip.dst)
  79. key = (src, sport, dst, dport)
  80. buf = flows.get(key, b"") + payload
  81. frames, rest = split_frames(buf)
  82. now = time.time()
  83. flow = f"{src}:{sport}>{dst}:{dport}"
  84. for h, body in frames:
  85. rec = {
  86. "t": round(now - start, 2),
  87. "ts": round(now, 3),
  88. "dir": d,
  89. "flow": flow,
  90. "frame_hex": (
  91. # 原始 48B 头在 buf 中已消费, 从解析字段重编码等价;
  92. # 这里直接存 48B 头的 encode6 输出 + body, 保证可重放
  93. _reencode_header_hex(h) + body.hex()
  94. ),
  95. "pre": h[0],
  96. "msg": h[1],
  97. "recog": h[2],
  98. "p1": h[3],
  99. "p2": h[4],
  100. "p3": h[5],
  101. "p6": h[6],
  102. "blen": h[7],
  103. "tail": h[8],
  104. "body_hex": body.hex(),
  105. "body_utf8": body_utf8(body),
  106. }
  107. fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
  108. stats["frames"] += 1
  109. flows[key] = rest
  110. return on_pkt, stats
  111. def _reencode_header_hex(h: list[int]) -> str:
  112. from pbot.codec.bitbuf import encode6
  113. return encode6(struct.pack("<9i", *h)).hex()
  114. def main() -> None:
  115. """主入口: 解析参数, 启动 scapy 抓包循环(需 root)。"""
  116. ap = argparse.ArgumentParser()
  117. ap.add_argument("--out", default="/tmp/cap_guide.jsonl") # noqa: S108
  118. ap.add_argument("--duration", type=int, default=0, help="秒, 0=直到 Ctrl-C")
  119. a = ap.parse_args()
  120. from scapy.all import sniff # noqa: PLC0415 (ruff 版本差异, 保留防误报)
  121. fh = Path(a.out).open("w", buffering=1) # noqa: SIM115 行缓冲实时落盘
  122. handler, stats = make_handler(fh)
  123. print( # noqa: T201 抓包工具 CLI 输出
  124. f"[sniff_v3] ports {PORT_LO}-{PORT_HI} → {a.out} (实时写入)",
  125. flush=True,
  126. )
  127. try:
  128. sniff(
  129. filter=f"tcp portrange {PORT_LO}-{PORT_HI}",
  130. prn=handler,
  131. store=False,
  132. timeout=a.duration or None,
  133. )
  134. except KeyboardInterrupt:
  135. pass
  136. finally:
  137. fh.close()
  138. print(f"[done] {stats['frames']} frames / {stats['pkts']} pkts → {a.out}") # noqa: T201
  139. if __name__ == "__main__":
  140. main()