#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Extract FIFA 17's real player roster (playerId -> name + rating) from a LIVE FIFA17.exe process. READ-ONLY: /proc/PID/mem is opened 'rb' and only ever seek()/read(). There is no write path in this file. WHY THIS FILE EXISTS, AND WHY IT IS NOT "dbdata.dll" --------------------------------------------------- The plan of record was to pull the roster out of `/mnt/games/FIFA 17/dbdata.dll` (2,686,152 bytes, one export `getTableData`, 2.5 MB `.xdata` payload). That DLL is NOT a database. Verified this session by building tools/dbdata_probe.c with x86_64-w64-mingw32-gcc and running it under Wine: base=00006FFFFA980000 getTableData=00006FFFFA9816B0 (rva 0x16b0) call 0: ret=00007FFFFEBF5DB0 len=1012 -> 1012 chars of base64url decoded: 759 bytes, md5 dc97c0dfd5edea5fb379dc14d8017980, entropy ~7.9 `.xdata` measures 7.52 bits/byte of entropy uniformly across its whole 2,515,528 bytes (sampled at 0x0/0x1000/0x100000/0x200000/0x260000, zero 16-byte NUL runs), i.e. it is encrypted/packed, and the single export hands back a ~759-byte attestation blob, not tables. There is no table selector argument. So the roster cannot be read out of dbdata.dll without breaking its packer. The roster IS, however, fully resident in the running game. FIFA 17 builds a flat, rating-sorted index of every player in the base DB and keeps it on the heap. That is what this tool reads. THE STRUCTURE (resolved live, 2026-08-04, pid 11864, game sitting at the menu) ----------------------------------------------------------------------------- Two heap regions cooperate: * a "name pool" region (seen at 0x0d790000..0x0dc40000, 4.8 MB, rw-p) holding ~17.5k individually-allocated, NUL-terminated UTF-8 strings in the form "||" e.g. "Cristiano|Ronaldo|", "Neymar|da Silva Santos Jr.|Neymar". commonName is usually empty (the string then ends in "||"). * an index table (seen at 0x0b8450d40 .. 0x0b8562fc0, rw-p) of 17,547 entries at a constant stride of 0x40 bytes, no gaps, sorted by rating DESCENDING: +0x00 u32 playerId (20801 = Cristiano Ronaldo) +0x04 u32 rank (0..16546, dense, == entry index) +0x08 u32 rating (73..94 at the head, down to 40s at tail) +0x0c u32 aux (0 for most entries; a 32-bit hash for some -- purpose unresolved) +0x10 char* name begin -> into the name pool +0x18 char* name end == begin + strlen +0x20 char* name end + 1 +0x28 u64 0x2c020e50 (constant across every entry) +0x30 u64 1, or a 32-bit hash in the low dword +0x38 u64 0x6ffffc32a968 (constant across every entry -- a vtable or allocator handle in the Wine range) The {begin, end, end+1} triple at +0x10 is the reliable signature: it is self-validating (end-begin == strlen, and end+1 == the third pointer), which is why this tool anchors on it instead of on any hard-coded address. Nothing here is a fixed VA: run it against any FIFA17.exe and it re-locates the table. WHAT THIS GIVES YOU AND WHAT IT DOES NOT ---------------------------------------- GIVES: playerId, rank, rating, firstName, lastName, commonName -- for the complete 17,547-player FIFA 17 roster. DOES NOT: position, nationality, teamId, or the six face attributes. Those are NOT in this table. See the "STILL MISSING" note at the bottom of this file for the leads that were found for them. ANCHOR CHECK (the one the task asked for): playerId 20801 must be "Cristiano|Ronaldo|" rated 94. --check enforces it and exits non-zero if the parse disagrees. USAGE ----- ./dbdata_extract.py # extract, write players_fifa17.json ./dbdata_extract.py --check # extract + assert the Ronaldo anchor ./dbdata_extract.py -o /tmp/roster.json ./dbdata_extract.py --pid 11864 ./dbdata_extract.py --top 40 # print the top 40 and exit Requires ptrace access to the FIFA process (this project already runs with kernel.yama.ptrace_scope=1 and the same uid, which is sufficient). """ import argparse import json import os import re import struct import sys from collections import Counter ENTRY_STRIDE = 0x40 NAME_TRIPLE_OFF = 0x10 # offset of {begin,end,end+1} inside an entry MAX_NAME_LEN = 120 RONALDO_ID = 20801 RONALDO_RATING = 94 RONALDO_NAME = "Cristiano|Ronaldo|" def find_pid(): for d in os.listdir('/proc'): if not d.isdigit(): continue try: if open('/proc/%s/comm' % d).read().strip() == 'FIFA17.exe': return int(d) except OSError: pass raise SystemExit("FIFA17.exe is not running (this tool needs the live game)") def read_maps(pid): """Readable, non-file-backed-or-anon RW regions, small enough to slurp.""" out = [] for line in open('/proc/%d/maps' % pid): parts = line.split() lo, hi = parts[0].split('-') lo, hi = int(lo, 16), int(hi, 16) perms = parts[1] if 'r' not in perms: continue if hi - lo > (1 << 31): continue out.append((lo, hi, perms, parts[5] if len(parts) > 5 else '')) return out class Mem(object): def __init__(self, pid): self.f = open('/proc/%d/mem' % pid, 'rb', 0) self.cache = {} def read(self, va, n): self.f.seek(va) return self.f.read(n) def region(self, lo, hi): if (lo, hi) not in self.cache: try: self.f.seek(lo) self.cache[(lo, hi)] = self.f.read(hi - lo) except OSError: self.cache[(lo, hi)] = b'' return self.cache[(lo, hi)] def find_name_pools(mem, maps): """Regions containing many '||\\0' strings = the player-name pool.""" pat = re.compile(rb'[^\x00|][^\x00|]{0,44}\|[^\x00|]{0,49}\|[^\x00|]{0,49}\x00') pools = [] for lo, hi, perms, name in maps: if 'w' not in perms or name: continue if not (0x100000 <= hi - lo <= 0x4000000): continue d = mem.region(lo, hi) if not d: continue n = len(pat.findall(d)) if n >= 2000: pools.append((lo, hi, n)) return pools def scan_entries(mem, maps, pools): """Anchor on the self-validating {begin,end,end+1} name triple.""" lows = [(lo, hi) for lo, hi, _ in pools] def in_pool(va): for lo, hi in lows: if lo <= va < hi: return True return False def pool_bytes(va, n): for lo, hi in lows: if lo <= va and va - lo + n <= hi - lo: return mem.region(lo, hi)[va - lo:va - lo + n] return mem.read(va, n) found = {} for lo, hi, perms, name in maps: if 'w' not in perms or name: continue d = mem.region(lo, hi) if len(d) < ENTRY_STRIDE: continue for off in range(0, len(d) - ENTRY_STRIDE, 8): b, e, c = struct.unpack_from(' %r rating %d\n" % (RONALDO_ID, got, cr['rating'])) if cr['rating'] != RONALDO_RATING or got != RONALDO_NAME: sys.stderr.write("ANCHOR FAIL: expected %r / %d\n" % (RONALDO_NAME, RONALDO_RATING)) ok = False with open(args.out, 'w') as fh: json.dump(rows, fh, ensure_ascii=False, indent=1) sys.stderr.write("wrote %s (%d players)\n" % (args.out, len(rows))) if args.check and not ok: return 1 return 0 # STILL MISSING: position / nationality / teamId / the six attributes. # # They are NOT in the index table above. Two leads were located live and are # recorded here so the next pass does not have to re-find them: # # (1) Materialised FUT card records. In the 40 MB heap region at 0x0b63b0000 # the squad's resolved cards sit at a 0x180 stride, e.g. 0x0b840c2c0 = # Lewandowski and 0x0b840c440 = Luis Suarez. Layout relative to the record # word at +0x00c (0xf0, 0xf1 -- consecutive, an index): # +0x010..+0x02c six u32 attributes then two more u32 # Lewandowski: 77 88 75 82 42 82 | 99 90 # Suarez : 83 90 79 87 42 80 | 99 92 # the last u32 is the overall rating (90 / 92, both correct for # FIFA 17), the 99 is constant across both. # +0x030 char[16] firstName ("Robert", "Luis") # +0x040 char[16] lastName ("Lewandowski", "Suárez") # Only ~a hundred of these exist process-wide -- they are built per card # that the client actually materialises, not a table. So this is a # VERIFICATION oracle for attribute values, not a bulk source. # # (2) A 32-byte-stride keyed table in the 238 MB heap region 0x37440000.., # seen at 0x42e8dbe8, carrying u64 fields keyed by playerId: # 20801 -> 27, 94, 77 # 41236 -> 25, 90, 80 (41236 = Zlatan Ibrahimovic, rating 90) # The rating column is right in both rows; the 27/25 and 77/80 columns were # NOT identified. A 32-bit hash is interleaved in the high dword of a # rotating slot, so it is a hash container, not a flat array. Worth one # focused pass. # # Neither of these was pushed to completion in the session that wrote this file. # Do not cite them as resolved. if __name__ == '__main__': sys.exit(main())