43557989f5
The subsystem that was fully greyed-out this morning now lists a card on the transfer
market: price screen, Submit, "your item is now up for trade", TRANSFER LIST 0/100,
auctionCount 1, and STORE.listings() holds the auction. Every step verified at the
instruction level first, then confirmed live. Three fixes, all behind flags, all off by
default until this run proved them.
1. WE WERE BANNING OUR OWN TRADING. userInfo.feature (atom 0x11c) is a RESTRICTION map,
not a grant; we sent feature={"trade":true}, which is a trade BAN. Verified in
q_feature_trade.py: FUN_18013ec10 parses feature/trade into userInfo+0x17c, and at
the massinfo END_OBJECT the client runs
cmp byte [rsi+0x17c],0 / jz skip / mov dword [rsi+0x50],0
feeding applier 0x18011dc91 -> IS_TRADING_ENABLED (model+0x1fd2e) = 0. It runs LAST
and unconditionally, which is why the gate read 0 all day regardless of /settings or
the Blaze config store. FUT_TRADING sends feature={} instead. Live: gate flipped
0 -> 1 on UT re-entry (model rebuilt, pointer changed, byte read 1).
2. TRANSFER LIST CAPACITY 0/0. pileSizeClientData (massinfo atom 0x227, parser
0x18013adb0) is the capacity, NOT the "MY CLUB counter" the old comment claimed.
Verified in q_pilesize_keys.py: exactly two storing arms, key 2 -> model+0x1fd1c
(TRADE_PILE_SIZE) and key 4 -> +0x1fd20 (watch list), every other key SKIP'd. The old
code would have sprayed the 246 club count into the capacity. FUT_PILESIZES sends
key 2 = 100, key 4 = 50. Live: capacity read 0 -> 100, header showed 0/100.
3. THE PRICE SCREEN FROZE THE CLIENT. GET marketdata/pricelimits was answered with an
OBJECT {minPrice,maxPrice}; the deser 0x180163ee0 reads a BARE TOP-LEVEL ARRAY
(root loop while tok != 0xd), so object-where-array desynced the SAX reader into the
0x1801c7f1a busy loop (confirmed live: utime climbing 227 ticks/s, core pinned).
Verified in q_pricelimits.py: element fields defId 0xcf, maxPrice 0x1c2, minPrice
0x1ca, all scalar ints. marketdata_route now returns a bare array, one element per
requested defId. Live: price screen opened and Submit succeeded.
Corrected along the way, all now in the code: two prior "trading root causes" from
earlier today were wrong (the Blaze IS_TRADING_ENABLED keys are output-only names, and
the applier is a virtual method at vtable+0x988, not unreachable). Those refutations are
recorded in blaze_responder_v3b.py and the doc.
Also lands the transfer-market recon doc (plan-2026-08-06-transfer-market.md) and the
market Ghidra query set.
Server-authoritative economy note: the 5% transfer fee and the price bands (currently a
150..15000 placeholder per defId) are not yet real; that is refinement, not a freeze.
The live-auction market SCREEN ("List on Transfer Market" browse) is a separate surface
still to do (P4 auction-counts route, P5 empty market bodies).
Live: 439 contract checks pass. Card listed and persisted, auctionCount 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
91 lines
3.8 KiB
Python
91 lines
3.8 KiB
Python
"""ADVERSARIAL VERIFICATION BATCH 1 (dim4 + dim5).
|
|
|
|
HYPOTHESES UNDER ATTACK
|
|
H1 (dim5 f5/f7): the publisher FUN_18006cc60 maps model vtable slots to IS_* names,
|
|
and IS_TRADING_ENABLED (0x1801fc118) has exactly ONE rip-relative reference in
|
|
.text (the lea), i.e. the name is output-only.
|
|
CONTROL: run the same rip-relative scanner against a literal that IS known to be
|
|
compared, e.g. one of the ISOfferTrade error strings 0x180228f20, which must show
|
|
up in a *different* instruction context, and against IS_STORE_ENABLED.
|
|
H2 (dim5 f5 positive control): IS_STORE_ENABLED's accessor (vt+0x280) - what does it
|
|
actually compute? If it is a live-evaluable expression we can compare STORE vs
|
|
TRADING under the same publish mechanism.
|
|
H3 (dim4 f2): FutGetSuggestedPricing deser 0x180163ee0 top-level token is
|
|
START_ARRAY (loop terminates on 0xd) - CONTROL FUN_180165df0 (ISStart) must
|
|
terminate on 10.
|
|
H4 (dim4 f4): 0x1801642c0 is `return 1;`.
|
|
H5 (dim4 f6): tradeState table 0x180229e40 / bidState ladder FUN_180166380.
|
|
H6 (dim4 f9): IS_MAX_AUCTIONS publisher FUN_1800377c0 + GetAuctionCount deser
|
|
0x180163770.
|
|
H7 (dim4 f8): error mapper FUN_1801844c0.
|
|
Everything printed IN FULL with len(src).
|
|
"""
|
|
import traceback, struct
|
|
|
|
def full(tag, va):
|
|
try:
|
|
s = dec(va)
|
|
print("\n----- %s %#x len=%d -----" % (tag, va, len(s)))
|
|
print(s)
|
|
except Exception:
|
|
traceback.print_exc()
|
|
|
|
try:
|
|
print("### H1: publisher FUN_18006cc60")
|
|
full("publisher", 0x18006cc60)
|
|
|
|
print("\n### model vtable slots")
|
|
VT = 0x18021c2a0
|
|
for off in (0x270, 0x280, 0x2b0, 0x988, 0x998, 0xa58, 0xa60, 0x130, 0x5b8, 0xa00):
|
|
t = qword(VT + off)
|
|
print(" vt+%#05x -> %#x %s" % (off, t, fname(t) if 'fname' in dir() else ''))
|
|
full("vt+0x280 IS_STORE_ENABLED accessor", qword(VT + 0x280))
|
|
full("vt+0x270 IS_TRADING_ENABLED accessor", qword(VT + 0x270))
|
|
full("vt+0xa58 TRADE_PILE_SIZE accessor", qword(VT + 0xa58))
|
|
|
|
print("\n### H1 rip-relative reference scan, form independent")
|
|
# Scan .text for any 4-byte little-endian rel32 whose target == literal VA,
|
|
# for every instruction end position. This catches lea/mov/cmp/push equally.
|
|
tblk = None
|
|
for b in mem.getBlocks():
|
|
if b.getName() == ".text":
|
|
tblk = b
|
|
TS = int(tblk.getStart().getOffset()); TE = int(tblk.getEnd().getOffset())
|
|
text = read_bytes(TS, TE - TS + 1)
|
|
print(" .text %#x..%#x len=%d" % (TS, TE, len(text)))
|
|
|
|
def ripscan(target, label):
|
|
hits = []
|
|
for i in range(0, len(text) - 4):
|
|
rel = struct.unpack_from('<i', text, i)[0]
|
|
# instruction end = TS + i + 4 (rel32 is the last field of the insn)
|
|
if TS + i + 4 + rel == target:
|
|
hits.append(TS + i)
|
|
print(" %-34s target %#x : %d candidate rel32 sites" % (label, target, len(hits)))
|
|
for h in hits[:20]:
|
|
print(" at %#x bytes %s fn %s" % (h - 3, text[h - 6:h + 6].hex(),
|
|
(fm.getFunctionContaining(addr(h)) or "?")))
|
|
return hits
|
|
|
|
lits = {}
|
|
for nm in (b"IS_TRADING_ENABLED\x00", b"IS_STORE_ENABLED\x00",
|
|
b"IS_DRAFT_MODE_ENABLED\x00", b"TRADE_PILE_SIZE\x00",
|
|
b"IS_MAX_AUCTIONS\x00", b"NUM_MAX_AUCTIONS\x00",
|
|
b"You are not allowed to bid on this trade\x00"):
|
|
f = find_all(nm, blocks=(".rdata", ".data", ".text"))
|
|
lits[nm] = f
|
|
print(" literal %-45r -> %s" % (nm[:40], [hex(x) for x in f]))
|
|
for nm, f in lits.items():
|
|
for a in f:
|
|
ripscan(a, nm[:30].decode(errors='replace'))
|
|
|
|
print("\n### H3 pricelimits vs ISStart control")
|
|
full("FutGetSuggestedPricing deser", 0x180163ee0)
|
|
full("FutISStart deser CONTROL", 0x180165df0)
|
|
|
|
print("\n### H4 generic ack deser")
|
|
full("ack deser", 0x1801642c0)
|
|
|
|
except Exception:
|
|
traceback.print_exc()
|