Files
plex-docker/Freeloader/windows/README.md
T
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

6.7 KiB
Raw Blame History

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-byte jmp [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 to 0xFFFFFFFF. 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.h is 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.
  • InterlockedExchange for 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)