Files
OpenFUT/fifa17-recon/tools/ghidra_queries/q_md_gates_2.py
T

94 lines
4.2 KiB
Python

"""DIMENSION 2 Q1/Q3: slot->disp resolution + READER search per gate byte.
HYPOTHESIS: each publisher slot is an accessor stub `0f b6 81 <disp32> c3`
(movzx eax,byte[rcx+disp]; ret) at model vtable 0x18021c2a0. For the refusing
modes (season/draft/tournament), the ONLY reader of the gate byte is the publisher
FUN_18006cc60, which hands the value to the script layer -- i.e. no native mode gate.
CONTROL: slot 0x270 must decode to disp 0x1fd2e (trading), already proven by two
prior docs. Reader scan must find FUN_18011dc50 (applier, WRITES 0x1fd2e) and
FUN_1801a7260 (TO_TRADE_PILE predicate, READS 0x1fd2e) among the disp-32 hits for
0x1fd2e -- both known, so if either is missing the scan form is wrong.
"""
import traceback
try:
MODEL_VT = 0x18021c2a0
slots = {
0x270: "IS_TRADING_ENABLED",
0x280: "IS_STORE_ENABLED",
0x2b0: "IS_FRIENDLY_SEASON_ENABLED",
0x2b8: "IS_TOURNAMENT_QUIT_ENABLED",
0x2c0: "IS_PROCESSING_STATE_ENABLED",
0x2c8: "IS_DRAFT_MODE_ENABLED",
0x2d8: "IS_STORY_MODE_REWARD_ENABLED",
0x2f0: "IS_RETURNING_USER_REWARDS_SCREEN_ENABLED",
}
print("=" * 70)
print("SLOT -> accessor -> displacement (model offset)")
print("=" * 70)
disp_by_name = {}
for slot in sorted(slots):
tgt = qword(MODEL_VT + slot)
stub = read_bytes(tgt, 8)
disp = None
# 0f b6 81 <disp32> c3 -> movzx eax, byte [rcx+disp32] ; ret
if stub[0:3] == b"\x0f\xb6\x81" and stub[7] == 0xc3:
disp = int.from_bytes(stub[3:7], "little")
# 8b 81 <disp32> c3 -> mov eax, [rcx+disp32] ; ret (int getter, 4-byte)
elif stub[0:2] == b"\x8b\x81" and stub[6] == 0xc3:
disp = int.from_bytes(stub[2:6], "little")
name = slots[slot]
disp_by_name[name] = disp
print("slot +%#05x %-42s -> %#011x stub=%s disp=%s"
% (slot, name, tgt, stub.hex(),
("%#x" % disp) if disp is not None else "??"))
print()
print("=" * 70)
print("READERS: .text hits for each displacement (raw disp32 LE, form-agnostic)")
print("catches movzx/mov/cmp/lea/setcc in every encoding")
print("=" * 70)
for name, disp in disp_by_name.items():
if disp is None:
continue
pat = disp.to_bytes(4, "little")
hits = find_all(pat, blocks=(".text",))
print("\n%-42s disp %#x (%d hit(s))" % (name, disp, len(hits)))
for h in hits:
f = fm.getFunctionContaining(addr(h))
fn = f.getName() if f else "?"
ent = int(f.getEntryPoint().getOffset()) if f else 0
ins = listing.getInstructionAt(addr(h - 3)) or listing.getInstructionAt(addr(h - 2)) or listing.getInstructionAt(addr(h))
print(" %#011x in %-16s (%#x) ins~ %s"
% (h, fn, ent, str(ins) if ins else "?"))
print()
print("=" * 70)
print("READERS via vtable slot call: .text scan for call [reg+slot] (ff /2 disp32)")
print("=" * 70)
# FF /2 with mod=10 (disp32): modrm 0x90..0x97 (rax..rdi), 0x94 needs SIB
call_modrm = [0x90, 0x91, 0x92, 0x93, 0x95, 0x96, 0x97]
for slot in sorted(slots):
pat_disp = slot.to_bytes(4, "little")
found = []
for mrm in call_modrm:
pat = bytes([0xff, mrm]) + pat_disp
for h in find_all(pat, blocks=(".text",)):
f = fm.getFunctionContaining(addr(h))
found.append((h, f.getName() if f else "?",
int(f.getEntryPoint().getOffset()) if f else 0))
# also REX.W/B variants (41 ff /2, 48/49 not valid for call reg-indirect but include 41)
for rex in (0x41,):
for mrm in [0x90, 0x91, 0x92, 0x93, 0x95, 0x96, 0x97]:
pat = bytes([rex, 0xff, mrm]) + pat_disp
for h in find_all(pat, blocks=(".text",)):
f = fm.getFunctionContaining(addr(h))
found.append((h, f.getName() if f else "?",
int(f.getEntryPoint().getOffset()) if f else 0))
print("\nslot +%#05x %-42s (%d call-site(s))" % (slot, slots[slot], len(found)))
for h, fn, ent in found:
print(" %#011x in %-16s (%#x)" % (h, fn, ent))
except Exception:
traceback.print_exc()