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)
This commit is contained in:
1 parent
4399a8288d
commit
72f4661bdc
72 files changed
+77927
-17
No files matched your search
@@ -0,0 +1,165 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user