#!/usr/bin/env python3 """ Verify that all x86-64 hook signatures in Freeloader/src/hook.cpp match the Plex Media Server binary. Exits non-zero if any signature is missing. Usage: python3 verify_signatures.py When a signature fails, a partial-match diagnostic is printed to help locate the new pattern. """ import sys import re import struct import subprocess # ── Signature definitions ───────────────────────────────────────────────── # These mirror the sig_scan() calls in Freeloader/src/hook.cpp (x86-64 path). # Each entry: (step_name, pattern_string, must_match) SIGNATURES = [ ( "STEP1 sub_122B2F2 (preference getter)", "55 48 89 E5 41 57 41 56 53 48 83 EC 18 48 89 F3 49 89 FE 0F B6 46 17 48 89 F1 84 C0", True, ), ( "STEP3 bitset reference (lea rcx + mov rdx + xchg)", "48 8D 0D ? ? ? ? 48 8B 94 05 ? ? ? ? 48 87 14 08", True, ), ( "STEP3 bs_init (FeatureManager constructor)", "55 48 89 E5 41 57 41 56 41 55 41 54 53 48 81 EC ? ? 00 00 49 89 FE 48 8D 9D ? ? ? ? 48 89 DF E8 ? ? ? ? 48 8B 1B 48 85 DB", True, ), ( "STEP4 legacy is_user_feature_set", "55 48 89 E5 48 8B 07 48 85 C0 74 09", False, # fallback — may not be needed if STEP3 works ), ( "STEP4 legacy map_find", "55 48 89 E5 41 57 41 56 53 48 83 EC ? 49 89 F7 4C 8D 77", False, # fallback ), ] def parse_pattern(p): out = [] for tok in p.split(): if tok in ("??", "?"): out.append(None) else: out.append(int(tok, 16)) return out def find_matches(data, pattern): matches = [] plen = len(pattern) for i in range(len(data) - plen + 1): if all(pb is None or data[i + j] == pb for j, pb in enumerate(pattern)): matches.append(i) return matches def partial_match_length(data, offset, pattern): """How many leading bytes of the pattern match at this offset.""" matched = 0 for j, pb in enumerate(pattern): if offset + j >= len(data): break if pb is not None and data[offset + j] != pb: break matched += 1 return matched def best_partial_matches(data, pattern, top_n=5): """Find the offsets with the longest leading match of the pattern.""" scores = [] plen = len(pattern) for i in range(len(data) - min(plen, 8) + 1): score = partial_match_length(data, i, pattern) if score >= min(8, plen): # at least 8 bytes or full pattern scores.append((score, i)) scores.sort(reverse=True) return scores[:top_n] def main(): if len(sys.argv) != 2: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(2) pms_path = sys.argv[1] try: with open(pms_path, "rb") as f: data = f.read() except Exception as e: print(f"ERROR: cannot read {pms_path}: {e}", file=sys.stderr) sys.exit(2) print(f"Verifying signatures against: {pms_path} ({len(data)} bytes)") print() all_required_ok = True any_required_missing = False for name, pattern_str, required in SIGNATURES: pattern = parse_pattern(pattern_str) matches = find_matches(data, pattern) if matches: status = "OK" detail = f"{len(matches)} match(es)" if len(matches) > 1 and required: status = "WARN" detail = f"{len(matches)} matches (expected 1)" print(f" [{status}] {name}: {detail}") for m in matches[:3]: ctx = data[m : m + min(len(pattern) + 8, 32)].hex(" ") print(f" 0x{m:08x}: {ctx}") else: if required: print(f" [FAIL] {name}: NOT FOUND") any_required_missing = True all_required_ok = False # Print partial matches to help locate the new pattern partials = best_partial_matches(data, pattern) if partials: print(f" Best partial matches (leading bytes):") for score, off in partials: ctx = data[off : off + min(len(pattern) + 8, 32)].hex(" ") print( f" 0x{off:08x} ({score}/{len(pattern)} bytes): {ctx}" ) else: print(f" No partial matches found (>=8 leading bytes).") else: print(f" [SKIP] {name}: not found (optional fallback)") print() if all_required_ok: print("All required signatures matched. Build can proceed.") sys.exit(0) else: print( "REQUIRED SIGNATURE(S) MISSING — the hook will not work on this PMS " "version.", file=sys.stderr, ) print( "Update the patterns in Freeloader/src/hook.cpp and rebuild.", file=sys.stderr, ) sys.exit(1) if __name__ == "__main__": main()