fb38ee6087
Read the client's COMPLETE UTAS route surface out of CardsDLL's .rdata in the
running process (new tools/url_template_probe.py) and probed every one against
staging, where the Python upstream is deliberately dead so anything the Rust host
does not own answers 502 instead of being silently proxied.
That found five routes whose handlers already existed and were dead code because
`classify` never produced their Route -- the same defect as `season/list` and
`watchList`, whose fix comments are still in the file. This is the third and
fourth time:
captcha -> handle_static_ack, which already returns the oracle's exact
{encodedImg,sequence,sizeBeforeEncode}
tfa -> handle_static_ack, {}
livemessage -> handle_static_ack, {}
activeMessage -> handle_static_ack, {}
tournament/user-> FeatureOffEmpty, {} == the oracle with FUT_MODES off
(tools/utas_server.py:1504); the client builds this literal
at CardsDLL 0x18021e540 and the bare `tournament` arm never
matched it
Route's own doc comment already claimed the first four as "Rust-owned
UNCONDITIONAL", so the documentation was wrong rather than the intent. All five
are byte-identical to the oracle, so claiming them is parity, not new behaviour.
Invisible in production because the upstream answers there.
Two tests pin the vocabularies so a handler cannot go unreachable a fifth time;
both are mutation-checked (removing the captcha arm fails the first).
Also documents the surface in docs/CLIENT_ROUTE_SURFACE.md, including the trap
that bit me repeatedly: an .rdata literal is a FRAGMENT, not a callable path.
`clientdata`, `purchasegroup`, `sbs/challenges`, `squadBuildingSets`, `club/items`
and `item` all looked unserved and are not. Only `squad/mode` is genuinely
unserved, and correctly so -- it is Draft-only, which is out of scope.
L5 finding: there is NO consumable-apply route anywhere in the binary. The only
owned-item mutations the client can express are PUT item (move/pile), DELETE
item/<id> and POST delete/item (quick sell), and PUT squad. So applying a
consumable is not a dedicated endpoint; L5/L6 must be pursued by capturing the
PUT item payload, not by implementing a route that does not exist.
Host 123 lib + 45 host_test, fmt and clippy clean. tournament/user, livemessage
and activeMessage verified 200 on staging (were 502).
103 lines
3.4 KiB
Python
Executable File
103 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""Enumerate every UTAS URL template CardsDLL can build, from live memory.
|
|
|
|
READ-ONLY: /proc/PID/mem opened 'rb'. No write path in this file.
|
|
|
|
WHY
|
|
---
|
|
Support level L5 ("apply endpoint") for consumables was recorded as unreversed,
|
|
with an earlier note claiming there is "no training/position/chemistry/
|
|
manager-league endpoint at all" and that the only owned-item mutations upstream
|
|
are quick sell and move/pile. That claim is load-bearing -- if true, applying a
|
|
consumable is not a server route at all and L5/L6 cannot be implemented as one --
|
|
so it deserves to be checked against the binary rather than inherited.
|
|
|
|
This scans CardsDLL's .rdata for route-shaped strings and prints them, so the
|
|
full reachable surface can be read at once.
|
|
|
|
Positive control: known-live routes MUST appear (e.g. a 'item' path and a
|
|
'club' path). If the control is empty the region is wrong, not the game.
|
|
|
|
Usage:
|
|
python3 url_template_probe.py # route-shaped strings
|
|
python3 url_template_probe.py --all # every printable string >= 6 chars
|
|
python3 url_template_probe.py --grep pat # substring filter (case-insensitive)
|
|
"""
|
|
import argparse
|
|
import re
|
|
import sys
|
|
|
|
import watch_club_model as W
|
|
|
|
RDATA_LO, RDATA_HI = 0x1801E5000, 0x18028A000
|
|
DATA_LO, DATA_HI = 0x18028A000, 0x1802F0000
|
|
|
|
# Route-ish: contains a slash and no spaces, or looks like a UTAS path fragment.
|
|
ROUTE_HINTS = ("ut/", "game/", "item", "club", "squad", "purchase", "consumable",
|
|
"apply", "training", "position", "chemistry", "contract",
|
|
"fitness", "healing", "playstyle", "manager", "pile", "delete",
|
|
"transfer", "market", "auction", "sbs", "pack", "store")
|
|
|
|
PRINTABLE = re.compile(rb"[\x20-\x7e]{6,}")
|
|
|
|
|
|
def strings(mem, lo, hi):
|
|
buf, bad = mem.read_pages(W_live(lo), hi - lo)
|
|
if not buf:
|
|
return [], bad
|
|
out = []
|
|
for m in PRINTABLE.finditer(bytes(buf)):
|
|
out.append((lo + m.start(), m.group().decode("ascii")))
|
|
return out, bad
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--all", action="store_true")
|
|
ap.add_argument("--grep")
|
|
a = ap.parse_args()
|
|
|
|
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 yet." % (pid, W.DLL))
|
|
return 1
|
|
mem = W.Mem(pid)
|
|
|
|
global W_live
|
|
W_live = lambda i: base + (i - W.IMG_BASE)
|
|
|
|
print("pid=%d CardsDLL live base %#x" % (pid, base))
|
|
found = []
|
|
for lo, hi, name in ((RDATA_LO, RDATA_HI, ".rdata"), (DATA_LO, DATA_HI, ".data")):
|
|
ss, bad = strings(mem, lo, hi)
|
|
print(" %s: %d strings (%d bad pages)" % (name, len(ss), len(bad)))
|
|
found.extend(ss)
|
|
|
|
if a.grep:
|
|
pat = a.grep.lower()
|
|
sel = [(va, s) for va, s in found if pat in s.lower()]
|
|
elif a.all:
|
|
sel = found
|
|
else:
|
|
sel = [(va, s) for va, s in found
|
|
if "/" in s and " " not in s
|
|
and any(h in s.lower() for h in ROUTE_HINTS)]
|
|
|
|
print("\n%d matching string(s):" % len(sel))
|
|
for va, s in sel:
|
|
print(" %#x %s" % (va, s))
|
|
|
|
ctrl = [s for _, s in found if "ut/game" in s.lower()]
|
|
print("\nCONTROL ('ut/game' present): %s (%d)"
|
|
% ("OK" if ctrl else "EMPTY -> wrong region", len(ctrl)))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|