fifa17-recon: reconcile authoritative tools with running backend (B)
- Add 8 files present in docker/fifa17-python/tools but missing from the top-level tree: fut_accounts.py + 7 test_*.py contracts (all committed in the server's docker tree; byte-identical to the running image). - Preserve newer responder work already matching the running container: utas_server.py (offlineSeason), lsx_responder_v2.py (OPENFUT_BIND), blaze_responder_v3b.py, autopatch.py, pow_server.py, fut_store.py, test_fut_contract.py, fifa17-hook-m1.sh. - Add 30 newer ghidra_queries (draft purchase/state, SBC 9-26, runtime registries). Local tree is now a strict superset of B with all shared files byte-identical.
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
"""Trace FutPurchaseDraftModeServerResponse beyond its known seven-int parser."""
|
||||
|
||||
cls = "FutPurchaseDraftModeServerResponse"
|
||||
print("CLASS", cls, class_deser(cls))
|
||||
|
||||
seen = set()
|
||||
for deser, vt, factory in class_deser(cls):
|
||||
print("\nVTABLE", hex(vt), "FACTORY", hex(factory), "DESER", hex(deser))
|
||||
print(vtable(vt, 32))
|
||||
for target in [factory, deser] + [t for _, t, name in vtable(vt, 32) if name]:
|
||||
if target in seen:
|
||||
continue
|
||||
seen.add(target)
|
||||
print("\n===", hex(target), fname(target), "===")
|
||||
print(dec(target, 300))
|
||||
print("XREFS", xrefs_to(target)[:100])
|
||||
|
||||
for target in (0x18014C090, 0x18014C260, 0x18014C820, 0x18014C8A0):
|
||||
if target in seen:
|
||||
continue
|
||||
print("\n=== CANDIDATE", hex(target), fname(target), "===")
|
||||
print(dec(target, 300))
|
||||
print("XREFS", xrefs_to(target)[:100])
|
||||
|
||||
print("QUERY_DONE")
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Bind the live draft-purchase URI builder to one of its two response factories."""
|
||||
|
||||
for literal in (
|
||||
"purchase/mode/",
|
||||
"/purchase/mode/",
|
||||
"draft",
|
||||
"ut/%s/draft/mode",
|
||||
"FutPurchaseDraftModeServerResponse",
|
||||
):
|
||||
print("\nLITERAL", repr(literal))
|
||||
for hit in find_all(literal.encode() + b"\x00"):
|
||||
print(hex(hit), rd_str(hit), xrefs_to(hit)[:100])
|
||||
|
||||
for target in (0x180224EF8, 0x1802262F0, 0x18014C090, 0x180150260):
|
||||
print("\nTARGET", hex(target), fname(target))
|
||||
print("XREFS", xrefs_to(target)[:200])
|
||||
for frm, typ, fn, ent in xrefs_to(target):
|
||||
if ent:
|
||||
print("\nOWNER", hex(ent), fn)
|
||||
print(dec(ent, 300))
|
||||
|
||||
print("QUERY_DONE")
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Dump both request vtables sharing FutPurchaseDraftModeServerResponse."""
|
||||
|
||||
for vt in (0x180226300, 0x180224F08):
|
||||
print("\nREQUEST_VTABLE", hex(vt))
|
||||
rows = vtable(vt, 40)
|
||||
print(rows)
|
||||
seen = set()
|
||||
for off, target, name in rows:
|
||||
if not name or target in seen:
|
||||
continue
|
||||
seen.add(target)
|
||||
print("\n=== SLOT", hex(off), hex(target), name, "===")
|
||||
print(dec(target, 300))
|
||||
print("XREFS", xrefs_to(target)[:100])
|
||||
|
||||
print("QUERY_DONE")
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Recover the JSON element shape consumed by the array-root draft response."""
|
||||
|
||||
targets = (
|
||||
0x180138BD0, # helper called once per array element
|
||||
0x180150310, # array-root FutPurchaseDraftModeServerResponse parser
|
||||
)
|
||||
|
||||
seen = set()
|
||||
for target in targets:
|
||||
print("\n=== TARGET", hex(target), fname(target), "===")
|
||||
print(dec(target, 500))
|
||||
print("XREFS", xrefs_to(target)[:150])
|
||||
|
||||
# Include direct callees so small string/value accessors used by the helper
|
||||
# are visible without broad, noisy whole-program searching.
|
||||
for callee, name in callees(target):
|
||||
if callee in seen:
|
||||
continue
|
||||
seen.add(callee)
|
||||
print("\n--- CALLEE", hex(callee), name, "---")
|
||||
print(dec(callee, 250))
|
||||
|
||||
print("QUERY_DONE")
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Trace active draft-state enum literals and the current-state response consumers."""
|
||||
|
||||
for literal in ("DRAFTSQUAD_ON", "DRAFTSQUAD_OFF", "DRAFT_SQUAD", "squadState",
|
||||
"stateParam1", "stateParam2", "roundsInfo"):
|
||||
print("\n=== LITERAL", literal, "===")
|
||||
for hit in find_all(literal.encode() + b"\x00", (".rdata", ".data")):
|
||||
print("HIT", hex(hit), "XREFS", xrefs_to(hit)[:100])
|
||||
for _frm, _typ, _name, entry in xrefs_to(hit):
|
||||
print("\n--- XREF FUNCTION", hex(entry), fname(entry), "---")
|
||||
print(dec(entry, 500))
|
||||
|
||||
for target in (0x180147070,):
|
||||
print("\n=== STATE DESERIALIZER", hex(target), fname(target), "===")
|
||||
print(dec(target, 500))
|
||||
print("CALLERS", callers(target))
|
||||
|
||||
print("QUERY_DONE")
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Resolve draft-state atom IDs to their authoritative wire strings."""
|
||||
|
||||
ATOM_TABLE = 0x1802D2760
|
||||
|
||||
def atom_name(index):
|
||||
pointer = qword(ATOM_TABLE + index * 8)
|
||||
return rd_str(pointer, 96)
|
||||
|
||||
groups = {
|
||||
"squadState values": (0x1AC, 0x6A, 0x9C, 0x12C, 0x169, 0x225, 0x23E, 0x277, 0x278),
|
||||
"stateParam1 values": (0x169, 0x1AA, 0x22D),
|
||||
"entranceCriteria keys": (0x96, 0xDF, 0x241),
|
||||
"top-level keys": (0x108, 0x13B, 0x293, 0x2CD, 0x2D5, 0x2EE, 0x2EF),
|
||||
}
|
||||
|
||||
for group, indices in groups.items():
|
||||
print("\n===", group, "===")
|
||||
for index in indices:
|
||||
print(hex(index), repr(atom_name(index)))
|
||||
|
||||
print("QUERY_DONE")
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Resolve the concrete owner behind request+0x08 for the SBC category request.
|
||||
|
||||
q_md_sbc_9 proved generic slot +0x88 (0x1801631e0) invokes:
|
||||
owner = *(request + 8)
|
||||
owner.vtable[+0x18](owner, parsed_response, 0)
|
||||
|
||||
Work backwards from the category request constructor and its callers to identify who
|
||||
supplies request+8, then map candidate owner vtables and their +0x18 consumers.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
def show(a, label):
|
||||
f = func(a)
|
||||
print("\n=== %s %#x %s ===" % (label, a, f.getName() if f else "?"))
|
||||
print(dec(a))
|
||||
|
||||
ctor = 0x18017a7c0
|
||||
show(ctor, "category request constructor")
|
||||
print("\n=== ctor callers ===")
|
||||
for ent, name in callers(ctor):
|
||||
print(" %#x %s" % (ent, name))
|
||||
show(ent, "ctor caller")
|
||||
print("\n=== ctor xrefs ===")
|
||||
for frm, typ, name, ent in xrefs_to(ctor):
|
||||
print(" from=%#x type=%s fn=%s entry=%#x" % (frm, typ, name, ent))
|
||||
|
||||
# The request base constructor is usually visible as the first direct call in
|
||||
# the category constructor. Dump every direct callee so request+8 initialization
|
||||
# can be distinguished from URI/tag setup.
|
||||
print("\n=== constructor direct callees ===")
|
||||
for target, name in callees(ctor):
|
||||
print(" %#x %s" % (target, name))
|
||||
show(target, "ctor callee")
|
||||
|
||||
# Ghidra did not create a function at the traced +0x90 thunk. Print its raw
|
||||
# instructions and nearby containing-function identity without assuming a body.
|
||||
print("\n=== raw callback thunk at 0x180154830 ===")
|
||||
ad = addr(0x180154830)
|
||||
for _ in range(48):
|
||||
ins = listing.getInstructionAt(ad)
|
||||
if ins is None:
|
||||
print(" %s <not disassembled>" % ad)
|
||||
ad = ad.add(1)
|
||||
continue
|
||||
print(" %s %s" % (ad, ins))
|
||||
ad = ins.getNext().getAddress() if ins.getNext() else ad.add(ins.getLength())
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Trace the FUT-root constructor's third argument, inherited by every request at +8.
|
||||
|
||||
The category request lives at FUT root +0x4140 (qword index 0x828). Its base ctor
|
||||
stores the root constructor's param_3 at request+8, making that object the receiver
|
||||
of owner.vtable[+0x18](owner, parsed_response, 0).
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
root_ctor = 0x18010cdc0
|
||||
print("=== root ctor callers ===")
|
||||
for ent, name in callers(root_ctor):
|
||||
print("\n--- %#x %s ---" % (ent, name))
|
||||
print(dec(ent))
|
||||
|
||||
print("\n=== root ctor xrefs ===")
|
||||
for frm, typ, name, ent in xrefs_to(root_ctor):
|
||||
print(" from=%#x type=%s fn=%s entry=%#x" % (frm, typ, name, ent))
|
||||
if ent:
|
||||
print(dec(ent))
|
||||
|
||||
# Static singleton slot and root vtables provide adjacent factory/type metadata.
|
||||
for site in (0x1802e6398, 0x18021c2a0, 0x18021cda8, 0x18021cdb8):
|
||||
print("\n=== qwords around %#x ===" % site)
|
||||
for i in range(-8, 16):
|
||||
p = site + i * 8
|
||||
try:
|
||||
value = qword(p)
|
||||
except Exception:
|
||||
continue
|
||||
print(" [%#x] = %#x %s" % (p, value, fname(value)))
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Map the category success notifier already instrumented at 0x18017aa80.
|
||||
|
||||
The checkpoint hook can passively record ctx+0x88 and the +0x58..+0x60 handler
|
||||
vector. Establish where this notifier sits relative to request ownership transfer and
|
||||
whether it is the concrete receiver-side publication path we need to observe live.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
target = 0x18017aa80
|
||||
print("=== notifier 0x18017aa80 ===")
|
||||
print(dec(target))
|
||||
print("\n=== notifier callers ===")
|
||||
for ent, name in callers(target):
|
||||
print(" %#x %s" % (ent, name))
|
||||
print(dec(ent))
|
||||
print("\n=== notifier xrefs ===")
|
||||
for frm, typ, name, ent in xrefs_to(target):
|
||||
print(" from=%#x type=%s fn=%s entry=%#x" % (frm, typ, name, ent))
|
||||
|
||||
# Adjacent category request methods often expose the notifier through a vtable
|
||||
# or callback descriptor; inspect nearby functions and data references.
|
||||
for a in (0x18017aa80, 0x18017aaf0, 0x18017ab80, 0x18017b1c0):
|
||||
f = func(a)
|
||||
print("\n=== %#x %s ===" % (a, f.getName() if f else "?"))
|
||||
print(dec(a))
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Map the sole live category-notifier listener into CardsDLL.
|
||||
|
||||
Live capture 2026-08-07:
|
||||
listener object 0x4216ca48
|
||||
listener vtable 0x6ffffc20d6d0
|
||||
vtable +0x08 0x6ffffc1e577a
|
||||
CardsDLL slide 0x6ffe7c020000
|
||||
static method 0x1801c577a
|
||||
"""
|
||||
|
||||
TARGET = 0x1801C577A
|
||||
VTABLE = 0x1801ED6D0
|
||||
|
||||
print("=== live notifier listener method ===")
|
||||
target_function = func(TARGET)
|
||||
if target_function is None:
|
||||
print("no Ghidra function at %#x" % TARGET)
|
||||
print("raw PE decoding: jmp [0x1801e5200], imported CRT _purecall")
|
||||
else:
|
||||
print("containing function:", target_function.getName(),
|
||||
hex(int(target_function.getEntryPoint().getOffset())))
|
||||
print(dec(TARGET))
|
||||
|
||||
print("\n=== listener vtable ===")
|
||||
for slot, target, name in vtable(VTABLE, 12):
|
||||
print("%+#04x %#x %s" % (slot, target, name))
|
||||
|
||||
print("\n=== method callers/xrefs ===")
|
||||
print("callers:", callers(TARGET) if target_function is not None else [])
|
||||
for row in xrefs_to(TARGET):
|
||||
print(row)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Find concrete siblings of the live notifier listener's abstract vtable."""
|
||||
|
||||
import struct
|
||||
|
||||
VTABLE = 0x1801ED6D0
|
||||
DTOR = 0x180018EF0
|
||||
PURECALL_THUNK = 0x1801C577A
|
||||
|
||||
print("=== exact vtable references ===")
|
||||
for row in xrefs_to(VTABLE):
|
||||
print(row)
|
||||
|
||||
print("\n=== vtables sharing the live listener destructor ===")
|
||||
for hit in find_all(struct.pack("<Q", DTOR), blocks=(".rdata", ".data")):
|
||||
try:
|
||||
slots = [qword(hit + i * 8) for i in range(12)]
|
||||
except Exception:
|
||||
continue
|
||||
# Require the same broad interface shape: destructor in slot 0 and at least
|
||||
# one CardsDLL code pointer after it. This filters incidental data matches.
|
||||
if slots[0] != DTOR or not any(0x180000000 <= x < 0x1801E5000 for x in slots[1:]):
|
||||
continue
|
||||
print("vtable=%#x slot8=%#x %s" %
|
||||
(hit, slots[1], "PURE" if slots[1] == PURECALL_THUNK else "CONCRETE"))
|
||||
for i, target in enumerate(slots):
|
||||
print(" +%#04x %#x %s" % (i * 8, target, fname(target)))
|
||||
refs_here = xrefs_to(hit)
|
||||
if refs_here:
|
||||
print(" refs:", refs_here)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Locate event 0x753c users and category-listener registration/removal paths."""
|
||||
|
||||
import struct
|
||||
|
||||
EVENT = 0x753C
|
||||
NOTIFIER = 0x18017AA80
|
||||
|
||||
print("=== immediate/data occurrences of event 0x753c ===")
|
||||
seen = set()
|
||||
for hit in find_all(struct.pack("<I", EVENT)):
|
||||
print("hit", hex(hit))
|
||||
owner = func(hit)
|
||||
if owner is not None:
|
||||
entry = int(owner.getEntryPoint().getOffset())
|
||||
print(" containing", hex(entry), owner.getName())
|
||||
seen.add(entry)
|
||||
for row in xrefs_to(hit):
|
||||
print(" ", row)
|
||||
if row[3]:
|
||||
seen.add(row[3])
|
||||
|
||||
print("\n=== decompile functions referencing event literal ===")
|
||||
for entry in sorted(seen):
|
||||
print("\n--- %#x %s ---" % (entry, fname(entry)))
|
||||
print(dec(entry))
|
||||
|
||||
print("\n=== category request ctor/dtor and notifier neighborhood ===")
|
||||
for target in (0x18017A7C0, 0x18017AA10, NOTIFIER, 0x18017AAF0, 0x18017B1C0):
|
||||
print("\n--- %#x %s ---" % (target, fname(target)))
|
||||
print("callers", callers(target))
|
||||
print("xrefs", xrefs_to(target))
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Resolve the SBC controller and its 0x756c refresh registration/dispatch contract."""
|
||||
|
||||
TARGETS = (
|
||||
(0x1800B5260, "SBC controller allocation/ctor neighborhood"),
|
||||
(0x1800B53F0, "SBC controller constructor"),
|
||||
(0x1800B5760, "SBC service/controller constructor"),
|
||||
(0x1800B5E00, "SBC tile builder"),
|
||||
(0x1801A4A70, "event registration"),
|
||||
(0x1801A4CD0, "event dispatch"),
|
||||
)
|
||||
|
||||
for target, label in TARGETS:
|
||||
print("\n=== %s %#x %s ===" % (label, target, fname(target)))
|
||||
print(dec(target))
|
||||
print("callers", callers(target))
|
||||
print("xrefs", xrefs_to(target))
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Decompile the concrete SBC controller event-listener vtable."""
|
||||
|
||||
VTABLE = 0x18020A888
|
||||
print("=== SBC controller event subobject vtable ===")
|
||||
for off in range(0, 0x80, 8):
|
||||
target = qword(VTABLE + off)
|
||||
print("\nslot +%#x -> %#x %s" % (off, target, fname(target)))
|
||||
if 0x180001000 <= target < 0x180200000:
|
||||
print(dec(target, 180))
|
||||
print("callers", callers(target)[:30])
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Follow the SBC category-completion continuation registered by event 0x753c."""
|
||||
|
||||
for target in (0x1800B8950, 0x1800B89D0, 0x1800B8C30, 0x1800BA460, 0x1800B7090):
|
||||
print("\n=== %#x %s ===" % (target, fname(target)))
|
||||
print(dec(target, 300))
|
||||
print("callers", callers(target)[:50])
|
||||
print("xrefs", xrefs_to(target)[:50])
|
||||
@@ -0,0 +1,10 @@
|
||||
"""Resolve manager +0xe0 used to schedule the ServerErrSets continuation."""
|
||||
|
||||
for target in (0x180009C80, 0x1800D7170, 0x180154830, 0x1801631E0):
|
||||
print("\n=== %#x %s ===" % (target, fname(target)))
|
||||
print(dec(target, 300))
|
||||
print("xrefs", xrefs_to(target)[:80])
|
||||
|
||||
print("\n=== candidate manager vtables referencing category request callbacks ===")
|
||||
for target in (0x1800B8950, 0x18017AA80, 0x18017B2B0):
|
||||
print(hex(target), xrefs_to(target)[:100])
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Find completion callbacks that test the same status field at response+0x1c."""
|
||||
|
||||
patterns = (
|
||||
bytes.fromhex("83 7a 1c 00"), # cmp dword ptr [rdx+1c],0
|
||||
bytes.fromhex("83 79 1c 00"), # cmp dword ptr [rcx+1c],0
|
||||
bytes.fromhex("83 78 1c 00"), # cmp dword ptr [rax+1c],0
|
||||
)
|
||||
|
||||
seen = set()
|
||||
for pattern in patterns:
|
||||
print("\npattern", pattern.hex())
|
||||
for hit in find_all(pattern):
|
||||
f = func(hit)
|
||||
if f is None:
|
||||
continue
|
||||
entry = int(f.getEntryPoint().getOffset())
|
||||
if entry in seen:
|
||||
continue
|
||||
seen.add(entry)
|
||||
print("\n=== hit %#x function %#x %s ===" % (hit, entry, f.getName()))
|
||||
print(dec(f, 180)[:5000])
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Map the live category response object's vtable and status-bearing base class."""
|
||||
|
||||
VTABLE = 0x18022E5B0
|
||||
print("=== live category response vtable ===")
|
||||
print("vtable xrefs", xrefs_to(VTABLE)[:100])
|
||||
for off in range(0, 0x100, 8):
|
||||
target = qword(VTABLE + off)
|
||||
print("slot +%#x -> %#x %s" % (off, target, fname(target)))
|
||||
if 0x180001000 <= target < 0x180200000 and off < 0x60:
|
||||
print(dec(target, 120)[:3000])
|
||||
|
||||
print("\n=== direct references to vtable entries/address ===")
|
||||
for a in range(VTABLE - 0x20, VTABLE + 0x20, 8):
|
||||
print(hex(a), xrefs_to(a)[:40])
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Find static assignments/usages of completion status 999 (0x3e7)."""
|
||||
|
||||
patterns = []
|
||||
for modrm in (0x40, 0x41, 0x42, 0x43, 0x46, 0x47, 0x80, 0x81, 0x82, 0x83, 0x86, 0x87):
|
||||
patterns.append(bytes((0xC7, modrm, 0x1C, 0xE7, 0x03, 0x00, 0x00)))
|
||||
patterns.extend((bytes.fromhex("b8 e7 03 00 00"), bytes.fromhex("b9 e7 03 00 00"),
|
||||
bytes.fromhex("ba e7 03 00 00"), bytes.fromhex("41 b8 e7 03 00 00")))
|
||||
|
||||
seen = set()
|
||||
for pattern in patterns:
|
||||
for hit in find_all(pattern):
|
||||
f = func(hit)
|
||||
entry = int(f.getEntryPoint().getOffset()) if f else 0
|
||||
key = (entry, hit)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
print("\n=== pattern %s hit %#x function %#x %s ===" %
|
||||
(pattern.hex(), hit, entry, f.getName() if f else "?"))
|
||||
if f:
|
||||
print(dec(f, 240)[:10000])
|
||||
print("callers", callers(f)[:80])
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Trace callers of the HTTP/FUT status mapper returning 999."""
|
||||
|
||||
for target in (0x1801844C0, 0x180163120, 0x180165050, 0x180165CC0,
|
||||
0x18016C060, 0x180184A90):
|
||||
print("\n=== %#x %s ===" % (target, fname(target)))
|
||||
print(dec(target, 300)[:18000])
|
||||
print("callers", callers(target)[:100])
|
||||
print("xrefs", xrefs_to(target)[:100])
|
||||
@@ -0,0 +1,26 @@
|
||||
"""Decompile the transport-result conversion and SBC response base methods."""
|
||||
|
||||
|
||||
TARGETS = (
|
||||
0x180184420,
|
||||
0x1801844C0,
|
||||
0x180184A90,
|
||||
0x180163120,
|
||||
0x1801631E0,
|
||||
0x180165050,
|
||||
0x180165CC0,
|
||||
0x18016C060,
|
||||
0x18016C110,
|
||||
0x18016C950,
|
||||
0x18016CA40,
|
||||
0x18016CAC0,
|
||||
0x18016CB20,
|
||||
0x18016CB90,
|
||||
0x18016CBE0,
|
||||
0x18016CCA0,
|
||||
0x18016D230,
|
||||
)
|
||||
|
||||
for address in TARGETS:
|
||||
print("\n===== %#x %s =====" % (address, fname(address)))
|
||||
print(dec(address, 60))
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Enumerate CardsDLL instructions that write a dword-like value to object +0x1c.
|
||||
|
||||
This is intentionally a read-only listing query. It finds explicit memory writes whose
|
||||
rendered destination operand contains displacement 0x1c, then groups them by function.
|
||||
"""
|
||||
|
||||
listing = prog.getListing()
|
||||
seen = set()
|
||||
|
||||
for insn in listing.getInstructions(True):
|
||||
text = insn.toString().lower()
|
||||
if "0x1c" not in text and "+1ch" not in text:
|
||||
continue
|
||||
refs = insn.getReferencesFrom()
|
||||
has_write = any(ref.getReferenceType().isWrite() for ref in refs)
|
||||
# Register-relative memory writes do not always produce a Ghidra reference, so retain
|
||||
# the common write mnemonics and require the first rendered operand to contain +0x1c.
|
||||
mnemonic = insn.getMnemonicString().lower()
|
||||
dst = insn.getDefaultOperandRepresentation(0).lower()
|
||||
if "0x1c" not in dst and "+1ch" not in dst:
|
||||
continue
|
||||
if not has_write and mnemonic not in ("mov", "movzx", "and", "or", "xor", "inc", "dec"):
|
||||
continue
|
||||
owner = func(int(insn.getAddress().getOffset()))
|
||||
entry = int(owner.getEntryPoint().getOffset()) if owner else 0
|
||||
key = (entry, int(insn.getAddress().getOffset()))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
print("%#x function=%#x %s :: %s" %
|
||||
(key[1], entry, owner.getName() if owner else "?", insn.toString()))
|
||||
@@ -0,0 +1,7 @@
|
||||
"""Inspect the two additional CardsDLL functions with explicit dword writes to +0x1c."""
|
||||
|
||||
for target in (0x180171970, 0x1801790A0):
|
||||
print("\n===== %#x %s =====" % (target, fname(target)))
|
||||
print(dec(target, 180))
|
||||
print("callers", callers(target)[:100])
|
||||
print("xrefs", xrefs_to(target)[:100])
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Continue the SBC response handoff analysis after the 2026-08-07 passive trace.
|
||||
|
||||
Proven live boundary:
|
||||
request +0x80 factory -> response 0x18022e5b0
|
||||
response +0x08 -> 0x18017b2b0 returns true
|
||||
request +0x90 -> parsed response callback returns normally
|
||||
request +0x88 -> ownership transfer returns normally
|
||||
|
||||
The next unknown is the receiving owner's virtual +0x18 consumer called by
|
||||
0x1801631e0. Recover the concrete receiver, its vtable, and downstream publication.
|
||||
"""
|
||||
import traceback
|
||||
|
||||
try:
|
||||
def dump_function(a, label):
|
||||
f = func(a)
|
||||
print("\n=== %s @%#x (%s) ===" % (label, a, f.getName() if f else "?"))
|
||||
if f:
|
||||
print("entry=%s body=%s" % (f.getEntryPoint(), f.getBody()))
|
||||
print(dec(a))
|
||||
|
||||
def dump_instructions(a, before=0, count=80):
|
||||
f = func(a)
|
||||
print("\n=== instructions around %#x ===" % a)
|
||||
if not f:
|
||||
return
|
||||
rows = []
|
||||
for ad in f.getBody().getAddresses(True):
|
||||
ins = listing.getInstructionAt(ad)
|
||||
if ins:
|
||||
rows.append(ins)
|
||||
pivot = next((i for i, ins in enumerate(rows)
|
||||
if int(ins.getAddress().getOffset()) >= a), 0)
|
||||
for ins in rows[max(0, pivot-before):pivot+count]:
|
||||
print(" %s %s" % (ins.getAddress(), ins))
|
||||
|
||||
dump_function(0x1801631e0, "post-request ownership handoff / owner consumer")
|
||||
dump_instructions(0x1801631e0, count=120)
|
||||
|
||||
print("\n=== callers/xrefs of 0x1801631e0 ===")
|
||||
for ent, name in callers(0x1801631e0):
|
||||
print(" caller %#x %s" % (ent, name))
|
||||
print(dec(ent))
|
||||
for frm, typ, name, ent in xrefs_to(0x1801631e0):
|
||||
print(" xref from=%#x type=%s fn=%s entry=%#x" %
|
||||
(frm, typ, name, ent))
|
||||
|
||||
request_vtable = 0x18022e5c0
|
||||
print("\n=== category request vtable %#x ===" % request_vtable)
|
||||
for off, target, name in vtable(request_vtable, 40):
|
||||
print(" +%#04x -> %#x %s" % (off, target, name))
|
||||
|
||||
for slot, label in ((0x80, "typed factory"),
|
||||
(0x88, "ownership transfer"),
|
||||
(0x90, "completion callback")):
|
||||
target = qword(request_vtable + slot)
|
||||
dump_function(target, "request %s slot +%#x" % (label, slot))
|
||||
dump_instructions(target, count=100)
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Find indirect calls to service-interface slot +0xe0 and compare contracts."""
|
||||
|
||||
PATTERNS = (
|
||||
bytes.fromhex("ff 90 e0 00 00 00"),
|
||||
bytes.fromhex("ff 91 e0 00 00 00"),
|
||||
bytes.fromhex("ff 92 e0 00 00 00"),
|
||||
bytes.fromhex("ff 93 e0 00 00 00"),
|
||||
bytes.fromhex("ff 96 e0 00 00 00"),
|
||||
bytes.fromhex("ff 97 e0 00 00 00"),
|
||||
bytes.fromhex("41 ff 90 e0 00 00 00"),
|
||||
bytes.fromhex("41 ff 91 e0 00 00 00"),
|
||||
bytes.fromhex("41 ff 92 e0 00 00 00"),
|
||||
bytes.fromhex("41 ff 93 e0 00 00 00"),
|
||||
)
|
||||
|
||||
seen = set()
|
||||
for pattern in PATTERNS:
|
||||
for hit in find_all(pattern, blocks=(".text",)):
|
||||
owner = func(hit)
|
||||
if owner is None:
|
||||
continue
|
||||
entry = int(owner.getEntryPoint().getOffset())
|
||||
if entry in seen:
|
||||
continue
|
||||
seen.add(entry)
|
||||
print("\n===== call %#x function %#x %s =====" %
|
||||
(hit, entry, owner.getName()))
|
||||
print(dec(owner, 120)[:12000])
|
||||
print("callees", callees(owner)[:80])
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Map the FIFA response-registry primitives surrounding state-3 completion."""
|
||||
|
||||
TARGETS = (
|
||||
0x145336C50,
|
||||
0x145336E60,
|
||||
0x1453370B0,
|
||||
0x1453371B0,
|
||||
0x145337B20,
|
||||
0x1453388A0,
|
||||
0x145338950,
|
||||
0x145339650,
|
||||
0x1453396A0,
|
||||
0x145339B10,
|
||||
0x145374E10,
|
||||
0x145375070,
|
||||
0x145376200,
|
||||
0x145376270,
|
||||
0x1453762E0,
|
||||
0x145376360,
|
||||
)
|
||||
|
||||
for target in TARGETS:
|
||||
print("\n===== %#x %s =====" % (target, fname(target)))
|
||||
print(dec(target, 180)[:16000])
|
||||
print("callers", callers(target)[:120])
|
||||
print("callees", callees(target)[:120])
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Analyze the unpacked FIFA17 SBC completion route offline."""
|
||||
|
||||
TARGETS = (
|
||||
(0x146B805B0, "SBC request owner thunk"),
|
||||
(0x145374E80, "generic request-state dispatcher"),
|
||||
(0x145376270, "state-3 completion handler"),
|
||||
(0x1453388A0, "response registry lookup (manager mode 1)"),
|
||||
(0x145338950, "response registry lookup (manager mode 2)"),
|
||||
(0x146162F50, "completion broadcast invoked on lookup miss"),
|
||||
)
|
||||
|
||||
for target, label in TARGETS:
|
||||
print("\n=== %s %#x %s ===" % (label, target, fname(target)))
|
||||
print(dec(target, 300))
|
||||
print("callers", callers(target)[:100])
|
||||
print("xrefs", xrefs_to(target)[:100])
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Resolve the two data tables that reference the SBC request-owner thunk."""
|
||||
|
||||
THUNK = 0x146B805B0
|
||||
REFERENCES = (0x14366D3E0, 0x14381B5D0)
|
||||
|
||||
print("thunk bytes", read_bytes(THUNK, 32).hex(" "))
|
||||
|
||||
for reference in REFERENCES:
|
||||
print("\n=== reference %#x ===" % reference)
|
||||
print("raw", read_bytes(reference - 0x40, 0x90).hex(" "))
|
||||
for slot in range(reference - 0x40, reference + 0x48, 8):
|
||||
target = qword(slot)
|
||||
print("%#x rel=%+#x -> %#x %s xrefs=%s" %
|
||||
(slot, slot - reference, target, fname(target), xrefs_to(slot)[:8]))
|
||||
if func(target) is not None:
|
||||
print(dec(target, 60)[:5000])
|
||||
|
||||
Reference in New Issue
Block a user