#!/usr/bin/env python3 """ lsx_force_online.py -- force FIFA 17's Origin/LSX layer to report ONLINE. THIS IS LAYER 1. It must succeed before ANY Blaze work (blaze_responder_v3.py) is reachable. The client's own flow graph gates FUT behind Origin: {"name":"launchFUTFlow","file":"/online/origin.nav", "outputs":{"OriginIsOnlineTrue":"startFutBlazeLogin","quit":"mainMenu"}} so while Origin says offline the client shows "Unable to connect to the EA Servers ... log in to Origin in Online Mode" (TXT_ORIGIN_OFFLINE_POPUP_TEXT) and sends Authentication::logout (1/0x46) to Blaze instead of login (1/0x0A). PREFERRED FIX IS NOT THIS FILE. Prefer `lsx_responder.py`: bind 127.0.0.1:4216 BEFORE launching FIFA 17. The Steampunks stub's socket setup (sub_0x6ffffc932130) does bind -> listen -> accept with NO SO_REUSEADDR and, on bind failure, branches to 0x6ffffc932245 -> freeaddrinfo/closesocket/WSACleanup/ return 1 -- i.e. it stands down CLEANLY and the game's OriginSDK connects to us. That is a real request-driven LSX server and can also answer GetAuthCode, GetProfile and QueryEntitlements, which no memory patch can synthesise. USE THIS FILE when the game is ALREADY RUNNING and you only want to flip the online verdict (e.g. to confirm `OriginIsOnlineTrue` fires at all). ------------------------------------------------------------------------------ PATCH (A) -- the emu's response template. DEFAULT. ------------------------------------------------------------------------------ Region : stp-origin_emu.dll unpacked image, 0x6ffffc931000-0x6ffffc93d000, already mapped rwxp (no mprotect needed). Template: 0x6ffffc9353b0 VA : 0x6ffffc9353f4 (the '0' inside connected="0") BEFORE : 30 ('0') AFTER : 31 ('1') The format string is re-read on every use, so the patch applies to every future emission -- but the stub is a BLIND FIXED-SCRIPT REPLAYER (18 canned responses in a fixed order, then ErrorSuccess forever, loop 0x6ffffc932dd3). Template 17 is the InternetConnectedState one. If the game has already passed step 17, the stub is parked in the ErrorSuccess loop and will never emit this template again -- the patch then does nothing, and Q-to-reconnect does NOT help. Patch BEFORE the game boots past the Origin probe, or use patch (B) / lsx_responder.py. ------------------------------------------------------------------------------ PATCH (B) -- g_originOnline, the parsed verdict itself. --flag / --hold ------------------------------------------------------------------------------ Module : FIFA17.exe, mapped flat at 0x140000000 under Wine/Proton. VA : 0x1443337f8 (g_originOnline, one byte) BEFORE : 00 (or stale garbage -- see caveat below) AFTER : 01 Sole reader : 0x146f38aa9 movzx eax, BYTE PTR [0x1443337f8] (the popup / OriginIsOnline predicate; a bare global read, no refresh) Sole writer : 0x146f1e6d9 mov BYTE PTR [0x1443337f8], al -- the LSX GetInternetConnectedState callback (0x146f1e6b0), which also broadcasts FE::FIFA::OriginOnlineEvent. CAVEAT (measured live): this byte currently reads 0x01 already, yet the flow still fails. The stub answered the FIRST GetInternetConnectedState (id 17) with a well-formed connected="0" but answered the LATER polls (ids 19-22) with a type-mismatched generic ErrorSuccess, so the SDK found no `connected` attribute and stored stale garbage. Therefore: a 1 in this byte is NOT sufficient on its own -- the FE::FIFA::OriginOnlineEvent broadcast that the writer performs is what actually drives origin.nav. --hold keeps the byte at 1 so it cannot be clobbered, but only a real LSX reply (lsx_responder.py) makes the callback run and fire the event. Treat (B) as diagnostic. ------------------------------------------------------------------------------ CLEAN ROOM: every address above was recovered by static + dynamic analysis of binaries we own (FIFA17.exe and stp-origin_emu.dll as loaded in our own running process). Nothing derives from the 2021 EA/FIFA leak. USAGE python3 lsx_force_online.py # apply (A); idempotent python3 lsx_force_online.py --flag # apply (A) + (B) once python3 lsx_force_online.py --hold # (A) + rewrite (B) every 0.5s python3 lsx_force_online.py --status # read both, change nothing python3 lsx_force_online.py --restore # put the saved originals back python3 lsx_force_online.py --watch # wait for FIFA17.exe, then apply Requires /proc/PID/mem write access (kernel.yama.ptrace_scope=0, or run as the same user with ptrace_scope=1 which is already known to work here). """ import glob import os import sys import time # ------------------------------------------------------------------ patches # (A) stp-origin_emu.dll InternetConnectedState template. EMU_TEMPLATE_VA = 0x6FFFFC9353B0 EMU_PATCH_VA = 0x6FFFFC9353F4 EMU_BEFORE = b"0" # 0x30 EMU_AFTER = b"1" # 0x31 EMU_TEMPLATE_HEAD = b' (patch_va, template_va) or (None, None).""" want = EMU_TEMPLATE_HEAD try: head = rd(pid, EMU_TEMPLATE_VA, len(want)) if head == want: return EMU_PATCH_VA, EMU_TEMPLATE_VA except Exception: pass log("template not at 0x%x -- rescanning writable+executable maps" % EMU_TEMPLATE_VA) delta = EMU_PATCH_VA - EMU_TEMPLATE_VA # +0x44 try: maps = open("/proc/%d/maps" % pid).read().splitlines() except Exception as e: log("cannot read maps: %s" % e) return None, None for line in maps: try: rng, perms = line.split()[0], line.split()[1] if "w" not in perms or "r" not in perms: continue lo, hi = (int(x, 16) for x in rng.split("-")) if hi - lo > 64 * 1024 * 1024: continue blob = rd(pid, lo, hi - lo) except Exception: continue off = blob.find(want) while off != -1: tva = lo + off pva = tva + delta try: if rd(pid, pva, 1) in (EMU_BEFORE, EMU_AFTER): log("template relocated: 0x%x (patch byte 0x%x)" % (tva, pva)) return pva, tva except Exception: pass off = blob.find(want, off + 1) return None, None # ------------------------------------------------------------------ actions def show_template(pid, tva): try: raw = rd(pid, tva, 96).split(b"\x00")[0] log(" template @0x%x: %s" % (tva, raw.decode("ascii", "replace"))) except Exception: pass def apply_emu(pid): pva, tva = locate_emu_patch(pid) if pva is None: log("PATCH (A): template NOT FOUND -- is stp-origin_emu loaded? " "(is the game past the Origin probe already?)") return False cur = rd(pid, pva, 1) if cur == EMU_AFTER: log("PATCH (A): already applied at 0x%x (connected=\"1\")" % pva) show_template(pid, tva) return True if cur != EMU_BEFORE: log("PATCH (A): UNEXPECTED byte %s at 0x%x (want %s) -- refusing" % (cur.hex(), pva, EMU_BEFORE.hex())) return False backup(pva, cur) wr(pid, pva, EMU_AFTER) now = rd(pid, pva, 1) log("PATCH (A): 0x%x %s -> %s %s" % (pva, cur.hex(), now.hex(), "OK" if now == EMU_AFTER else "FAILED")) show_template(pid, tva) return now == EMU_AFTER def apply_flag(pid): cur = rd(pid, FLAG_VA, 1) if cur == FLAG_AFTER: log("PATCH (B): g_originOnline @0x%x already 0x01" % FLAG_VA) return True backup(FLAG_VA, cur) wr(pid, FLAG_VA, FLAG_AFTER) now = rd(pid, FLAG_VA, 1) log("PATCH (B): g_originOnline @0x%x %s -> %s %s" % (FLAG_VA, cur.hex(), now.hex(), "OK" if now == FLAG_AFTER else "FAILED")) return now == FLAG_AFTER def status(pid): pva, tva = locate_emu_patch(pid) if pva is None: log("STATUS (A): template not found in this process") else: b = rd(pid, pva, 1) log("STATUS (A): 0x%x = %s -> connected=\"%s\"%s" % (pva, b.hex(), b.decode("ascii", "replace"), " [PATCHED]" if b == EMU_AFTER else "")) show_template(pid, tva) b = rd(pid, FLAG_VA, 1) log("STATUS (B): g_originOnline @0x%x = %s (%s)" % (FLAG_VA, b.hex(), "online" if b == b"\x01" else "offline/garbage")) log("NOTE: a 1 in (B) is NOT proof of success -- see the CAVEAT in this " "file's docstring. Only a well-formed LSX connected=\"1\" reply makes " "the callback broadcast FE::FIFA::OriginOnlineEvent, which is what " "origin.nav actually consumes.") def restore(pid): if not os.path.isdir(BACKUP_DIR): log("RESTORE: nothing saved in %s" % BACKUP_DIR) return for fn in sorted(os.listdir(BACKUP_DIR)): if not fn.startswith("orig_"): continue va = int(fn[5:].split(".")[0], 16) orig = open(os.path.join(BACKUP_DIR, fn), "rb").read() try: wr(pid, va, orig) log("RESTORE: 0x%x <- %s (now %s)" % (va, orig.hex(), rd(pid, va, len(orig)).hex())) except Exception as e: log("RESTORE: 0x%x FAILED: %s" % (va, e)) # ------------------------------------------------------------------ main def main(): argv = sys.argv[1:] want_flag = "--flag" in argv or "--hold" in argv hold = "--hold" in argv if "--watch" in argv: log("=== WATCH: waiting for FIFA17.exe ===") seen = set() while True: pid = find_pid() if pid and pid not in seen: try: if apply_emu(pid): if want_flag: apply_flag(pid) seen.add(pid) except Exception as e: log("pid %d not ready yet (%s)" % (pid, e)) time.sleep(1) pid = find_pid() if not pid: raise SystemExit("FIFA17.exe not running (use --watch to wait for it)") log("=== lsx_force_online pid=%d ===" % pid) if "--status" in argv: status(pid) return if "--restore" in argv: restore(pid) return apply_emu(pid) if want_flag: apply_flag(pid) if hold: log("HOLD: rewriting g_originOnline every 0.5 s (Ctrl-C to stop)") try: while True: try: if rd(pid, FLAG_VA, 1) != FLAG_AFTER: wr(pid, FLAG_VA, FLAG_AFTER) log("HOLD: g_originOnline was clobbered, reset to 0x01") except Exception as e: log("HOLD: process gone (%s)" % e) break time.sleep(0.5) except KeyboardInterrupt: log("HOLD: stopped") if __name__ == "__main__": main()