| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- """Find game-218620's Lua XXTEA key.
- Layout hint from cq2.exe: sign b'4meJnPyl\\0\\0\\0\\0' + key b'34UFXMgtkz\\0\\0'
- — sign and key stored adjacently. Game 218620's cache uses sign 'XdvFTgd5',
- so its key is likely stored next to that string in ITS module binary
- (or in cq2.exe itself if multiple keys are embedded).
- Probe:
- 1. Search cq2.exe for 'XdvFTgd5' — read next 16 bytes as key candidate
- 2. Search game-218620's own module dir binaries
- 3. Try decrypting one of its .lua with each candidate
- """
- import struct
- import sys
- from pathlib import Path
- sys.path.insert(0, str(Path(__file__).parent))
- from decrypt_client_lua import SIGN, DELTA, bytes_to_u32, u32_to_bytes
- BASE = Path(r"D:\ProgramFiles\996box\GameBoxDownload\1\cq")
- SIGN2 = b"XdvFTgd5"
- def xxtea_decrypt(v, key):
- n = len(v)
- if n < 2:
- return v
- rounds = 6 + 52 // n
- total = (rounds * DELTA) & 0xFFFFFFFF
- y = v[0]
- while total != 0:
- e = (total >> 2) & 3
- for p in range(n - 1, 0, -1):
- z = v[p - 1]
- mx = (((z >> 5) ^ ((y << 2) & 0xFFFFFFFF)) + ((y >> 3) ^ ((z << 4) & 0xFFFFFFFF))) ^ ((total ^ y) + (key[(p & 3) ^ e] ^ z))
- mx &= 0xFFFFFFFF
- y = v[p] = (v[p] - mx) & 0xFFFFFFFF
- z = v[n - 1]
- mx = (((z >> 5) ^ ((y << 2) & 0xFFFFFFFF)) + ((y >> 3) ^ ((z << 4) & 0xFFFFFFFF))) ^ ((total ^ y) + (key[0 ^ e] ^ z))
- mx &= 0xFFFFFFFF
- y = v[0] = (v[0] - mx) & 0xFFFFFFFF
- total = (total - DELTA) & 0xFFFFFFFF
- return v
- def try_decrypt(raw: bytes, key16: bytes) -> bytes | None:
- try:
- v = bytes_to_u32(raw[8:], include_length=False)
- k = bytes_to_u32(key16, include_length=False)
- v = xxtea_decrypt(v, k)
- real_len = v[-1]
- if real_len <= 0 or real_len > (len(v) - 1) * 4:
- return None
- plain = u32_to_bytes(v[:-1])[:real_len]
- # validate: looks like Lua text
- head = plain[:60].lstrip()
- if head.startswith((b"--", b"local", b"function", b"return", b"require")):
- return plain
- return None
- except Exception:
- return None
- def find_key_candidates() -> list[bytes]:
- cands: list[bytes] = []
- targets = [
- BASE / "cq2.exe",
- BASE / "libcocos2d.dll",
- ]
- # also 218620's own module dir
- for extra in (BASE / "cache_218620").rglob("*.exe"):
- targets.append(extra)
- for extra in (BASE / "cache_218620").rglob("*.dll"):
- targets.append(extra)
- for t in targets:
- if not t.exists() or t.stat().st_size > 100_000_000:
- continue
- data = t.read_bytes()
- i = data.find(SIGN2)
- while i >= 0:
- # key likely follows the sign after NUL padding: read next 24 bytes
- after = data[i + 8 : i + 8 + 24]
- # extract printable run(s)
- import re
- for m in re.finditer(rb"[!-~]{6,20}", after):
- cand = m.group()
- cands.append(cand)
- # also the 16 raw bytes right after (padded)
- cands.append(after[:16].rstrip(b"\0").ljust(16, b"\0"))
- i = data.find(SIGN2, i + 1)
- return cands
- def main():
- sample = None
- for f in (BASE / "cache_218620").rglob("*.lua"):
- raw = f.read_bytes()
- if raw[:8] == SIGN2:
- sample = (f, raw)
- break
- if not sample:
- print("no sample with sign XdvFTgd5 found")
- return
- f, raw = sample
- print(f"sample: {f.name} ({len(raw)}B) sign={raw[:8]!r}")
- cands = find_key_candidates()
- print(f"key candidates from binaries: {len(cands)}")
- for c in set(cands):
- if len(c) < 6:
- continue
- key16 = c.ljust(16, b"\0")[:16]
- plain = try_decrypt(raw, key16)
- status = "HIT!" if plain else "-"
- print(f" {status} key={c!r}")
- if plain:
- print(f" first 80 chars: {plain[:80]!r}")
- out = Path("lua-dec2/cache_218620_test.lua")
- out.write_bytes(plain)
- print(f" saved -> {out}")
- return
- print("no candidate worked")
- main()
|