32:9 / 21:9 ultrawide support for Gundam Breaker 4 (UE4.27) via SUWSF + Ultimate ASI Loader. Forces Hor+ FOV (AspectRatioAxisConstraint=MaintainYFOV) and disables pillarboxing, patched in decrypted memory at runtime so it holds through missions (the exe is SteamStub-encrypted; a static patch is impossible). Confirmed working in gameplay at 5120x1440 (Samsung Odyssey G93SC) on Proton. Requires WINEDLLOVERRIDES="dsound=n,b" on Proton so the loader injects. Includes: patch/SUWSF.ini, Proton installer with clean uninstall, release-zip builder, and runtime dump/verify tooling. Third-party bundles under MIT (see THIRD_PARTY.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
128 lines
4.9 KiB
Python
Executable File
128 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Dump the DECRYPTED GB4-Win64-Shipping.exe image from a running (Proton) process.
|
|
|
|
Why: the on-disk exe is SteamStub-encrypted (.text entropy 8.0). The real UE4
|
|
code only exists in memory after the stub decrypts it at launch. This grabs that
|
|
memory so we can verify/derive the exact SUWSF byte patterns offline.
|
|
|
|
Usage:
|
|
# 1. Launch Gundam Breaker 4 (get to the main menu / lobby).
|
|
# 2. Run:
|
|
python3 tools/dump_decrypted.py
|
|
# -> writes scratch dump + region metadata, then prints AOB match report.
|
|
|
|
If you get "Operation not permitted", either run with sudo, or temporarily:
|
|
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope (reset to 1 after)
|
|
"""
|
|
import ctypes, ctypes.util, os, re, sys, json, struct, glob
|
|
|
|
MODULE_HINT = "GB4-Win64-Shipping.exe"
|
|
OUTDIR = os.environ.get("GB4_DUMP_DIR", "/tmp/claude-1000/-home-ckoch-Documents-Development-BG4-UltraWide-fix/1a8ebba4-6fbe-45ae-a873-0e69d867e0db/scratchpad/gb4dump")
|
|
|
|
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
|
|
|
|
|
|
class iovec(ctypes.Structure):
|
|
_fields_ = [("iov_base", ctypes.c_void_p), ("iov_len", ctypes.c_size_t)]
|
|
|
|
|
|
libc.process_vm_readv.restype = ctypes.c_ssize_t
|
|
libc.process_vm_readv.argtypes = [ctypes.c_int, ctypes.POINTER(iovec), ctypes.c_ulong,
|
|
ctypes.POINTER(iovec), ctypes.c_ulong, ctypes.c_ulong]
|
|
|
|
|
|
def find_pid():
|
|
if len(sys.argv) > 1 and sys.argv[1].isdigit():
|
|
return int(sys.argv[1])
|
|
for p in glob.glob("/proc/[0-9]*"):
|
|
try:
|
|
with open(f"{p}/cmdline", "rb") as f:
|
|
cl = f.read().replace(b"\0", b" ").decode("utf-8", "replace")
|
|
if MODULE_HINT in cl:
|
|
return int(os.path.basename(p))
|
|
except OSError:
|
|
continue
|
|
return None
|
|
|
|
|
|
def read_mem(pid, addr, size):
|
|
buf = ctypes.create_string_buffer(size)
|
|
local = iovec(ctypes.cast(buf, ctypes.c_void_p), size)
|
|
remote = iovec(ctypes.c_void_p(addr), size)
|
|
n = libc.process_vm_readv(pid, ctypes.byref(local), 1, ctypes.byref(remote), 1, 0)
|
|
if n < 0:
|
|
# fallback: /proc/pid/mem pread
|
|
try:
|
|
with open(f"/proc/{pid}/mem", "rb", 0) as m:
|
|
m.seek(addr)
|
|
return m.read(size)
|
|
except OSError as e:
|
|
raise OSError(f"read fail @0x{addr:x}: {os.strerror(ctypes.get_errno())} / {e}")
|
|
return buf.raw[:n]
|
|
|
|
|
|
def module_regions(pid):
|
|
"""Return list of (start,end,perms,path) VMAs backed by the module exe."""
|
|
regs = []
|
|
modpath = None
|
|
with open(f"/proc/{pid}/maps") as f:
|
|
for line in f:
|
|
m = re.match(r"([0-9a-f]+)-([0-9a-f]+) (\S{4}) \S+ \S+ \S+ *(.*)", line)
|
|
if not m:
|
|
continue
|
|
start, end, perms, path = int(m[1], 16), int(m[2], 16), m[3], m[4]
|
|
if MODULE_HINT in path:
|
|
if modpath is None:
|
|
modpath = path
|
|
regs.append((start, end, perms, path))
|
|
return modpath, regs
|
|
|
|
|
|
def main():
|
|
pid = find_pid()
|
|
if not pid:
|
|
print(f"[!] No running process matching '{MODULE_HINT}'. Launch the game first.")
|
|
sys.exit(1)
|
|
print(f"[+] pid = {pid}")
|
|
modpath, regs = module_regions(pid)
|
|
if not regs:
|
|
print("[!] Module not mapped yet. Reach the main menu, then retry.")
|
|
sys.exit(1)
|
|
base = min(r[0] for r in regs)
|
|
print(f"[+] module base = 0x{base:x} ({modpath})")
|
|
os.makedirs(OUTDIR, exist_ok=True)
|
|
meta = {"pid": pid, "module": modpath, "base": base, "regions": []}
|
|
blob = bytearray()
|
|
for start, end, perms, path in regs:
|
|
size = end - start
|
|
try:
|
|
data = read_mem(pid, start, size)
|
|
except OSError as e:
|
|
print(f" skip 0x{start:x}-0x{end:x} {perms}: {e}")
|
|
continue
|
|
rva = start - base
|
|
# pad blob so file offset == rva (sparse-ish, capped)
|
|
if rva >= 0 and rva < 0x20000000:
|
|
if len(blob) < rva:
|
|
blob.extend(b"\x00" * (rva - len(blob)))
|
|
blob[rva:rva + len(data)] = data
|
|
meta["regions"].append({"start": start, "end": end, "rva": rva,
|
|
"perms": perms, "size": size, "got": len(data)})
|
|
print(f" dumped 0x{start:x}-0x{end:x} {perms} rva=0x{rva:x} ({len(data)} bytes)")
|
|
dump_path = os.path.join(OUTDIR, "gb4_decrypted.bin")
|
|
with open(dump_path, "wb") as f:
|
|
f.write(blob)
|
|
with open(os.path.join(OUTDIR, "gb4_dump_meta.json"), "w") as f:
|
|
json.dump(meta, f, indent=2)
|
|
print(f"[+] wrote {dump_path} ({len(blob)} bytes, rva-aligned)")
|
|
print(f"[+] wrote {os.path.join(OUTDIR, 'gb4_dump_meta.json')}")
|
|
|
|
# quick sanity: is it decrypted? look for the UE4 version string / a UTF-16 hint
|
|
if b"++UE4+Release-4.27" in blob or b"CalculateProjectionMatrix" in blob:
|
|
print("[+] decrypted UE4 code confirmed in dump.")
|
|
print("\nNext: python3 tools/aob.py (verifies SUWSF patterns against this dump)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|