94 lines
4.2 KiB
Python
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()
|