#!/usr/bin/env python3
"""Forge a FifaOnline::FirstPartyAuthCodeFutureImpl node and enqueue it so DoTick
(@0x146f199c0) fires GetAuthCode over LSX. Clean-room; from our own RE (ENQUEUE_PLAN.md,
adversarially verified). ptrace_scope=0 required. LSX responder MUST be answering
GetAuthCode first (the auth call is synchronous with a 15s timeout).

Two gates (both live-verified 0): the retriever queue slot AND OriginSDK[+0x3a0] default-user.
This sets BOTH. Treat the first run as a PROBE: setting the default user flips ~15 other
GetDefaultUser consumers; `--revert` restores the originals.

Usage:  python3 forge_node.py            # forge + enqueue
        python3 forge_node.py --revert   # restore SDK default-user + clear the slot
"""
import sys, struct, glob, os, json

ONLINEMGR_PP = 0x1448a3b20   # *-> OnlineManager ; retriever = +0x4e98
RETR_OFF     = 0x4e98
GUARD        = 0x1448a3ac3   # enqueue guard byte (must be 1)
SDK_PP       = 0x144b7c7a0   # *-> OriginSDK
SDK_DEFUSER  = 0x3a0         # default-user slot (BLOCKER)  -- set +0x3a0 AND +0x3a8
SDK_DEREF    = 0x3b0         # deref'd unconditionally downstream; must stay non-null
VPTR   = 0x1438f5d58         # node primary vtable (AddRef/Release/dtor/GetStatus/GetResult)
VPTR2  = 0x1438f5d90         # node secondary vtable -- MUST be this, never 0 (Release calls [this+8]->[0])
NODE_VA = 0x14300a380        # validated zero/unreferenced scratch (ENQUEUE_PLAN §4) -- re-checked below
CLIENTID = b"FIFA17PC"       # only proven constraint: non-empty
SAVE = "/tmp/forge_node_orig.json"

def pid():
    for d in glob.glob('/proc/[0-9]*'):
        try:
            if open(d+'/comm').read().strip()=='FIFA17.exe': return int(d.split('/')[-1])
        except Exception: pass
    raise SystemExit("FIFA17.exe not running")

def build_node():
    b = bytearray(0xF0)
    struct.pack_into('<Q', b, 0x00, VPTR)
    struct.pack_into('<Q', b, 0x08, VPTR2)
    struct.pack_into('<I', b, 0x10, 2)                 # refcount=2 -> survives DoTick's Release, never freed
    b[0x18:0x18+len(CLIENTID)] = CLIENTID              # inline clientId, NUL-terminated
    return bytes(b)

def main():
    p = pid(); mp = f"/proc/{p}/mem"
    f = open(mp, "r+b")
    def rq(va): f.seek(va); return struct.unpack('<Q', f.read(8))[0]
    def rd(va,n): f.seek(va); return f.read(n)
    def wr(va,b): f.seek(va); f.write(b)

    onlinemgr = rq(ONLINEMGR_PP); retr = onlinemgr + RETR_OFF
    sdk = rq(SDK_PP)
    slot = retr + 0x08

    if "--revert" in sys.argv:
        orig = json.load(open(SAVE)) if os.path.exists(SAVE) else {}
        wr(sdk+SDK_DEFUSER, struct.pack('<Q', orig.get("defuser",0)))
        wr(sdk+0x3a8,       struct.pack('<Q', orig.get("defuser8",0)))
        wr(slot,            struct.pack('<Q', orig.get("slot",0)))
        print(f"[revert] SDK+0x3a0/0x3a8 -> {orig.get('defuser',0):#x}/{orig.get('defuser8',0):#x}, slot -> {orig.get('slot',0):#x}")
        return

    # --- preconditions (verify, do not assume) ---
    assert rd(GUARD,1)[0] == 1, "guard byte != 1"
    assert rq(sdk+SDK_DEREF) != 0, "SDK+0x3b0 is NULL (would fault downstream) -- abort"
    assert rq(slot) == 0, f"queue slot already non-zero ({rq(slot):#x}) -- abort"
    scratch = rd(NODE_VA, 0xF0)
    assert all(x==0 for x in scratch), "scratch NODE_VA not zero -- abort"
    assert rq(VPTR) == 0x147e8f160, "node vtable[0] mismatch -- wrong build?"

    # save originals for --revert
    json.dump({"defuser": rq(sdk+SDK_DEFUSER), "defuser8": rq(sdk+0x3a8), "slot": rq(slot)}, open(SAVE,"w"))

    # 1) forge the node into scratch (BEFORE anything is armed)
    wr(NODE_VA, build_node())
    assert rd(NODE_VA,0xF0) == build_node(), "node write-back mismatch"
    print(f"[+] node forged @ {NODE_VA:#x}  clientId={CLIENTID.decode()}  refcount=2")

    # 2) gate 2: set the Origin default user (both slots, like the real SDK)
    wr(sdk+SDK_DEFUSER, struct.pack('<Q', sdk))
    wr(sdk+0x3a8,       struct.pack('<Q', sdk))
    assert rq(sdk+SDK_DEFUSER)==sdk and rq(sdk+0x3a8)==sdk, "defuser write-back mismatch"
    print(f"[+] OriginSDK[+0x3a0]/[+0x3a8] set -> {sdk:#x} (default user)")

    # 3) gate 1 (the TRIGGER, set last): enqueue the node
    wr(slot, struct.pack('<Q', NODE_VA))
    assert rq(slot)==NODE_VA, "slot write-back mismatch"
    print(f"[+] retriever+0x8 ({slot:#x}) -> {NODE_VA:#x}  *** ENQUEUED ***")
    print("    Watch /tmp/lsx.log for <GetAuthCode ClientId=\"FIFA17PC\">. Then node+0xE8 -> 1,")
    print("    node+0xE0=200 = error (read node+0x58 msg). --revert to undo.")

if __name__ == "__main__":
    main()
