#!/usr/bin/env python3 """Capture-first LSX diagnostic for FIFA 17 on 127.0.0.1:4216. Goal: learn the REAL handshake the game speaks, losing nothing to a crash. - Detects who sends first (server-initiates vs client-initiates). - Sends our best-guess Challenge, then captures the client's ChallengeResponse (its key + exact XML format) -- FLUSHED TO DISK BEFORE we send anything risky. - Then runs the full instrumented handshake (H, session key, encrypted loop), logging every computed value + every frame both directions. So even if the game crashes on our ChallengeAccepted, the key capture is saved. """ import socket, time, threading, os, sys import lsx_responder as L # reuse crypto + build_reply; importing does NOT run main LOG = "/tmp/lsx_capture.log" RAWDIR = "/tmp/lsx_raw" os.makedirs(RAWDIR, exist_ok=True) _n = 0 def log(m): line = "[%s] %s" % (time.strftime("%H:%M:%S"), m) print(line, flush=True) with open(LOG, "a") as f: f.write(line + "\n"); f.flush(); os.fsync(f.fileno()) def dump(tag, b): log("%s : %dB" % (tag, len(b))) for i in range(0, min(len(b), 512), 16): c = b[i:i+16] hx = " ".join("%02x" % x for x in c) asc = "".join(chr(x) if 32 <= x < 127 else "." for x in c) log(" %04x: %-47s %s" % (i, hx, asc)) try: p = "%s/%s_%d.bin" % (RAWDIR, tag.replace(" ", "_").replace("#", "").replace(":", ""), int(time.time()*1000) % 1000000) open(p, "wb").write(b) except Exception: pass def serve(conn, addr): global _n; _n += 1; n = _n log("================= CONN #%d from %s =================" % (n, addr)) # Phase 0 -- does the CLIENT speak first? (would mean lsx_responder has the # initiator backwards, a prime crash suspect) conn.settimeout(3.0) try: pre = conn.recv(8192) if pre: dump("C%d PRE (client spoke FIRST)" % n, pre) log("C%d: !!! client initiates -- our server-sends-Challenge model is WRONG" % n) except socket.timeout: log("C%d: silent 3s -> server initiates (send Challenge)" % n) pre = b"" except Exception as e: log("C%d pre-recv err: %s" % (n, e)); pre = b"" # Phase 1 -- send our best-guess plaintext Challenge, capture the reply. chal = ('' '' % (L.CHALLENGE_KEY, L.BUILD, L.VERSION)) try: conn.sendall(chal.encode() + b"\0") log("C%d >> Challenge sent (%dB): %s" % (n, len(chal)+1, chal)) except Exception as e: log("C%d send-Challenge err: %s" % (n, e)); return try: rep = conn.recv(8192) except Exception as e: log("C%d recv-after-Challenge err: %s" % (n, e)); return if not rep: log("C%d: client closed after our Challenge (no ChallengeResponse) -- " "Challenge format likely rejected" % n); return dump("C%d CHALLENGE-RESPONSE (client)" % n, rep) # <-- THE KEY CAPTURE, already flushed # Phase 2 -- log what WE would compute (do NOT let a crypto error abort logging) txt = rep.split(b"\0")[0].decode(errors="replace") log("C%d client-reply text: %s" % (n, txt)) import re mk = re.search(r'key="([^"]*)"', txt) mr = re.search(r'response="([^"]*)"', txt) client_key = mk.group(1) if mk else L.CHALLENGE_KEY client_resp = mr.group(1) if mr else None log("C%d parsed: client_key=%r client_response=%r" % (n, client_key, client_resp)) try: our_h = L.challenge_response(client_key) log("C%d our computed H (ChallengeAccepted.response) = %s" % (n, our_h)) if client_resp: log("C%d MATCH client_response==our_H ? %s" % (n, client_resp == our_h)) skey = L.derive_session_key(our_h) log("C%d derived session_key = %s" % (n, skey.hex())) except Exception as e: log("C%d crypto compute err: %s" % (n, e)); our_h = None; skey = None # Phase 3 -- OPTIONAL: send ChallengeAccepted + run the encrypted loop. # Guarded by env so the first run can stay capture-only (no risky send). if os.environ.get("LSX_FULL") == "1" and our_h and skey: try: conn.sendall(L.resp(1, 'ChallengeAccepted response="%s"' % our_h, "EALS").encode() + b"\0") log("C%d >> ChallengeAccepted sent" % n) except Exception as e: log("C%d send-Accepted err: %s" % (n, e)); return while True: try: data = conn.recv(65536) except Exception as e: log("C%d recv-loop err: %s" % (n, e)); break if not data: log("C%d: client closed" % n); break dump("C%d ENC-IN" % n, data) for chunk in filter(None, data.split(b"\0")): try: xml = L.lsx_decrypt(chunk + b"\0", skey) log("C%d << decrypted: %s" % (n, xml)) except Exception as e: log("C%d decrypt fail: %s (raw %s)" % (n, e, chunk[:40])); continue mm = L.REQ_RE.search(xml) if mm: reply = L.build_reply(mm.group(1), mm.group(2), dict(L.ATTR_RE.findall(mm.group(3)))) try: conn.sendall(L.lsx_encrypt(reply, skey)) log("C%d >> %s" % (n, reply)) except Exception as e: log("C%d send-reply err: %s" % (n, e)); break else: log("C%d: capture-only (set LSX_FULL=1 to attempt full handshake). Holding 10s." % n) time.sleep(10) try: conn.close() except Exception: pass def main(): s = socket.socket() s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(("127.0.0.1", 4216)); s.listen(8) log("=== capture_lsx listening on 127.0.0.1:4216 (LSX_FULL=%s) ===" % os.environ.get("LSX_FULL", "0")) while True: c, a = s.accept() threading.Thread(target=serve, args=(c, a), daemon=True).start() if __name__ == "__main__": main()