#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Resolve the runtime string comparator behind `DAT_1802ddfd8 + 0x248`, and settle whether the `itemState` match is case-sensitive. READ-ONLY. /proc/PID/mem is opened 'rb'; there is no write path in this file. WHY THIS EXISTS --------------- `itemState` arrives on the wire as a STRING ("free", "activeHomeKit", ...) and the client turns it into its runtime enum by comparing that string against its own table. The compare goes through `FUN_180008190`, whose whole body is: mov rax, [DAT_1802ddfd8] ; the service object, populated at runtime mov r9, [rax + 0x248] ; slot 0x248 jmp r9 ; tail-jump The slot is empty on disk, so `plan-2026-08-06-card-subsystem.md` section 5 recorded the casing question as "almost certainly unresolvable statically" and listed this as a read-only live probe. It is worth answering: every shaper in openfut-adapter-fifa17 emits these tokens, and if the comparator folded case then our table's casing would be a convention rather than a contract. WHAT IT DOES ------------ Reads the slot in the live process and follows the forwarding chain (`e9` rel32 thunk -> `ff 25` IAT jump -> body), attributing each hop to a module. Wine maps PE images as anonymous, so a mapping's own path is usually empty; the module is recovered from the nearest PRECEDING named mapping, which is the PE header page. At the body it decides case sensitivity from the instruction stream rather than from a symbol name: a case-insensitive comparator MUST fold case, so it carries an `or ..,0x20` / lowercase-table lookup. A byte compare with no folding is case-SENSITIVE. MEASURED 2026-08-21 (pid 6580): slot -> 0x146d1c020 (thunk) -> 0x145e27fe0 (IAT) -> msvcr120.dll + 0x3c330 body is strncmp: `sub rdx,rcx` / `test r8,r8` (count) / `test al,al` (NUL) / `cmp al,[rcx+rdx]` with NO case folding, plus the MSVC NUL-detect constants 0x8080808080808080 and 0xfefefefefefefeff. => the itemState match is CASE-SENSITIVE. Emit the table's exact casing. Usage: python3 service_ptr_probe.py """ import re import struct import sys import watch_club_model as W DAT_SERVICE = 0x1802DDFD8 SLOT = 0x248 MAX_HOPS = 8 # A case-insensitive comparator has to fold case somewhere. These are the two # ways MSVC does it; neither appears in a plain strcmp/strncmp/memcmp. FOLD_OR_IMM8 = b"\x0c\x20" # or al, 0x20 FOLD_OR_EAX = b"\x83\xc8\x20" # or eax, 0x20 def mappings(pid): out = [] with open("/proc/%d/maps" % pid) as fh: for line in fh: m = re.match(r"([0-9a-f]+)-([0-9a-f]+) (\S{4}) \S+ \S+ \S+\s*(.*)", line) if m: out.append((int(m.group(1), 16), int(m.group(2), 16), m.group(3), m.group(4))) return out def attribute(maps, va): """(module_path, perms, offset_from_module_base) for `va`. Wine maps PE sections anonymously, so the owning mapping usually has no path; the module is the nearest preceding NAMED mapping (its header page). """ named = None for start, end, perms, path in maps: if path: named = (start, path) if start <= va < end: if named: return named[1], perms, va - named[0] return path or "[anonymous]", perms, None return None, None, None def main(): pid = W.find_pid() if pid is None: print("FIFA17.exe is not running.") return 1 base = W.dll_base(pid) if base is None: print("pid %d is up but %s is not mapped." % (pid, W.DLL)) return 1 mem = W.Mem(pid) maps = mappings(pid) glob = base + (DAT_SERVICE - W.IMG_BASE) svc = mem.q(glob) print("pid=%d %s base=%#x" % (pid, W.DLL, base)) print("DAT_1802ddfd8 @ %#x -> service %#x" % (glob, svc or 0)) if not svc: print("service pointer is NULL; the host has not handed CardsDLL its table yet.") return 2 va = mem.q(svc + SLOT) print("*(service + %#x) = %#x" % (SLOT, va or 0)) if not va: print("slot %#x is empty." % SLOT) return 2 print() body = None for hop in range(MAX_HOPS): buf = mem.read(va, 16) if not buf or len(buf) < 6: print("hop %d: %#x unreadable" % (hop, va)) return 2 path, perms, off = attribute(maps, va) where = "%s+%#x" % (path, off) if off is not None else str(path) print("hop %d: %#x [%s] %s %s" % (hop, va, perms, where, buf[:8].hex())) if buf[0] == 0xE9: # jmp rel32 va = va + 5 + struct.unpack(" function body") break if body is None: print("chain did not settle within %d hops." % MAX_HOPS) return 2 addr, path, off = body code = mem.read(addr, 256) or b"" folds = FOLD_OR_IMM8 in code or FOLD_OR_EAX in code print() print("=" * 70) print("COMPARATOR: %s+%#x (%#x)" % (path, off if off is not None else 0, addr)) print("case folding in first %d bytes: %s" % (len(code), "YES" if folds else "NO")) if folds: print("VERDICT: case-INSENSITIVE. itemState casing is a convention, not a contract.") else: print("VERDICT: case-SENSITIVE. A byte compare with no folding means the") print(" wire token must match the table's casing EXACTLY -- a") print(" mis-cased token silently resolves to itemState 0 (invalid).") print(" openfut-adapter-fifa17's fut::item_state table is therefore") print(" a contract: emit its casing verbatim.") print("=" * 70) return 0 if __name__ == "__main__": sys.exit(main())