Files
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

160 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
# 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`).
```bat
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:**
```bat
build\plex_inject.exe
```
**Launch PMS through the injector (recommended — hook installs before features load):**
```bat
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](https://learn.microsoft.com/en-us/sysinternals/downloads/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) |