decrypt_client_lua.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. """Self-contained cocos2d-x XXTEA Lua decryptor.
  2. File format (per cocos2d-x xxtea.c + cq2-00 docs):
  3. [8-byte sign "4meJnPyl"] [XXTEA ciphertext of zlib-compressed lua source]
  4. Key: "34UFXMgtkz" padded to 16 bytes with NULs.
  5. Decrypt flow (mirrors cocos exactly):
  6. 1. ciphertext bytes -> uint32 LE array (no length append)
  7. 2. standard XXTEA decrypt (delta=0x9E3779B9, rounds=6+52//n)
  8. 3. last uint32 = real plaintext length -> truncate
  9. 4. zlib decompress
  10. Usage:
  11. uv run python scripts/decrypt_client_lua.py # decrypts key proxies
  12. uv run python scripts/decrypt_client_lua.py ALL # decrypt whole stab tree
  13. """
  14. import struct
  15. import sys
  16. import zlib
  17. from pathlib import Path
  18. SIGN = b"4meJnPyl"
  19. KEY = b"34UFXMgtkz".ljust(16, b"\0")
  20. DELTA = 0x9E3779B9
  21. STAB = Path(r"D:\ProgramFiles\996box\GameBoxDownload\1\cq\cache_206923\mod_fgcq\stab\scripts")
  22. OUT = Path("lua-dec2")
  23. def xxtea_decrypt(v: list[int], key: list[int]) -> list[int]:
  24. n = len(v)
  25. if n < 2:
  26. return v # cocos: n<1 returns; n==1 unchanged by loop below effectively
  27. rounds = 6 + 52 // n
  28. total = (rounds * DELTA) & 0xFFFFFFFF
  29. y = v[0]
  30. while total != 0:
  31. e = (total >> 2) & 3
  32. for p in range(n - 1, 0, -1):
  33. z = v[p - 1]
  34. mx = (((z >> 5) ^ ((y << 2) & 0xFFFFFFFF)) + ((y >> 3) ^ ((z << 4) & 0xFFFFFFFF))) ^ ((total ^ y) + (key[(p & 3) ^ e] ^ z))
  35. mx &= 0xFFFFFFFF
  36. y = v[p] = (v[p] - mx) & 0xFFFFFFFF
  37. z = v[n - 1]
  38. mx = (((z >> 5) ^ ((y << 2) & 0xFFFFFFFF)) + ((y >> 3) ^ ((z << 4) & 0xFFFFFFFF))) ^ ((total ^ y) + (key[0 ^ e] ^ z))
  39. mx &= 0xFFFFFFFF
  40. y = v[0] = (v[0] - mx) & 0xFFFFFFFF
  41. total = (total - DELTA) & 0xFFFFFFFF
  42. return v
  43. def bytes_to_u32(data: bytes, include_length: bool) -> list[int]:
  44. # cocos xxtea_to_uint_array: pad tail bytes into last u32 by OR (no truncation)
  45. n = (len(data) >> 2) + (0 if (len(data) & 3) == 0 else 1)
  46. v = [0] * n
  47. for i in range(len(data)):
  48. v[i >> 2] |= data[i] << ((i & 3) << 3)
  49. if include_length:
  50. v.append(len(data))
  51. return v
  52. def u32_to_bytes(v: list[int]) -> bytes:
  53. return struct.pack(f"<{len(v)}I", *v)
  54. def cocos_decrypt(raw: bytes) -> bytes:
  55. if raw[:8] != SIGN:
  56. raise ValueError(f"bad sign {raw[:8]!r}")
  57. cipher = raw[8:]
  58. v = bytes_to_u32(cipher, include_length=False)
  59. key = bytes_to_u32(KEY, include_length=False)
  60. v = xxtea_decrypt(v, key)
  61. real_len = v[-1]
  62. if real_len <= 0 or real_len > (len(v) - 1) * 4:
  63. raise ValueError(f"implausible length {real_len}")
  64. plain = u32_to_bytes(v[:-1])[:real_len]
  65. # This game's scripts are stored as plain Lua text (no zlib layer).
  66. # But tolerate both: if it starts with zlib magic, decompress.
  67. if plain[:2] == b"\x78\x9c" or plain[:2] == b"\x78\x01" or plain[:2] == b"\x78\xda":
  68. plain = zlib.decompress(plain)
  69. return plain
  70. def decrypt_file(src: Path, dst: Path) -> bool:
  71. try:
  72. plain = cocos_decrypt(src.read_bytes())
  73. dst.parent.mkdir(parents=True, exist_ok=True)
  74. dst.write_bytes(plain)
  75. return True
  76. except Exception as e:
  77. print(f" FAIL {src.name}: {type(e).__name__}: {e}")
  78. return False
  79. def main() -> int:
  80. mode = sys.argv[1] if len(sys.argv) > 1 else "KEY"
  81. if mode.upper() == "ALL":
  82. files = list(STAB.rglob("*.lua"))
  83. out_root = OUT / "stab"
  84. ok = 0
  85. for i, f in enumerate(files):
  86. rel = f.relative_to(STAB)
  87. if decrypt_file(f, out_root / rel):
  88. ok += 1
  89. if i % 200 == 0:
  90. print(f" {i}/{len(files)}...")
  91. print(f"decrypted {ok}/{len(files)} -> {out_root}")
  92. else:
  93. targets = [
  94. "game/proxy/remote/AuthProxy.lua",
  95. "game/proxy/remote/LoginOtpPassWordProxy.lua",
  96. "game/proxy/remote/LoginProxy.lua",
  97. "game/proxy/remote/RemoteProxy.lua",
  98. "network/networkUtil.lua",
  99. "game/proxy/GameEnvironmentProxy.lua",
  100. ]
  101. for t in targets:
  102. src = STAB / t
  103. if not src.exists():
  104. print(f" missing: {t}")
  105. continue
  106. dst = OUT / t
  107. if decrypt_file(src, dst):
  108. print(f" OK: {t} -> {dst} ({dst.stat().st_size}B)")
  109. return 0
  110. if __name__ == "__main__":
  111. sys.exit(main())