Files
plex-docker/scripts/verify_signatures.py
benjamin 72f4661bdc Replace patchelf crack with Freeloader LD_PRELOAD approach
- Multi-stage Dockerfile: discover patterns from PMS binary (capstone),
  compile .so with zig (musl), layer onto lscr.io/linuxserver/plex
- Uses LD_PRELOAD instead of patchelf (which corrupts Plex's musl loader)
- Auto-discovery: broad structural patterns with string-anchored fallback
  (//feature) and relationship-based fallback (BITSET_REF within BS_INIT)
- hook.cpp uses __has_include for generated patterns with hardcoded fallbacks
- Custom wrapper.sh (no traffic_logger preload)
- Vendored Freeloader source (github.com/authrequest/Freeloader, AGPL-3.0)
- Removed stale plexmediaserver_crack.so binary
- Supports Plex 1.43.3+ (verified against 1.43.2 and 1.43.3)
2026-08-19 22:33:42 +02:00

166 lines
5.1 KiB
Python

#!/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 <path-to-Plex Media Server binary>
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]} <path-to-Plex Media Server>", 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()