b0bbc2a07f
The oracle is three-valued, and all three fingerprints are now confirmed live
against a running client rather than read out of Ghidra:
NAMED our sentinel rating 7 survives and a real name appears. The id is
real, and teamid/nation/leagueId come back FILLED by the game
because we send them as zero.
placeholder rating 7 survives but the name is 'Jamal Blackman', team 0. The
players-table row exists and is an empty slot. This is the trap:
169193 does this and it was in VERIFIED_ASSET_IDS.
MISS rating 0x32, teamid 0x78d, nation 0xe, position 2, name ' '. That
is the binary's miss-fill, byte for byte, and it is exactly the
blank card photographed in a pack today.
Scale: 5000 candidates per response ingests cleanly; 20000 was served and then
silently not ingested (the map did not change at all), so the ceiling is between
them and auto chunks default to 5000.
Auto-advance: the client PAGES the club, so one visit yields several fetches.
'auto:lo-hi:step' hands out the next chunk per fetch. Item ids derive from the
candidate's offset in the WHOLE range, not its index in the chunk, so chunks never
collide and results accumulate across fetches for a single probe at the end.
sweep_collect.py accumulates into data/players.json and rejects the placeholder
name as a matter of course. Yield in 20000-24999 was 19 real ids per 5000, which
is why auto-advance matters: the real roster clusters in 150000-240000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Force the FIFA 17 FUT store OPEN by patching CardsDLL's online-readiness gate.
|
|
|
|
The "store is not available" screen is gated by FIFA's online-mode manager
|
|
(FUT::CompetitionManager). Three CardsDLL methods report store-enabled and only
|
|
return true once that manager reaches the online "service ready" state -- an
|
|
online/Blaze wall we can't reach offline. This pokes them to return true.
|
|
|
|
Reversible: originals are saved to /tmp/orig_<va>.bin; `restore` puts them back;
|
|
a FIFA restart also clears the patch (live-memory only, per session).
|
|
|
|
Needs ptrace_scope=0 (tools/root_arm.sh) and FIFA running. Run as YOUR action:
|
|
!python3 tools/store_enable_poke.py # patch (open the store)
|
|
!python3 tools/store_enable_poke.py restore # undo
|
|
|
|
Static gate methods (CardsDLL image base 0x180000000), patched to `mov eax,1; ret`:
|
|
0x1800f7fb0 IS_EASTORE_SERVICE_READY (mgr[0x6d4]==0x1fbd0)
|
|
0x1800fb850 IS_STORE_ENABLED (online-mode state stack non-empty)
|
|
0x180100500 IS_COIN_PURCHASABLE (commerce config)
|
|
"""
|
|
import sys, glob, os
|
|
|
|
IMG_BASE = 0x180000000
|
|
RET_TRUE = bytes.fromhex("b801000000c3") # mov eax,1 ; ret
|
|
NOP2 = bytes.fromhex("9090")
|
|
|
|
PATCHES = {
|
|
0x1800f7fb0: ("IS_EASTORE_SERVICE_READY", RET_TRUE),
|
|
0x1800fb850: ("IS_STORE_ENABLED", RET_TRUE),
|
|
0x180100500: ("IS_COIN_PURCHASABLE", RET_TRUE),
|
|
0x180013cf0: ("IS_STORE_AVAILABLE", RET_TRUE),
|
|
0x180017543: ("JMP_BYPASS_RESOLUTION", bytes.fromhex("eb3f")),
|
|
0x180017487: ("NOP_JE_STORE_AVAILABLE", NOP2),
|
|
0x180017490: ("NOP_JNE_STORE_CACHED", NOP2),
|
|
0x1800175aa: ("NOP_JE_STORE_ENTITLEMENT", NOP2),
|
|
}
|
|
DLL_MATCH = "CardsDLL"
|
|
|
|
|
|
def find_pid():
|
|
for d in glob.glob('/proc/[0-9]*'):
|
|
try:
|
|
if open(d + '/comm').read().strip() == 'FIFA17.exe':
|
|
return int(d.split('/')[-1])
|
|
except Exception:
|
|
pass
|
|
raise SystemExit("FIFA17.exe not running")
|
|
|
|
|
|
def cardsdll_base(pid):
|
|
for line in open(f'/proc/{pid}/maps'):
|
|
if DLL_MATCH in line:
|
|
return int(line.split('-')[0], 16) # lowest mapping = module base
|
|
raise SystemExit("CardsDLL not mapped in FIFA process")
|
|
|
|
|
|
def main():
|
|
restore = len(sys.argv) > 1 and sys.argv[1] == "restore"
|
|
pid = find_pid()
|
|
base = cardsdll_base(pid)
|
|
print(f"FIFA pid={pid} CardsDLL base={base:#x} ({'RESTORE' if restore else 'PATCH'})")
|
|
mem = f'/proc/{pid}/mem'
|
|
for va, (name, patch_bytes) in PATCHES.items():
|
|
live = base + (va - IMG_BASE)
|
|
origf = f'/tmp/orig_{live:x}.bin'
|
|
if restore:
|
|
if not os.path.exists(origf):
|
|
print(f" {name}: no saved original, skip"); continue
|
|
data = open(origf, 'rb').read()
|
|
else:
|
|
with open(mem, 'rb') as f:
|
|
f.seek(live); orig = f.read(len(patch_bytes))
|
|
open(origf, 'wb').write(orig)
|
|
data = patch_bytes
|
|
with open(mem, 'r+b') as f:
|
|
f.seek(live); f.write(data)
|
|
f.seek(live); chk = f.read(len(data))
|
|
print(f" {name:26s} @ {live:#x} -> {chk.hex()}")
|
|
print("Done." + ("" if restore else " Now open the FUT Store in-game."))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|