- 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)
9.4 KiB
9.4 KiB
AGENTS.md - Plex_Patch
Project overview
Reverse-engineering notes and a runtime patch for Plex Media Server on
Linux x86-64 (and ARM64/aarch64). It hooks Plex's
FeatureManager and forces every feature bit on, unlocking gated features.
Educational / RE use on software you run yourself; it ships no Plex code and
bypasses no account/server authentication.
Target & runtime reality (important)
- Target: the main
Plex Media Serverexecutable (PIE). The feature machinery moved here fromlibsoci_core.soon post-2024/08/13 builds;libsoci_core.sois no longer the target. - Plex runs against its OWN bundled musl libc + libgcompat
(
/usr/lib/plexmediaserver/lib/), NOT the host glibc. This drives the build and injection choices below. - ARM64: confirmed working on aarch64-linux-musl. The ARM64 binary has the
same musl constraint; zig cross-compilation targets
aarch64-linux-musl.
Architecture
x86-64
- Language: C++20, plus the vendored Zydis C amalgamation for instruction decoding.
- Module discovery:
dl_iterate_phdrused in early versions; current code uses/proc/self/maps(the constructor runs before the main PIE is loaded by musl, sodl_iterate_phdrreturns 0 callbacks). - Hook:
sig_scan()finds target functions by byte-pattern signature;create_hook()installs a 14-bytejmp [rip+0x06]trampoline (Zydis decodes the prologue so relocated bytes stay valid). - Feature unlock effect: after Plex applies its MyPlex feature list, the
hook forces all 14
g_feature_bitset_slotsqwords on (std::bitset<896>), so every feature (including Plex Pass, code 92, slot 11) reads as enabled. - Webhook unlock: an additional hook targets
sub_122B2F2(the generic preference getter at0x122B2F2). When the key is"WebHooksEnabled", it returnstrueregardless of the actual persisted value, enabling Plex's built-in webhook dispatch (play/pause/stop/rate events). This is a separate mechanism from the feature bitset —WebHooksEnabledis a plain boolean preference, not a feature bit. Both dispatch functions (sub_125A6D4@0x125A6D4andsub_125B766@0x125B766) check only this preference with no secondary feature-bit gate.
ARM64 / aarch64
- Language: C++20. Zydis is NOT used on ARM64 (fixed 4-byte instructions,
no length decoding needed).
Zydis.his conditionally excluded via#ifndef __aarch64__. - Module discovery: same
/proc/self/mapsparser (get_dottext_info()) works identically on ARM64. - Hook:
create_hook_arm64()installs a 16-byteLDR X17, [PC, #8]; BR X17; <8-byte target>trampoline. No Zydis needed. The trampoline copies 4 original instructions (16 bytes) and appends the same LDR+BR jump-back sequence. - Feature unlock effect: same bitset-force approach — hook the
FeatureManager constructor via ARM64 signature, then force all 14 uint64_t
slots to
UINT64_MAX. - Webhook unlock: currently targets preference init functions (SSO-check
prologue patterns like
ldrb w?, [x?, #0x17]). The ARM64 callbacks are generic feature-return-true for now; WebHooksEnabled-specific key matching (like the x86-64 sub_122B2F2 hook) is pending identification of the exact ARM64 preference getter function. - Analysis tooling:
get_arm64_sigs.pyuses Capstone to extract ARM64 function signatures with ADRP page counting;check_webhooks.pydumps ADRP+ADD string loads inside candidate functions;disasm_final.py/disasm_key_areas.pyare earlier (broken) Capstone analysis scripts.
ARM64 function signatures (from get_arm64_sigs.py)
| VAddr | Signature | Notes |
|---|---|---|
0x10dd904 |
FD 7B BA A9 FC 6F 01 A9 FA 67 02 A9 F8 5F 03 A9 F6 57 04 A9 F4 4F 05 A9 FD 03 00 91 FF 43 09 D1 |
Preference init function — 6 stp pairs (save 12 regs), sub sp,#0x250. The function at this address loads "WebHooksEnabled" string via ADRP+ADD at 0x10e0eac. |
0x658120 |
FD 7B BD A9 F5 0B 00 F9 F4 4F 02 A9 FD 03 00 91 F3 03 01 AA F4 03 00 AA 61 00 80 52 E0 03 13 AA |
FeatureManager class function — saves 2 regs, calls with x0/x1, loads FeatureManager strings from page 0x1B7000. |
0x658070 |
FD 7B 03 A9 F4 4F 04 A9 FD C3 00 91 |
FeatureManager constructor-like — stp x29,x30,[sp,#-0x30]! ; stp x20,x19,[sp,#0x10] ; mov x29,sp. Used as catch-all FeatureManager hook target. |
0xeba0b4 |
FD 7B BE A9 F3 0B 00 F9 FD 03 00 91 08 5C 40 39 09 04 40 F9 F3 03 00 AA 0A 1D 00 13 5F 01 00 71 |
Feature-check function — stp x29,x30,[sp,#-0x10]! ; str x19,[sp,#8] ; mov x29,sp ; ldrb w8,[x0,#0x17] (SSO check). 3 ADRP refs to page 0x1BD000 (hasPlexPass strings). |
0x5e4188 |
FD 7B 01 A9 FD 43 00 91 A8 65 00 B0 08 85 42 F9 49 66 00 F0 ... |
hasPlexPass checking function — 3 ADRP refs to page 0x1BD000. |
0x5e42c0 |
FD 7B BE A9 F4 4F 01 A9 FD 03 00 91 74 66 00 90 88 82 46 39 ... |
hasPlexPass checking function — 3 ADRP refs to page 0x1BD000. |
0x5e4368 |
FD 7B BE A9 F3 0B 00 F9 FD 03 00 91 73 66 00 90 68 A2 47 39 ... |
hasPlexPass checking function — 3 ADRP refs to page 0x1BD000. |
ARM64 binary layout (from _Plex Media Server, text section at file offset 0x5d35bc)
| Page | Contains | Notable Strings |
|---|---|---|
0x1B7000 |
FeatureManager strings, preference init strings | "FeatureManager", "FeatureManager: Using cached data" (@ 0x1B72AB), "hasPlexPass" (@ 0x1B749B) |
0x1BD000 |
Feature checking strings | "playing", "paused", "buffering", "media_css_min_assets_cache_ms" |
0x363000 |
Webhook/web-related strings | "WebHooksEnabled" (@ 0x363871) |
ARM64 text section
- Text section: vaddr=0x5e35bc, file_offset=0x5d35bc, size=0xc05964 (~12MB)
- Built for aarch64 Linux (little-endian), PIE position-independent
- All functions use ARM64 standard prologue:
stp x29, x30, [sp, #-N]! - String references via ADRP+ADD pairs (PC-relative page + 12-bit offset)
- No fixed function addresses when PIE-loaded; all discovery via sig_scan
Build & inject (details in README.md / docs/BUILD.md)
- Build with musl via
zig(-target x86_64-linux-musl):bash build.sh. For ARM64:bash build.sh --arm64. A glibc build cannot relocate glibc-only symbols (__isoc23_strtol,arc4random,*_chk,_dl_find_object) in Plex's musl runtime → exit 127. - Inject with
LD_PRELOADviascripts/plex-crack-wrapper.sh+ a systemd
Key files
src/hook.cpp/src/hook.hpp— hook engine, feature logic, feature-UUID catalogsrc/main.cpp— library constructor (unsetenv("LD_PRELOAD")thenhook())build.sh— musl build via zig (auto-downloaded) with an ABI sanity gatescripts/plex-crack-wrapper.sh—LD_PRELOADlauncher scoped to the PMS processscripts/readbitset.py— live feature-bitset verifierthird_party/zydis/— vendored Zydis disassembler (MIT)get_arm64_sigs.py— Capstone-based ARM64 function signature extractor with ADRP page countingcheck_webhooks.py— dump ADRP+ADD string loads within candidate ARM64 functionsexperimental/debug_hook.c— standalone alternate hook (legacyis_feature_availablesignature)
RE artifacts (the Plex Media Server binary, libsoci_core.so, and *.i64 IDA
databases) are git-ignored and not redistributed.
Signature patterns
Hex bytes with ? wildcards; spaces ignored (? = one-byte wildcard). Patterns
are version-specific — re-verify after PMS updates.
x86-64 signatures
| Target | Address | Signature | Notes |
|---|---|---|---|
bitset_init (modern path constructor) |
dynamic | 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 |
FeatureManager_apply_feature_list_xml — post-2024/08/13 |
sub_122B2F2 (preference getter) |
0x122B2F2 |
48 89 F3 4C 89 F7 0F B6 46 17 48 89 F1 84 C0 |
mov rbx, rsi; mov r14, rdi; movzx eax,[rsi+0x17] — std::string SSO check prologue |
is_user_feature_set (legacy) |
dynamic | 55 48 89 E5 48 8B 07 48 85 C0 74 09 |
Pre-2024/08/13 fallback |
is_feature_available (legacy) |
dynamic | E8 ? ? ? ? 86 43 (call rel32 + test al, byte ptr [rbx+3]) |
Rel32 followed, pre-2024/08/13 fallback |
map_find (legacy) |
dynamic | 55 48 89 E5 41 57 41 56 53 48 83 EC ? 49 89 F7 4C 8D 77 |
Pre-2024/08/13 fallback |
ARM64 signatures (built with bash build.sh --arm64)
| Target | Signature | Notes |
|---|---|---|
FeatureManager_init (bitset constructor) |
FD 7B 03 A9 F4 4F 04 A9 FD C3 00 91 |
stp x29,x30,[sp,#-0x30]! ; stp x20,x19,[sp,#0x10] ; mov x29,sp. Catches FeatureManager init to force bits on. |
FeatureManager_class_method (alternative) |
FD 7B BD A9 F5 0B 00 F9 F4 4F 02 A9 FD 03 00 91 F3 03 01 AA F4 03 00 AA 61 00 80 52 E0 03 13 AA |
Saves 1 reg pair + str ; handles x0/x1 args. |
feature_check_sso (feature checker) |
FD 7B BE A9 F3 0B 00 F9 FD 03 00 91 08 5C 40 39 |
stp x29,x30,[sp,#-0x10]! ; str x19,[sp,#8] ; mov x29,sp ; ldrb w8,[x0,#0x17]. Hooked to always return true. |
pref_init (preference init) |
FD 7B BA A9 FC 6F 01 A9 FA 67 02 A9 F8 5F 03 A9 F6 57 04 A9 F4 4F 05 A9 FD 03 00 91 FF 43 09 D1 |
6 stp pairs, sub sp,#0x250. Loads "WebHooksEnabled" string. WebHooksEnabled-specific hook pending. |
pref_getter_sso (generic getter, placeholder) |
FD 7B ?? A9 ?? ?? ?? A9 FD 03 00 91 ?? 5C 40 39 ?? |
Broad pattern: prologue + SSO check on any x-reg. May match the preference getter. |