- 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)
6.7 KiB
Plex_Patch — Windows x64
Feature-unlock patch for Plex Media Server on Windows x64. Port of the
Linux LD_PRELOAD approach to a DLL injector model.
⚠️ Disclaimer — Educational / reverse-engineering only, on software you legally own and run yourself. No Plex code is shipped. Use at your own risk.
How it works
plex_inject.exe ──CreateRemoteThread(LoadLibraryA)──▶ Plex Media Server.exe
│
plex_patch.dll (DllMain)
│
┌────────────────────────────────┘
▼
1. Parse PE image (base, .text, .data bounds)
2. Version guard: scan for "1.43.2.10687" — refuse if wrong
3. Resolve g_feature_bitset via RVA 0x1D9E670 (bounds-checked)
4. Immediate force: 14 × InterlockedExchange(0xFFFFFFFF)
5. Hook FeatureManager_set_features_from_uuids (Zydis trampoline)
→ calls original, then re-forces all bits
6. Guard thread: polls 2s, re-forces if any dword reverted
Two complementary mechanisms keep every feature bit on:
- Hook (
trampoline.h): an inline 14-bytejmp [rip+0]redirect on the populator function. After the original runs (so Plex's internal state is consistent), the hook atomically forces all 14 dwords to0xFFFFFFFF. This is deterministic — every MyPlex refresh immediately becomes "all-enabled." - Guard thread (
feature_patch.h): belt-and-suspenders. Polls at 2 s and re-forces if any dword reverted — catches code paths that write the bitset outside the hooked function, or a failed hook install.
Architecture
windows/
├── src/
│ ├── log.h zero-alloc OutputDebugString logging
│ ├── pe_image.h PE base, section bounds, version guard, RVA resolution
│ ├── sig_scan.h byte-pattern scanner + RIP-relative resolver
│ ├── trampoline.h x64 inline hook engine (14-byte, Zydis prologue decode)
│ ├── feature_patch.h orchestration: discover → hook → force → guard
│ ├── dllmain.cpp DLL entry (defers work to a thread: loader-lock safe)
│ └── injector.cpp attach-to-running or launch-suspended injector
├── build.bat zig 0.13.0 build (reuses vendored Zydis)
└── README.md
Layering (inward dependencies only):
| Layer | Module | Depends on |
|---|---|---|
| Primitives | log.h |
<windows.h> only |
| Discovery | pe_image.h |
log |
| Discovery | sig_scan.h |
nothing (pure) |
| Engine | trampoline.h |
log, Zydis |
| Orchestration | feature_patch.h |
pe_image, trampoline, log |
| Entry | dllmain.cpp |
feature_patch |
| Launcher | injector.cpp |
<windows.h> only (standalone binary) |
Design decisions
- RVA + version guard over blind signature scan as the primary discovery.
The version string must be present in the image before any hardcoded RVA is
used, so a wrong build gets a clean refusal, not memory corruption.
sig_scan.his included for future update-resilience. - Hook + guard thread (belt and suspenders) over either alone. The hook catches refreshes deterministically; the guard catches edge cases and compensates if the hook fails. Either alone would work; together they are robust.
- No Zydis for the injector. Only the DLL links Zydis (for prologue decode). The injector is a small standalone binary using only kernel32.
InterlockedExchangefor all bitset writes, matching the binary's own atomic store sequence exactly (not a memset; each dword is written atomically).- DllMain defers to a thread. Heavy work (PE parsing, hook install, guard spawn) runs outside the loader lock, avoiding the DllMain deadlock trap.
Security
- Version guard: the patch refuses to activate on an unknown build.
- Bounds-check: RVAs are validated against the PE section table.
- No shell: the injector passes an argv list, never a command string.
- The DLL logs to
OutputDebugString, never to disk (no file creation).
Build
Requires zig 0.13.0 (same as the Linux build; auto-downloaded to
toolchain/ by build.sh).
cd windows
build.bat
Outputs:
build\plex_patch.dll(723 KB — includes vendored Zydis)build\plex_inject.exe(153 KB)
Use
Attach to a running PMS:
build\plex_inject.exe
Launch PMS through the injector (recommended — hook installs before features load):
build\plex_inject.exe --launch "C:\Program Files\Plex\Plex Media Server\Plex Media Server.exe"
Copy both files to any directory; the injector resolves plex_patch.dll
relative to its own path.
Verify: open DebugView
and look for [plex_patch INF] messages:
[plex_patch INF] version guard passed (build 1.43.2.10687)
[plex_patch INF] g_feature_bitset at 0x7FF...
[plex_patch INF] hook installed on FeatureManager_set_features_from_uuids
[plex_patch INF] guard thread started (interval 2000 ms)
[plex_patch INF] feature bitset forced after set_features
Known values (build 1.43.2.10687)
| Symbol | RVA | Size | IDB name |
|---|---|---|---|
g_feature_bitset |
0x1D9E670 |
56 B (14 × DWORD) | g_feature_bitset |
FeatureManager_set_features_from_uuids |
0x0BC8060 |
— | FeatureManager_set_features_from_uuids |
g_feature_uuid_code_table |
0x19C9600 |
~2.2 KB (111 × 20 B) | g_feature_uuid_code_table |
FeatureManager_refresh_from_myplex |
0x0BC6D10 |
— | FeatureManager_refresh_from_myplex |
For a new PMS build: update kExpectedVersion, kBitsetRVA, and kPopulatorRVA
in feature_patch.h, or add a signature-scan fallback using sig_scan.h.
Differences from the Linux version
| Aspect | Linux (src/hook.cpp) |
Windows (windows/) |
|---|---|---|
| Injection | LD_PRELOAD |
CreateRemoteThread + LoadLibraryA |
| Module discovery | dl_iterate_phdr |
PE header parsing (GetModuleHandle) |
| Memory protection | mmap / mprotect |
VirtualAlloc / VirtualProtect |
| Cache flush | not needed (x86 coherent) | FlushInstructionCache (required by API) |
| Bitset storage | 14 × uint64 (libstdc++ std::bitset) |
14 × uint32 (MSVC std::bitset) |
| Guard thread | not needed (hook-only on Linux) | 2 s poll (belt-and-suspenders) |
| Disassembler | Zydis (vendored, same) | Zydis (vendored, same) |