cq2_pure_enter.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. # -*- coding: utf-8 -*-
  2. """
  3. cq2_pure_enter.py —— 纯协议全链路: 登录 → 创角/选角 → 进游戏 (无游戏客户端)
  4. ==========================================================================
  5. 2026-09-19 抓包逆向实证 (z52full.pcap 等, 38区/52区双样本验证):
  6. HTTP: 登录(极验滑块)→JWT → EnterGameClient(zone) → 平台记录
  7. 登录服(端口因区而异, 38区=8041, 52区=8001):
  8. CS2001 body=uid/encode6(jwt)/encode6(channel)/1/0 [pre字段随机, tail=0]
  9. → SC529 body=encode6(key32/...) 会话密钥
  10. CS104 body=encode6(key32) [tail=1] → SC530(sid在pre字段)
  11. CS100 body=uid/sid/zone/channel/gameid/key32 [tail=2] → SC520 角色列表JSON
  12. CS101 body={"server_name":zone,"chr":"名","secsdk":"0","job":0,"sex":0,
  13. "account":uid,"sdkid":channel} 创角(角色已存在→SC522)
  14. → SC521 body="角色名/UserID" 进区票据(UserID=zone+00+ts+0000+rand)
  15. CS103 body="uid/角色名" [p3=sid] 选角
  16. → SC525 body="127.0.0.1/游戏端口" 游戏网关端口
  17. 游戏服(登录服端口+2, 38区=8043, 52区=8003):
  18. CS20 body="*#uid/名/UserID/sid/1/0/channel/0/1/1/1/1"
  19. 头=(pre=900000, id=20, recog=随机, p1=zone, p2=游戏端口, p3=sid)
  20. ★ 流首直接encode6, 无0x00前缀(38区样本的0x00是TCP粘包残渣)
  21. → SC游戏数据(zlib) + 心跳CS60000(recog=unix时间戳, p3=sid, 2秒间隔)持续应答
  22. 用法:
  23. .venv-mac/bin/python cq2_pure_enter.py --zone 14459329 [--name 名字] [--wait 60] [--auto-heal]
  24. """
  25. import argparse
  26. import json
  27. import random
  28. import socket
  29. import struct
  30. import sys
  31. import time
  32. import threading
  33. import os
  34. import zlib
  35. import requests as rq
  36. import urllib3
  37. urllib3.disable_warnings()
  38. HERE = os.path.dirname(os.path.abspath(__file__))
  39. sys.path.insert(0, HERE)
  40. from encode6bitbuf_final import encode6, decode6 # noqa: E402
  41. import login_996pc as L # noqa: E402
  42. GAME_ID = 206923
  43. GW_HOST = "s{zone}-jycq-206923.dhsf.xqhuyu.com"
  44. CHANNEL = "110585"
  45. UID = 2001849239 # 默认值; 实际以登录响应 userInfo.id 为准(login_gate 的 uid 参数)
  46. LOGIN_PORT = 8001
  47. def box_auth_header(method, path, body, tok=""):
  48. import hashlib, hmac, string
  49. nonce = "".join(random.choice(string.digits + string.ascii_uppercase) for _ in range(32))
  50. ts = str(int(time.time() * 1000))
  51. canonical = "\n".join([method, path, ts, nonce, "pc", L.DEVICE_ID,
  52. "5.1.1.9007", tok, L.UA]) + "\n" + body + "\n"
  53. sig = hmac.new(b"BB0AAAC906FC4B7CADB116A213D46689",
  54. canonical.encode(), hashlib.sha256).hexdigest()
  55. return f"BOXAPP-SHA256 NONCE={nonce};TIMESTAMP={ts};SIGNATURE={sig};"
  56. def enter_game(session, jwt, zone_id):
  57. device_info = json.dumps({"type": "pc", "model": "pc", "os": "Win10",
  58. "width": 1920, "height": 1032, "lang": "zh-CN",
  59. "id": "43b2d8df251be893bf57a67560b1e74e",
  60. "net_type": "unknown", "mac": L.DEVICE_ID,
  61. "ua": L.UA}, separators=(",", ":"))
  62. body = json.dumps({"box_game_id": GAME_ID, "serverid": zone_id,
  63. "device_info": device_info, "appid": "GAjWNTqNpOVx"},
  64. separators=(",", ":"))
  65. h = L.box_headers(jwt)
  66. h["Content-Type"] = "application/json;charset=UTF-8"
  67. h["X-BOX-AUTHORIZATION"] = box_auth_header(
  68. "POST", "/v1/homeapi/gameindex/EnterGameClient", body, jwt)
  69. r = session.post("https://pc-api.996hezi.com/v1/homeapi/gameindex/EnterGameClient",
  70. data=body.encode(), headers=h, timeout=25, verify=False)
  71. return r.json()
  72. def recv_frames(sock, timeout=5):
  73. """收满 timeout 秒(墙钟)或连接断开为止, 解析出全部帧。"""
  74. deadline = time.time() + timeout
  75. buf = b""
  76. while time.time() < deadline:
  77. sock.settimeout(max(0.2, deadline - time.time()))
  78. try:
  79. d = sock.recv(65536)
  80. if not d:
  81. break
  82. buf += d
  83. except (socket.timeout, ConnectionResetError, OSError):
  84. break
  85. out, pos = [], 0
  86. while pos + 48 <= len(buf):
  87. try:
  88. m = struct.unpack("<9i", decode6(buf[pos:pos + 48]))
  89. except Exception:
  90. break
  91. out.append((m, buf[pos + 48:pos + 48 + m[7]]))
  92. pos += 48 + m[7]
  93. return out
  94. def gateway_ports(zone_id, login_port):
  95. return login_port, login_port + 2
  96. def login_gate(zone_id, jwt, name, login_port, uid):
  97. g = socket.create_connection((GW_HOST.format(zone=zone_id), login_port), timeout=10)
  98. tail = 0
  99. def send(mid, recog, p1, p2, p3, body, t=None):
  100. nonlocal tail
  101. if t is None:
  102. tail += 1
  103. t = tail
  104. g.sendall(encode6(struct.pack("<9i", 0, mid, recog, p1, p2, p3,
  105. 0, len(body), t)) + body)
  106. body2001 = (f"{uid}/".encode() + encode6(jwt.encode()) + b"/" +
  107. encode6(CHANNEL.encode()) + b"/1/0")
  108. send(2001, random.randint(1, 2**30), zone_id, login_port, 0, body2001, t=0)
  109. key32 = sid = None
  110. for m, b in recv_frames(g, 6):
  111. if m[1] == 529:
  112. key32 = decode6(b).split(b"/")[0]
  113. if not key32:
  114. g.close()
  115. raise SystemExit("[!] 登录服未下发 529 key")
  116. send(104, 0, 0, 0, 0, encode6(key32))
  117. for m, b in recv_frames(g, 4):
  118. if m[1] == 530:
  119. sid = m[0]
  120. body100 = f"{uid}/{sid}/{zone_id}/{CHANNEL}/{GAME_ID}/".encode() + key32
  121. send(100, random.randint(1, 2**30), zone_id, login_port, sid, body100)
  122. roles = None
  123. for m, b in recv_frames(g, 6):
  124. if m[1] == 520 and b:
  125. try:
  126. roles = json.loads(decode6(b) if all(0x3C <= c <= 0x7B for c in b[:200]) else b)
  127. except Exception:
  128. roles = None
  129. print(f"[*] 角色列表: {json.dumps(roles, ensure_ascii=False)[:200] if roles else None}")
  130. chr_info = next((c for c in (roles or {}).get("list", []) if c["chr"] == name), None)
  131. if chr_info is None:
  132. # 真客户端创角前先发 107(要名)拿 539 确认(cap_hoe_success 2026-10-01 t=21.0-21.1),
  133. # 跳过则服务端对 101 静默(删角后重创实测)
  134. send(107, 0, 0, 0, sid, name.encode())
  135. for m, b in recv_frames(g, 4):
  136. if m[1] == 539:
  137. print(f"[*] 107→539 要名确认: {b[:32]}")
  138. body101 = json.dumps({"server_name": zone_id, "chr": name, "secsdk": "0",
  139. "job": 0, "sex": 0, "account": uid,
  140. "sdkid": CHANNEL}, ensure_ascii=False).encode()
  141. send(101, 0, 0, 0, sid, body101)
  142. ticket = None
  143. for m, b in recv_frames(g, 6):
  144. if m[1] == 521 and b:
  145. try:
  146. ticket = decode6(b).decode("utf-8", "replace")
  147. except Exception:
  148. ticket = b.decode("utf-8", "replace")
  149. if m[1] == 522:
  150. print("[!] 522: 角色名已存在或槽位已满")
  151. print(f"[*] 101创角 → 521票据: {ticket}")
  152. if ticket:
  153. chr_info = {"chr": name, "UserID": ticket.split("/", 1)[1]}
  154. if chr_info is None:
  155. lst = (roles or {}).get("list") or []
  156. if not lst:
  157. g.close()
  158. raise SystemExit("[!] 无角色且创角失败")
  159. chr_info = lst[-1]
  160. name = chr_info["chr"]
  161. ticket_key = chr_info["UserID"]
  162. send(103, 0, 0, 0, sid, f"{uid}/{name}".encode())
  163. game_port = None
  164. for m, b in recv_frames(g, 6):
  165. if m[1] == 525 and b:
  166. try:
  167. gw = decode6(b).decode("utf-8", "replace")
  168. game_port = int(gw.split("/")[-1])
  169. except Exception:
  170. pass
  171. g.close()
  172. _, default_game = gateway_ports(zone_id, login_port)
  173. return key32, sid, name, ticket_key, game_port or default_game
  174. def enter_game_gate(zone_id, name, ticket_key, sid, game_port, uid):
  175. g = socket.create_connection((GW_HOST.format(zone=zone_id), game_port), timeout=10)
  176. body20 = f"*#{uid}/{name}/{ticket_key}/{sid}/1/0/{CHANNEL}/0/1/1/1/1".encode()
  177. head = struct.pack("<9i", 900000, 20, random.randint(1, 2**30),
  178. zone_id, game_port, sid, 0, len(body20), 0)
  179. g.sendall(encode6(head) + body20)
  180. return g
  181. def heartbeat_loop(sock, sid, lock=None, sender=None):
  182. """CS60000 心跳。tail 必须与主发送序号共用一条递增链
  183. (2026-09-23 抓包实证: 真客户端 init=tail1-18, 心跳=tail19+ 全序连续;
  184. 独立计数器造成 tail 冲突 → 服务器判定会话非法 → SC28 拒绝一切移动)。
  185. sender=bot 时经 bot.send 走全局唯一 tail; 否则退回独立计数(旧危险行为)。"""
  186. state = {"n": 0}
  187. def hb():
  188. time.sleep(2)
  189. fails = 0
  190. while True:
  191. try:
  192. if sender is not None:
  193. sender(60000, int(time.time()), 0, 0, sid, b"")
  194. else:
  195. state["n"] += 1
  196. h = struct.pack("<9i", 0, 60000, int(time.time()), 0, 0,
  197. sid, 0, 0, state["n"])
  198. wire = encode6(h)
  199. if lock is not None:
  200. with lock:
  201. sock.sendall(wire)
  202. else:
  203. sock.sendall(wire)
  204. fails = 0
  205. except Exception:
  206. fails += 1
  207. if fails >= 3:
  208. return
  209. time.sleep(2)
  210. threading.Thread(target=hb, daemon=True).start()
  211. CFG_1018 = ('{"cfg_unbindlist":"e806a0f429dbba2caf91ce14becfa824","packageName":"com.mac.BoxMacOS.xq.game",'
  212. '"gameid":206923,"cfg_callmobset":"d41d8cd98f00b204e9800998ecf8427e",'
  213. '"cfg_suit":"e46446bb18ccaf568d906595dd5eea51156fae9eb97a06c4add808c9bb7a621e",'
  214. '"cfg_stditem":"b8a98d345b8929f7ab959b5f05156c11b92ebc3e1c67a7eb298d6f0f4da276f1",'
  215. '"cfg_cusabilmapping":"e2a78dceea819be6821b60b1b9a8a742",'
  216. '"channelId":"110585","tripartite":0}')
  217. TPL_DIR = os.path.join(HERE, "docs")
  218. def _load_ui_templates():
  219. path = os.path.join(TPL_DIR, "ui60_初始化模板.json")
  220. try:
  221. with open(path) as f:
  222. return json.load(f)
  223. except OSError:
  224. return None
  225. DEV_FINGERPRINT_5026 = (b'{"model":"Mac16,10","type":"mac","os":"26.6.2","width":"1230",'
  226. b'"id":"FCE92589-6485-5AB5-A8D5-46E7CDC20BC3",'
  227. b'"boxid":"206923:110585","height":"760"}')
  228. CHANNEL_5028 = b'{"sdkver":"1.0.0","channel":"996box_mac","appid":"ykOL4T4d6Oav","appver":"4.1.0"}'
  229. def send_init_sequence(sock, sid, uid):
  230. """真实客户端进游戏后的19帧初始化序列(完整资格版, 手册§十四/P4修正).
  231. 要点: 145B设备指纹5026 + 996box_mac渠道5028 + 415B功能位图5277,
  232. 缺任何一项服务器对UI交互静默(60区56613真UI对照实证)."""
  233. tpls = _load_ui_templates()
  234. body_5277 = (tpls["5277"].encode() if tpls and tpls.get("5277")
  235. else None)
  236. frames = [
  237. (1018, CFG_1018),
  238. (1046, None), (1046, None), (1046, None), (1046, None),
  239. (2020, f"{uid}/3.40.4087/3.40.9112/{uid}/0/1001/110585/0/0"),
  240. (5026, None, "fingerprint"),
  241. (5028, CHANNEL_5028.decode()),
  242. (5027, "3"),
  243. (5260, None), (1078, None), (1079, None), (5148, None),
  244. (5277, None, "bitmap"),
  245. (1103, None), (1151, None), (6209, None), (6208, None),
  246. (5027, "3"),
  247. ]
  248. t = 2
  249. for item in frames:
  250. mid, bd = item[0], item[1]
  251. kind = item[2] if len(item) > 2 else None
  252. if kind == "fingerprint":
  253. body = DEV_FINGERPRINT_5026
  254. elif kind == "bitmap":
  255. body = body_5277
  256. else:
  257. body = bd.encode() if bd else b""
  258. p1_1018, p2_1018 = (41878640, 98395) if mid == 1018 else (0, 0)
  259. sock.sendall(encode6(struct.pack("<9i", 0, mid, 0, p1_1018, p2_1018, sid,
  260. 0, len(body), t)) + body)
  261. t += 1
  262. time.sleep(0.15)
  263. deadline = time.time() + 2
  264. while time.time() < deadline:
  265. try:
  266. sock.settimeout(max(0.2, deadline - time.time()))
  267. d = sock.recv(65536)
  268. if not d:
  269. break
  270. except (socket.timeout, OSError):
  271. break
  272. def game_action(sock, sid, act, index, tail_seq, wait=5.0):
  273. """发送 CS3199 UI动作并等待响应, 返回(喝药开关状态 or None, 新tail序号)。"""
  274. body = ('{"index":%d,"act":"%s","subid":1}' % (index, act)).encode()
  275. sock.sendall(encode6(struct.pack("<9i", 0, 3199, 0, 0, 0, sid,
  276. 0, len(body), tail_seq)) + body)
  277. t0 = time.time()
  278. state = None
  279. while time.time() - t0 < wait:
  280. try:
  281. sock.settimeout(2)
  282. d = sock.recv(65536)
  283. except (socket.timeout, OSError):
  284. continue
  285. if not d:
  286. break
  287. off = 0
  288. while off < len(d) - 47:
  289. try:
  290. h = struct.unpack("<9i", decode6(d[off:off + 48]))
  291. except Exception:
  292. off += 1
  293. continue
  294. mid, blen = h[1], h[7]
  295. if blen < 0 or blen > 100000:
  296. off += 1
  297. continue
  298. body = d[off + 48:off + 48 + blen]
  299. if mid != 60000 and body[:2] == b"x\x9c":
  300. try:
  301. dec = zlib.decompress(body)
  302. blobs = [dec]
  303. if all(0x3C <= c <= 0x7B for c in dec[:60]):
  304. try:
  305. blobs.append(decode6(dec))
  306. except Exception:
  307. pass
  308. for blob in blobs:
  309. if b"yd_cy1" in blob:
  310. import re
  311. m2 = re.search(rb"id=yd_cy1[^}]*?but_[01]\.png", blob, re.S)
  312. if m2:
  313. state = "on" if b"but_1" in m2.group(0) else "off"
  314. break
  315. except Exception:
  316. pass
  317. off += 48 + blen
  318. return state
  319. def enable_auto_heal(sock, sid, want_on=True):
  320. """开启喝药保护。返回最终状态 'on'/'off'/None。
  321. 开关是 toggle: 先读当前状态, 不符则切换一次。"""
  322. game_action(sock, sid, "@openmianban,23", 109, 900)
  323. time.sleep(1)
  324. state = game_action(sock, sid, "@chiyao_kaiqi", 303, 901, wait=1.5)
  325. if state is None:
  326. # 面板未刷新过, 无法得知当前态: 假设为 off, 切换一次
  327. state = game_action(sock, sid, "@chiyao_kaiqi", 303, 902)
  328. if want_on and state == "off":
  329. state = game_action(sock, sid, "@chiyao_kaiqi", 303, 903)
  330. if not want_on and state == "on":
  331. state = game_action(sock, sid, "@chiyao_kaiqi", 303, 903)
  332. return state
  333. def main():
  334. ap = argparse.ArgumentParser()
  335. ap.add_argument("-u", "--user", default="z1098210167")
  336. ap.add_argument("-p", "--password", default="yang535006")
  337. ap.add_argument("--zone", type=int, default=14459329,
  338. help="霸者52区=14459329 霸者38区=14448583")
  339. ap.add_argument("--port", type=int, default=LOGIN_PORT,
  340. help="登录服端口(38区8041, 52区8001, 默认8001)")
  341. ap.add_argument("--name", default="泪无期")
  342. ap.add_argument("--wait", type=int, default=60)
  343. ap.add_argument("--auto-heal", action="store_true",
  344. help="进游戏后发送初始化序列并开启喝药保护")
  345. args = ap.parse_args()
  346. print(f"[*] 登录 {args.user} ...")
  347. s = rq.Session()
  348. j, _ = L.do_login(s, args.user, args.password)
  349. if j.get("code") != 200:
  350. raise SystemExit(f"[!] 登录失败: {j.get('msg')}")
  351. jwt = j["data"]["token"]
  352. uid = (j["data"].get("userInfo") or {}).get("id") or UID
  353. j2 = enter_game(s, jwt, args.zone)
  354. print(f"[*] EnterGameClient(zone={args.zone}): code={j2.get('code')}")
  355. key32, sid, name, ticket_key, game_port = login_gate(
  356. args.zone, jwt, args.name, args.port, uid)
  357. print(f"[✓] key32={key32.decode(errors='replace')}")
  358. print(f"[✓] sid={sid} uid={uid} 角色={name}")
  359. print(f"[✓] 票据key={ticket_key}")
  360. print(f"[✓] 游戏服端口={game_port}")
  361. g = enter_game_gate(args.zone, name, ticket_key, sid, game_port, uid)
  362. heartbeat_loop(g, sid)
  363. print("[*] CS20 已发 + 心跳运行")
  364. if args.auto_heal:
  365. time.sleep(2)
  366. send_init_sequence(g, sid, uid)
  367. print("[*] 17帧初始化序列已发")
  368. state = enable_auto_heal(g, sid, want_on=True)
  369. if state == "on":
  370. print("[✓] 喝药保护已开启 (服务端确认 but_1)")
  371. elif state == "off":
  372. print("[!] 喝药保护当前为关闭 (切换未生效?)")
  373. else:
  374. print("[!] 未捕获到开关状态面板")
  375. print("[*] 监听服务器数据 ...")
  376. deadline = time.time() + args.wait
  377. total = 0
  378. shown = 0
  379. while time.time() < deadline:
  380. got = recv_frames(g, min(5, max(1, int(deadline - time.time()))))
  381. for m, b in got:
  382. if m[1] == 60000:
  383. continue
  384. total += 1
  385. if shown < 20:
  386. shown += 1
  387. info = ""
  388. if b:
  389. if b[:2] == b"x\x9c":
  390. try:
  391. b = zlib.decompress(b)
  392. info += f"zlib[{len(b)}B] "
  393. except Exception:
  394. pass
  395. info += b[:80].decode("utf-8", "replace")
  396. print(f" S< id={m[1]} p3={m[5]} {info!r}")
  397. print(f"[✓] 完成 — 收到{total}帧游戏数据, 纯协议角色在线 {args.wait}s")
  398. if __name__ == "__main__":
  399. main()