mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-19 07:00:27 +02:00
214 lines
7.1 KiB
Python
214 lines
7.1 KiB
Python
"""January's string literals as an oracle, with owner verification.
|
|
|
|
csplit preserved .rdata, so every ``??_C@`` COMDAT in a target object carries
|
|
January's exact string bytes. Two uses, both fail-closed:
|
|
|
|
``defects`` — find strings we transcribed wrong. A raw source-text near-miss
|
|
scan has a measured 87% false-positive rate (20 of 23, 2026-08-07), because
|
|
source regexes cannot see macro stringization, they match ``#include`` lines
|
|
as literals, and a January string similar to one of ours usually belongs to a
|
|
*different, unwritten* function. This mode therefore verifies at the object
|
|
level before reporting: the January string must be absent from our **built
|
|
object** (not our source text), and its owning .text function must be written
|
|
and non-exact on our side. Everything else is dropped with its reason.
|
|
|
|
``synopsis`` — describe unwritten code. Strings referenced only by
|
|
target-only functions are format strings, assert expressions and command
|
|
names for code nobody has written; grouped by owning function they give a
|
|
per-function synopsis before a byte of disassembly is read.
|
|
|
|
Neither mode grants credit or edits anything.
|
|
|
|
Usage:
|
|
python tools/string_oracle.py defects [--config objdiff.json] [--min-ratio 0.8]
|
|
python tools/string_oracle.py synopsis [--config objdiff.json] [--unit NAME]
|
|
[--output research/unwritten_string_synopsis.json]
|
|
"""
|
|
import argparse
|
|
import difflib
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import coff_compare as cc
|
|
from residual_frontier import text_owners
|
|
|
|
|
|
def target_strings(obj):
|
|
"""Map string-section number -> (comdat name, decoded bytes)."""
|
|
out = {}
|
|
for sym in obj["symbols"]:
|
|
if sym["name"].startswith("??_C@") and sym["section"] > 0 \
|
|
and sym["value"] == 0:
|
|
section = obj["sections"][sym["section"] - 1]
|
|
raw = bytes(cc._section_bytes(obj, section))[:section["size"]]
|
|
out[sym["section"]] = (sym["name"], raw.rstrip(b"\0"))
|
|
return out
|
|
|
|
|
|
def string_owners(obj, owners):
|
|
"""Map string COMDAT name -> [owning .text function names]."""
|
|
refs = {}
|
|
for name, secno in owners.items():
|
|
info = cc.section_info_by_number(obj, secno)
|
|
for relocation in info["relocations"]:
|
|
symbolic = relocation.get("symbolic_target")
|
|
target = relocation["target"]
|
|
ref = None
|
|
if symbolic is not None:
|
|
ref = symbolic[1]
|
|
elif target[0] == "symbol":
|
|
ref = target[1]
|
|
elif target[0] == "defined-noncode":
|
|
ref = target[2]
|
|
if ref and ref.startswith("??_C@"):
|
|
refs.setdefault(ref, []).append(name)
|
|
return refs
|
|
|
|
|
|
def iter_units(config, only=None):
|
|
for unit in config["units"]:
|
|
if "base_path" not in unit:
|
|
continue
|
|
if "/libs/" in unit["target_path"].replace("\\", "/"):
|
|
continue
|
|
if only and unit["name"] != only:
|
|
continue
|
|
target_path = Path(unit["target_path"])
|
|
base_path = Path(unit["base_path"])
|
|
if target_path.exists() and base_path.exists():
|
|
yield unit["name"], target_path, base_path
|
|
|
|
|
|
def run_defects(config, min_ratio):
|
|
verified = []
|
|
dropped = {"in-base-object": 0, "owner-unwritten": 0, "owner-exact": 0,
|
|
"no-similar": 0, "undecodable": 0}
|
|
for name, target_path, base_path in iter_units(config):
|
|
target = cc.load(target_path)
|
|
base = cc.load(base_path)
|
|
base_data = base["data"]
|
|
strings = target_strings(target)
|
|
if not strings:
|
|
continue
|
|
owners = text_owners(target)
|
|
refs = string_owners(target, owners)
|
|
base_owners = text_owners(base)
|
|
base_literals = [
|
|
raw for _, raw in target_strings(base).values()]
|
|
for secno, (comdat, raw) in strings.items():
|
|
if raw + b"\0" in base_data or raw in base_literals:
|
|
continue # we already emit it somewhere
|
|
try:
|
|
text = raw.decode("ascii")
|
|
except UnicodeDecodeError:
|
|
dropped["undecodable"] += 1
|
|
continue
|
|
owner_names = refs.get(comdat, [])
|
|
written_residual = []
|
|
for fn in owner_names:
|
|
if fn not in base_owners:
|
|
continue
|
|
ti = cc.section_info_by_number(target, owners[fn])
|
|
bi = cc.section_info_by_number(base, base_owners[fn])
|
|
if not cc.section_infos_equal(ti, bi):
|
|
written_residual.append(fn)
|
|
if owner_names and not written_residual:
|
|
key = "owner-exact" if any(
|
|
fn in base_owners for fn in owner_names) else "owner-unwritten"
|
|
dropped[key] += 1
|
|
continue
|
|
best = None
|
|
for _, base_raw in target_strings(base).values():
|
|
try:
|
|
base_text = base_raw.decode("ascii")
|
|
except UnicodeDecodeError:
|
|
continue
|
|
ratio = difflib.SequenceMatcher(
|
|
None, text, base_text, autojunk=False).ratio()
|
|
if ratio >= min_ratio and (best is None or ratio > best[0]):
|
|
best = (ratio, base_text)
|
|
if best is None:
|
|
dropped["no-similar"] += 1
|
|
continue
|
|
verified.append({
|
|
"unit": name,
|
|
"january": text,
|
|
"ours": best[1],
|
|
"ratio": round(best[0], 3),
|
|
"owners": written_residual or owner_names,
|
|
})
|
|
verified.sort(key=lambda item: -item["ratio"])
|
|
for item in verified:
|
|
print(f"{item['unit']} {item['ratio']}")
|
|
print(f" JAN {item['january']!r}")
|
|
print(f" OUR {item['ours']!r}")
|
|
print(f" owner(s): {', '.join(item['owners']) or '(data only)'}")
|
|
print(f"\nverified defects: {len(verified)}")
|
|
print("dropped:", json.dumps(dropped))
|
|
return 0
|
|
|
|
|
|
def run_synopsis(config, only, output):
|
|
synopsis = {}
|
|
total_functions = 0
|
|
for name, target_path, base_path in iter_units(config, only):
|
|
target = cc.load(target_path)
|
|
base = cc.load(base_path)
|
|
strings = target_strings(target)
|
|
if not strings:
|
|
continue
|
|
owners = text_owners(target)
|
|
base_owners = text_owners(base)
|
|
refs = string_owners(target, owners)
|
|
per_function = {}
|
|
for comdat, owner_list in refs.items():
|
|
for secno, (comdat_name, raw) in strings.items():
|
|
if comdat_name != comdat:
|
|
continue
|
|
try:
|
|
text = raw.decode("ascii")
|
|
except UnicodeDecodeError:
|
|
continue
|
|
for fn in owner_list:
|
|
if fn in base_owners:
|
|
continue # written; synopsis is for unwritten code
|
|
per_function.setdefault(fn, []).append(text)
|
|
if per_function:
|
|
sized = {}
|
|
for fn, texts in per_function.items():
|
|
info = cc.section_info_by_number(target, owners[fn])
|
|
sized[fn] = {"size": info["size"], "strings": sorted(set(texts))}
|
|
synopsis[name] = sized
|
|
total_functions += len(sized)
|
|
if output:
|
|
Path(output).parent.mkdir(parents=True, exist_ok=True)
|
|
Path(output).write_text(
|
|
json.dumps(synopsis, indent=1) + "\n", encoding="utf-8")
|
|
print(f"units with unwritten string-bearing functions: {len(synopsis)}")
|
|
print(f"unwritten functions with a synopsis: {total_functions}")
|
|
if only:
|
|
print(json.dumps(synopsis.get(only, {}), indent=1))
|
|
return 0
|
|
|
|
|
|
def main(argv=None):
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("mode", choices=("defects", "synopsis"))
|
|
parser.add_argument("--config", default="objdiff.json")
|
|
parser.add_argument("--min-ratio", type=float, default=0.8)
|
|
parser.add_argument("--unit")
|
|
parser.add_argument(
|
|
"--output", default="research/unwritten_string_synopsis.json")
|
|
args = parser.parse_args(argv)
|
|
config = json.loads(Path(args.config).read_text(encoding="utf-8"))
|
|
if args.mode == "defects":
|
|
return run_defects(config, args.min_ratio)
|
|
return run_synopsis(config, args.unit, args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|