mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-27 07:59:03 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check that obfuscating the September client cannot break a by-name lookup.
|
||||
|
||||
Obfuscar renames types and members, but it cannot see reflection. Anywhere this plugin hands a
|
||||
*name* to a reflection API for one of its OWN types, that name has to survive the rename - hence
|
||||
the SkipMethod rules in 2023Patch/RadicadePatch/obfuscar.xml.
|
||||
|
||||
This script enumerates the names that must survive (from the C# sources) and then checks each one
|
||||
against Obfuscar's Mapping.txt to prove it was kept. Run after an obfuscation pass:
|
||||
|
||||
Tools/check-obfuscation-safety.py
|
||||
|
||||
Exit status is 0 only when every name was preserved.
|
||||
"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
CLIENT = ROOT / "2023Patch" / "RadicadePatch"
|
||||
SHARED = ROOT / "2023Patch" / "RadicadeEacBypass"
|
||||
MAPPING = CLIENT / "bin" / "obfuscated" / "Mapping.txt"
|
||||
|
||||
SOURCES = [p for d in (CLIENT, SHARED) for p in d.rglob("*.cs") if "/obj/" not in p.as_posix() and "/bin/" not in p.as_posix()]
|
||||
|
||||
|
||||
def source_texts():
|
||||
for path in SOURCES:
|
||||
yield path, path.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
|
||||
def our_type_names():
|
||||
"""Type names declared in the plugin and the shared folder (the ones Obfuscar can rename)."""
|
||||
names = set()
|
||||
pattern = re.compile(r"^\s*(?:public|internal|private|protected|static|sealed|abstract|partial|\s)*\b(?:class|struct|enum|interface|record)\s+([A-Za-z_]\w*)", re.M)
|
||||
for _, text in source_texts():
|
||||
names.update(pattern.findall(text))
|
||||
return names
|
||||
|
||||
|
||||
def names_resolved_at_runtime():
|
||||
"""Every member name handed to reflection for one of our own types."""
|
||||
ours = our_type_names()
|
||||
found = {}
|
||||
|
||||
def add(name, where, why):
|
||||
found.setdefault(name, (where, why))
|
||||
|
||||
for path, text in source_texts():
|
||||
lines = text.splitlines()
|
||||
for index, line in enumerate(lines, 1):
|
||||
for match in re.finditer(r"nameof\(\s*([A-Za-z_][\w\.]*)\s*\)", line):
|
||||
qualified = match.group(1)
|
||||
owner = qualified.split(".")[0] if "." in qualified else None
|
||||
member = qualified.split(".")[-1]
|
||||
# nameof(SomeGameType.Member) is the game's own name and survives untouched.
|
||||
if owner is not None and owner not in ours:
|
||||
continue
|
||||
add(member, f"{path.name}:{index}", f"nameof({qualified})")
|
||||
|
||||
# A literal handed to a reflection call on one of our own types.
|
||||
for match in re.finditer(r"(?:typeof\(\s*([A-Za-z_]\w*)\s*\)|GetType\(\s*\))[^;\n]*?[\"']([A-Za-z_]\w*)[\"']", line):
|
||||
owner, literal = match.group(1), match.group(2)
|
||||
if owner in ours:
|
||||
add(literal, f"{path.name}:{index}", f'"{literal}" on {owner}')
|
||||
|
||||
return found
|
||||
|
||||
|
||||
def harmony_patch_methods():
|
||||
"""Methods carrying a Harmony attribute.
|
||||
|
||||
Harmony resolves the special parameters __result/__instance/__state/__args/___field by NAME,
|
||||
and Obfuscar nulls a parameter's name whenever it renames the declaring method (see
|
||||
ShouldSkipParams). A renamed patch method therefore turns its own __result into P_0 and the
|
||||
patch throws at Apply time - which aborts the rest of Plugin.Load, so later patches never arm.
|
||||
Keeping the method name keeps the parameter names with it, so every one of these must be
|
||||
pinned.
|
||||
"""
|
||||
attribute = re.compile(r"^\s*\[\s*Harmony")
|
||||
declaration = re.compile(r"^\s*(?:[\w<>,\[\]\.\?]+\s+)+(?<![\w.])([A-Za-z_]\w*)\s*\(")
|
||||
found = {}
|
||||
for path, text in source_texts():
|
||||
lines = text.splitlines()
|
||||
for index, line in enumerate(lines):
|
||||
match = declaration.match(line)
|
||||
if not match or line.strip().startswith("["):
|
||||
continue
|
||||
name = match.group(1)
|
||||
if name in ("if", "for", "while", "foreach", "switch", "catch", "using", "return", "lock"):
|
||||
continue
|
||||
above = []
|
||||
cursor = index - 1
|
||||
while cursor >= 0 and len(above) < 6:
|
||||
stripped = lines[cursor].strip()
|
||||
if not stripped:
|
||||
cursor -= 1
|
||||
continue
|
||||
above.append(stripped)
|
||||
if stripped.startswith("["):
|
||||
cursor -= 1
|
||||
continue
|
||||
break
|
||||
if any(attribute.match(a) for a in above):
|
||||
found.setdefault(name, f"{path.name}:{index + 1}")
|
||||
return found
|
||||
|
||||
|
||||
def kept_by_obfuscar():
|
||||
"""Members Obfuscar reported as skipped (i.e. not renamed)."""
|
||||
kept = set()
|
||||
for line in MAPPING.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if "skipped:" not in line:
|
||||
continue
|
||||
match = re.search(r"::([A-Za-z_]\w*)(?:\[|\()", line)
|
||||
if match:
|
||||
kept.add(match.group(1))
|
||||
return kept
|
||||
|
||||
|
||||
def renamed_by_obfuscar():
|
||||
renamed = set()
|
||||
for line in MAPPING.read_text(encoding="utf-8", errors="replace").splitlines():
|
||||
if " -> " not in line or "::" not in line:
|
||||
continue
|
||||
match = re.search(r"::([A-Za-z_]\w*)(?:\[|\()", line)
|
||||
if match:
|
||||
renamed.add(match.group(1))
|
||||
return renamed
|
||||
|
||||
|
||||
def main():
|
||||
if not MAPPING.exists():
|
||||
print(f"no mapping at {MAPPING}; run obfuscar first", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
required = names_resolved_at_runtime()
|
||||
patches = harmony_patch_methods()
|
||||
for name, where in patches.items():
|
||||
required.setdefault(name, (where, "Harmony patch method (keeps __parameters)"))
|
||||
|
||||
kept = kept_by_obfuscar()
|
||||
renamed = renamed_by_obfuscar()
|
||||
|
||||
broken = sorted(n for n in required if n in renamed and n not in kept)
|
||||
print(f"by-name lookups that must survive : {len(required)}")
|
||||
print(f" of which Harmony patch methods : {len(patches)}")
|
||||
print(f"members Obfuscar kept : {len(kept)}")
|
||||
for name in sorted(required):
|
||||
where, why = required[name]
|
||||
status = "BROKEN" if name in broken else "kept"
|
||||
print(f" [{status:6}] {name:36} {why:46} {where}")
|
||||
|
||||
if broken:
|
||||
print(f"\nFAIL: {len(broken)} name(s) renamed but still looked up at runtime: {', '.join(broken)}")
|
||||
return 1
|
||||
|
||||
print("\nOK: every by-name lookup points at a name Obfuscar preserved.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user