From 72f4661bdc57344b4c2f03024e18ffe200702420 Mon Sep 17 00:00:00 2001 From: Benjamin Aarsen Date: Wed, 19 Aug 2026 22:33:42 +0200 Subject: [PATCH] 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) --- .dockerignore | 3 + .gitignore | 2 + Dockerfile | 98 +- Freeloader/.dockerignore | 76 + Freeloader/.gitattributes | 9 + Freeloader/.gitignore | 87 + Freeloader/AGENTS.md | 144 + Freeloader/FEEDBACK.md | 365 + Freeloader/LICENSE | 661 + Freeloader/README.md | 145 + Freeloader/build.sh | 182 + Freeloader/docker/Dockerfile.linuxserver | 116 + Freeloader/docker/Dockerfile.plexinc | 91 + Freeloader/docker/README.md | 113 + Freeloader/docker/plex-docker-patch.sh | 551 + Freeloader/docker/wrapper.sh | 36 + Freeloader/docs/BUILD.md | 107 + Freeloader/docs/DOCKER.md | 351 + Freeloader/docs/WINDOWS.md | 66 + Freeloader/experimental/debug_hook.c | 82 + Freeloader/plex_relay/.gitignore | 7 + Freeloader/plex_relay/README.md | 178 + Freeloader/plex_relay/conftest.py | 5 + Freeloader/plex_relay/pyproject.toml | 39 + .../plex_relay/src/plex_relay/__init__.py | 46 + Freeloader/plex_relay/src/plex_relay/cache.py | 97 + Freeloader/plex_relay/src/plex_relay/cli.py | 126 + .../plex_relay/src/plex_relay/config.py | 85 + .../plex_relay/src/plex_relay/controller.py | 162 + .../plex_relay/src/plex_relay/errors.py | 38 + Freeloader/plex_relay/src/plex_relay/keys.py | 105 + .../plex_relay/src/plex_relay/models.py | 110 + Freeloader/plex_relay/src/plex_relay/py.typed | 0 Freeloader/plex_relay/src/plex_relay/store.py | 65 + .../plex_relay/src/plex_relay/tunnel.py | 207 + Freeloader/plex_relay/tests/test_cache.py | 59 + Freeloader/plex_relay/tests/test_config.py | 44 + .../plex_relay/tests/test_controller.py | 157 + Freeloader/plex_relay/tests/test_keys.py | 70 + Freeloader/plex_relay/tests/test_models.py | 45 + Freeloader/plex_relay/tests/test_store.py | 61 + Freeloader/plex_relay/tests/test_tunnel.py | 91 + Freeloader/scripts/plex-crack-wrapper.sh | 14 + Freeloader/scripts/plex-tailnet/README.md | 172 + .../plex-tailnet/headscale-server-setup.sh | 185 + Freeloader/scripts/plex-tailnet/lib/common.sh | 76 + .../scripts/plex-tailnet/lib/plex_prefs.py | 114 + .../plex-tailnet/plex-tailscale-setup.sh | 374 + Freeloader/scripts/readbitset.py | 40 + Freeloader/src/hook.cpp | 798 + Freeloader/src/hook.hpp | 25 + Freeloader/src/main.cpp | 88 + Freeloader/src/traffic_logger.cpp | 487 + Freeloader/src/traffic_logger.hpp | 21 + Freeloader/src/webhook_handler.cpp | 1672 + Freeloader/src/webhook_handler.hpp | 33 + Freeloader/third_party/zydis/README.md | 12 + Freeloader/third_party/zydis/Zydis.c | 54990 ++++++++++++++++ Freeloader/third_party/zydis/Zydis.h | 12113 ++++ Freeloader/windows/README.md | 159 + Freeloader/windows/build.bat | 62 + Freeloader/windows/src/dllmain.cpp | 46 + Freeloader/windows/src/feature_patch.h | 186 + Freeloader/windows/src/injector.cpp | 157 + Freeloader/windows/src/log.h | 38 + Freeloader/windows/src/pe_image.h | 106 + Freeloader/windows/src/sig_scan.h | 97 + Freeloader/windows/src/trampoline.h | 110 + plexmediaserver_crack.so | Bin 9972072 -> 0 bytes scripts/discover_patterns.py | 507 + scripts/verify_signatures.py | 165 + wrapper.sh | 15 + 72 files changed, 77927 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Freeloader/.dockerignore create mode 100644 Freeloader/.gitattributes create mode 100644 Freeloader/.gitignore create mode 100644 Freeloader/AGENTS.md create mode 100644 Freeloader/FEEDBACK.md create mode 100644 Freeloader/LICENSE create mode 100644 Freeloader/README.md create mode 100644 Freeloader/build.sh create mode 100644 Freeloader/docker/Dockerfile.linuxserver create mode 100644 Freeloader/docker/Dockerfile.plexinc create mode 100644 Freeloader/docker/README.md create mode 100644 Freeloader/docker/plex-docker-patch.sh create mode 100644 Freeloader/docker/wrapper.sh create mode 100644 Freeloader/docs/BUILD.md create mode 100644 Freeloader/docs/DOCKER.md create mode 100644 Freeloader/docs/WINDOWS.md create mode 100644 Freeloader/experimental/debug_hook.c create mode 100644 Freeloader/plex_relay/.gitignore create mode 100644 Freeloader/plex_relay/README.md create mode 100644 Freeloader/plex_relay/conftest.py create mode 100644 Freeloader/plex_relay/pyproject.toml create mode 100644 Freeloader/plex_relay/src/plex_relay/__init__.py create mode 100644 Freeloader/plex_relay/src/plex_relay/cache.py create mode 100644 Freeloader/plex_relay/src/plex_relay/cli.py create mode 100644 Freeloader/plex_relay/src/plex_relay/config.py create mode 100644 Freeloader/plex_relay/src/plex_relay/controller.py create mode 100644 Freeloader/plex_relay/src/plex_relay/errors.py create mode 100644 Freeloader/plex_relay/src/plex_relay/keys.py create mode 100644 Freeloader/plex_relay/src/plex_relay/models.py create mode 100644 Freeloader/plex_relay/src/plex_relay/py.typed create mode 100644 Freeloader/plex_relay/src/plex_relay/store.py create mode 100644 Freeloader/plex_relay/src/plex_relay/tunnel.py create mode 100644 Freeloader/plex_relay/tests/test_cache.py create mode 100644 Freeloader/plex_relay/tests/test_config.py create mode 100644 Freeloader/plex_relay/tests/test_controller.py create mode 100644 Freeloader/plex_relay/tests/test_keys.py create mode 100644 Freeloader/plex_relay/tests/test_models.py create mode 100644 Freeloader/plex_relay/tests/test_store.py create mode 100644 Freeloader/plex_relay/tests/test_tunnel.py create mode 100644 Freeloader/scripts/plex-crack-wrapper.sh create mode 100644 Freeloader/scripts/plex-tailnet/README.md create mode 100644 Freeloader/scripts/plex-tailnet/headscale-server-setup.sh create mode 100644 Freeloader/scripts/plex-tailnet/lib/common.sh create mode 100644 Freeloader/scripts/plex-tailnet/lib/plex_prefs.py create mode 100644 Freeloader/scripts/plex-tailnet/plex-tailscale-setup.sh create mode 100644 Freeloader/scripts/readbitset.py create mode 100644 Freeloader/src/hook.cpp create mode 100644 Freeloader/src/hook.hpp create mode 100644 Freeloader/src/main.cpp create mode 100644 Freeloader/src/traffic_logger.cpp create mode 100644 Freeloader/src/traffic_logger.hpp create mode 100644 Freeloader/src/webhook_handler.cpp create mode 100644 Freeloader/src/webhook_handler.hpp create mode 100644 Freeloader/third_party/zydis/README.md create mode 100644 Freeloader/third_party/zydis/Zydis.c create mode 100644 Freeloader/third_party/zydis/Zydis.h create mode 100644 Freeloader/windows/README.md create mode 100644 Freeloader/windows/build.bat create mode 100644 Freeloader/windows/src/dllmain.cpp create mode 100644 Freeloader/windows/src/feature_patch.h create mode 100644 Freeloader/windows/src/injector.cpp create mode 100644 Freeloader/windows/src/log.h create mode 100644 Freeloader/windows/src/pe_image.h create mode 100644 Freeloader/windows/src/sig_scan.h create mode 100644 Freeloader/windows/src/trampoline.h delete mode 100644 plexmediaserver_crack.so create mode 100644 scripts/discover_patterns.py create mode 100644 scripts/verify_signatures.py create mode 100644 wrapper.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..924cc2c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +Freeloader/.git +Freeloader/.gitignore +.git diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a60b85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +*.pyc diff --git a/Dockerfile b/Dockerfile index 5a64692..6f768ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,85 @@ -FROM lscr.io/linuxserver/plex:latest +# syntax=docker/dockerfile:1.7 +# +# Multi-stage build with automatic signature discovery: +# 1. base — PMS base image (source of the binary to analyze) +# 2. discover — auto-discover hook patterns from the PMS binary +# 3. builder — cross-compile the .so with zig (musl) + generated patterns +# 4. runtime — layer onto lscr.io/linuxserver/plex with LD_PRELOAD wrapper +# +# Based on https://github.com/authrequest/Freeloader (AGPL-3.0-or-later). -# Install patchelf -RUN apt-get update && \ - apt-get install -y patchelf && \ - rm -rf /var/lib/apt/lists/* +ARG PLEX_BASE_IMAGE=lscr.io/linuxserver/plex:latest -# COPY plexmediaserver_crack.so /config/plexmediaserver_crack.so -# RUN chmod 644 /config/plexmediaserver_crack.so +# ── Stage 1: base (PMS image, used as source for discovery) ────────────── +FROM ${PLEX_BASE_IMAGE} AS base -# Download the Plex crack -RUN wget -O /tmp/plexmediaserver_crack.so \ - https://gitgud.io/yuv420p10le/plexmediaserver_crack/-/raw/master/binaries/plexmediaserver_crack.so && \ - cp /tmp/plexmediaserver_crack.so /config/plexmediaserver_crack.so && \ - chmod 644 /config/plexmediaserver_crack.so +# ── Stage 2: discover hook patterns from the PMS binary ───────────────── +# Auto-discovers byte patterns by analyzing the PMS binary. If a pattern +# can't be found, the build fails here — before compiling or shipping. +FROM debian:bookworm-slim AS discover +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 python3-pip \ + && pip3 install --break-system-packages capstone \ + && rm -rf /var/lib/apt/lists/* +COPY scripts/discover_patterns.py /tmp/discover_patterns.py +COPY --from=base /usr/lib/plexmediaserver/ /tmp/plex/ +RUN python3 /tmp/discover_patterns.py "/tmp/plex/Plex Media Server" \ + -o /tmp/patterns_generated.h \ + && cat /tmp/patterns_generated.h -# Apply the crack using patchelf -RUN PLEX_DIR=/usr/lib/plexmediaserver && \ - ln -sf /config/plexmediaserver_crack.so $PLEX_DIR/lib/plexmediaserver_crack.so && \ - patchelf --remove-needed plexmediaserver_crack.so $PLEX_DIR/lib/libsoci_core.so || true && \ - patchelf --add-needed plexmediaserver_crack.so $PLEX_DIR/lib/libsoci_core.so +# ── Stage 3: build the musl .so ─────────────────────────────────────────── +FROM debian:bookworm-slim AS builder + +ARG ZIG_VERSION=0.13.0 +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl xz-utils \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src +RUN mkdir -p /src/toolchain \ + && curl -fsSL \ + "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-x86_64-${ZIG_VERSION}.tar.xz" \ + -o /tmp/zig.tar.xz \ + && tar -C /src/toolchain --strip-components=1 -xf /tmp/zig.tar.xz \ + && rm /tmp/zig.tar.xz +ENV PATH="/src/toolchain:${PATH}" + +COPY Freeloader/build.sh ./ +COPY Freeloader/src ./src +COPY Freeloader/third_party ./third_party +COPY --from=discover /tmp/patterns_generated.h ./src/patterns_generated.h +RUN bash build.sh + +# ── Stage 4: runtime -- patch lscr.io/linuxserver/plex ──────────────────── +FROM ${PLEX_BASE_IMAGE} AS runtime + +ARG PLEX_BASE_IMAGE +ARG PATCH_VERSION=dev +LABEL org.opencontainers.image.title="plexmediaserver-crack (linuxserver)" \ + org.opencontainers.image.source="https://github.com/authrequest/Freeloader" \ + org.opencontainers.image.licenses="AGPL-3.0-or-later" \ + plex_patch.base="${PLEX_BASE_IMAGE}" \ + plex_patch.version="${PATCH_VERSION}" + +RUN set -eux; \ + PMS="/usr/lib/plexmediaserver/Plex Media Server"; \ + PMS_LIB="/usr/lib/plexmediaserver/lib"; \ + RUN_SCRIPT="/etc/s6-overlay/s6-rc.d/svc-plex/run"; \ + [ -x "${PMS}" ] || { echo "patcher: missing ${PMS} in ${PLEX_BASE_IMAGE}"; exit 1; }; \ + [ -d "${PMS_LIB}" ] || { echo "patcher: missing ${PMS_LIB}/ in ${PLEX_BASE_IMAGE}"; exit 1; }; \ + [ -f "${RUN_SCRIPT}" ] || { echo "patcher: missing ${RUN_SCRIPT} in ${PLEX_BASE_IMAGE}"; exit 1; } + +COPY --from=builder /src/build/plexmediaserver_crack.so \ + /usr/lib/plexmediaserver/lib/plexmediaserver_crack.so +RUN chmod 0644 /usr/lib/plexmediaserver/lib/plexmediaserver_crack.so +COPY wrapper.sh /usr/lib/plexmediaserver/plex-crack-wrapper.sh +RUN chmod 0755 /usr/lib/plexmediaserver/plex-crack-wrapper.sh + +RUN set -eux; \ + RUN_SCRIPT="/etc/s6-overlay/s6-rc.d/svc-plex/run"; \ + cp "${RUN_SCRIPT}" "${RUN_SCRIPT}.orig"; \ + printf '#!/usr/bin/with-contenv bash\nexec s6-setuidgid abc /usr/lib/plexmediaserver/plex-crack-wrapper.sh\n' \ + > "${RUN_SCRIPT}"; \ + chmod 0755 "${RUN_SCRIPT}" diff --git a/Freeloader/.dockerignore b/Freeloader/.dockerignore new file mode 100644 index 0000000..ee93965 --- /dev/null +++ b/Freeloader/.dockerignore @@ -0,0 +1,76 @@ +# .dockerignore — keep the Docker build context small. +# +# Both docker/Dockerfile.* only need: build.sh, src/, third_party/. +# Everything else is excluded so the daemon doesn't ship big/unrelated +# files into the buildkit context. + +# VCS / secrets +.git +.gitattributes +.mcp.json +.env +.env.* + +# Build outputs and toolchain +build/ +Build/ +build-*/ +dist/ +toolchain/ +compile_commands.json + +# Compiled artifacts +*.o +*.obj +*.lo +*.a +*.so +*.so.* +*.dll +*.dylib +*.exe +*.out + +# Plex binaries / IDA DBs +libsoci_core.so +Plex_Media_Server +*.i64 +*.idb +*.id0 +*.id1 +*.id2 +*.nam +*.til + +# Subprojects not part of the build +windows/ +plex_relay/ +experimental/ +scripts/ +docs/ + +# Docs and meta +AGENTS.md +README.md +LICENSE +.gitattributes + +# Python cruft +__pycache__/ +*.py[cod] +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +venv/ + +# OS / editor +.DS_Store +Thumbs.db +.idea/ +.vscode/ +.cache/ +*.tmp +*.log +*.bak +*.swp diff --git a/Freeloader/.gitattributes b/Freeloader/.gitattributes new file mode 100644 index 0000000..51c9127 --- /dev/null +++ b/Freeloader/.gitattributes @@ -0,0 +1,9 @@ +# Linux-targeted project: keep everything LF in the repo and on checkout. +# The /bin/sh + python launchers MUST stay LF or they break on the Plex host. +* text=auto eol=lf +*.sh text eol=lf +*.py text eol=lf +*.cpp text eol=lf +*.hpp text eol=lf +*.c text eol=lf +*.h text eol=lf diff --git a/Freeloader/.gitignore b/Freeloader/.gitignore new file mode 100644 index 0000000..1ecf0c1 --- /dev/null +++ b/Freeloader/.gitignore @@ -0,0 +1,87 @@ +# ─── Secrets / local machine config (NEVER COMMIT) ─────────────────────── +# .mcp.json here held a plaintext SSH password — keep all local creds out. +.mcp.json +.env +.env.* +*.pem +*.key +*_rsa +*_rsa.pub +id_ed25519* +id_rsa* + +# ─── Copyrighted Plex binaries (do NOT redistribute) ───────────────────── +/Plex_Media_Server +/Plex Media Server +pms.bin +libsoci_core.so + +# ─── IDA Pro databases (large; derived from the copyrighted binary) ────── +*.i64 +*.idb +*.id0 +*.id1 +*.id2 +*.nam +*.til + +# ─── Compiled output (from github/gitignore: C++) ──────────────────────── +*.o +*.obj +*.lo +*.slo +*.gch +*.pch +*.d +*.a +*.la +*.lai +*.lib +*.so +*.so.* +*.dylib +*.dll +*.out +*.app +*.exe + +# ─── Build dirs / generated ─────────────────────────────────────────────── +build/ +Build/ +build-*/ +dist/ +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +install_manifest.txt +compile_commands.json + +# ─── Toolchain auto-downloaded by build.sh ──────────────────────────────── +toolchain/ + +# ─── Python (plex_relay / scripts/plex-tailnet) ─────────────────────────── +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.venv/ +venv/ +.tox/ + +# ─── Temp / logs ────────────────────────────────────────────────────────── +*.tmp +*.log +*.bak +*.swp +*~ +.cache/ + +# ─── OS / editor cruft ──────────────────────────────────────────────────── +.DS_Store +Thumbs.db +desktop.ini +.idea/ +.vscode/ diff --git a/Freeloader/AGENTS.md b/Freeloader/AGENTS.md new file mode 100644 index 0000000..05557af --- /dev/null +++ b/Freeloader/AGENTS.md @@ -0,0 +1,144 @@ +# 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 Server` executable (PIE). The feature machinery + moved here from `libsoci_core.so` on post-2024/08/13 builds; `libsoci_core.so` + is 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_phdr` used in early versions; current code + uses `/proc/self/maps` (the constructor runs before the main PIE is loaded by + musl, so `dl_iterate_phdr` returns 0 callbacks). +- **Hook:** `sig_scan()` finds target functions by byte-pattern signature; + `create_hook()` installs a 14-byte `jmp [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_slots` qwords 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 at `0x122B2F2`). When the key is `"WebHooksEnabled"`, it + returns `true` regardless 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 — `WebHooksEnabled` is a plain boolean + preference, not a feature bit. Both dispatch functions (`sub_125A6D4` @ + `0x125A6D4` and `sub_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.h` is conditionally excluded via + `#ifndef __aarch64__`. +- **Module discovery:** same `/proc/self/maps` parser (`get_dottext_info()`) + works identically on ARM64. +- **Hook:** `create_hook_arm64()` installs a 16-byte + `LDR 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.py` uses Capstone to extract ARM64 + function signatures with ADRP page counting; `check_webhooks.py` dumps + ADRP+ADD string loads inside candidate functions; `disasm_final.py` / + `disasm_key_areas.py` are 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_PRELOAD`** via `scripts/plex-crack-wrapper.sh` + a systemd + +## Key files + +- `src/hook.cpp` / `src/hook.hpp` — hook engine, feature logic, feature-UUID catalog +- `src/main.cpp` — library constructor (`unsetenv("LD_PRELOAD")` then `hook()`) +- `build.sh` — musl build via zig (auto-downloaded) with an ABI sanity gate +- `scripts/plex-crack-wrapper.sh` — `LD_PRELOAD` launcher scoped to the PMS process +- `scripts/readbitset.py` — live feature-bitset verifier +- `third_party/zydis/` — vendored Zydis disassembler (MIT) +- `get_arm64_sigs.py` — Capstone-based ARM64 function signature extractor with ADRP page counting +- `check_webhooks.py` — dump ADRP+ADD string loads within candidate ARM64 functions +- `experimental/debug_hook.c` — standalone alternate hook (legacy `is_feature_available` signature) + +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. | diff --git a/Freeloader/FEEDBACK.md b/Freeloader/FEEDBACK.md new file mode 100644 index 0000000..6a8f463 --- /dev/null +++ b/Freeloader/FEEDBACK.md @@ -0,0 +1,365 @@ +# FEEDBACK.md — load this at the start of every session + +> **Read this file at the start of every session on this project.** +> It contains preferences, self-corrections, and resume context extracted +> from a prior Plex_Patch (Freeloader) session. +> +> **Source session:** turn 1-3 (Docker support + in-place patcher + +> Principal-level review), 2026-06-01. Reviewed by the user. +> +> **If the file gets long, trim it.** Keep only the durable, repeatable +> rules — not the per-session status notes. + +--- + +## 1 · User preferences (extracted from how they actually work) + +- **Hands-off, trusting style.** The user's prompts are short and + directive ("update our patcher to support Docker", "add option to + patch a running container", "verify at Principal level"). They + expect me to figure out the *how* once they give the *what*. +- **One-shot prompts, not iterative.** Each user turn is a complete + scope expansion. They do not iterate on micro-decisions. +- **Trusts my decisions.** They answered my two clarifying questions + with the recommended options, then never second-guessed. Don't + re-ask what I can decide. +- **Values quality over speed.** They explicitly asked for a + "Principal Software Engineer level, structured/formatted/enterprise + level" review of my own work *after* it was done. Match that + quality bar from the start. +- **Wants resumability.** They asked "What did we do so far?" mid- + session. They value the ability to context-switch. +- **Wants the safety valve.** "Continue if you have next steps, or + stop and ask for clarification if you are unsure how to proceed." + This is a good model — finish the work but stop when genuinely + blocked. +- **Wants me to do the prep.** "Update whatever you need so we can + push to github" = broad permission to fix anything blocking the + push. Do the audit, do the fixes, then commit and push. +- **Likes tabular structured output.** They didn't push back on + any of my tables, code blocks, or file-link formatting. Keep + using `| col | col |` tables and `file:///` links for file refs. + +## 2 · Commit style for this repo (Freeloader / Plex_Patch) + +Style observed in `git log` — match it: + +``` +Add top-level Windows patching doc index +Add Windows x64 godmode DLL + injector +Add relay reimplementation + remote-access tooling; label Remote Watch Pass +Add GNU AGPL-3.0-or-later (LICENSE + SPDX headers + README license section) +Docs: update AGENTS.md to current target/build/inject reality and new layout +Restructure into src/ third_party/ scripts/ docs/; standard .gitignore; drop stale duplicates and orphaned CM... +``` + +Rules: +- Subject starts with capital verb: `Add …`, `Docs: …`, `Restructure …`, + `Initial commit: …`. No `feat:` / `fix:` / `chore:` conventional-commits + prefixes. +- Subject ≤ 72 chars, concise. +- Body (when present) explains *what* + *why* and any non-obvious + decisions. Multiple paragraphs separated by blank lines are fine. +- One atomic commit per feature, not per file. + +## 3 · Things I should do differently next time (self-corrections) + +These are the actual mistakes I made this session: + +1. **Don't claim behavior I haven't verified.** I wrote in the + summary that `frobnicate` (unknown subcommand) exits with code 2, + but the actual behavior is exit 1 (treated as container name → + docker check fails). Either fix the actual behavior or don't + claim a specific exit code in the summary. + +2. **Don't fire background agents I won't engage with.** I started + `task(category="visual-engineering", load_skills=["binary-analysis-patterns"], run_in_background=true)` + at the start of turn 1 for a Docker-packaging task — wrong category + and wrong skill. The agent was never used. Either use the + background agent or don't start it. Background agents are + expensive; do not start speculatively. + +3. **Don't re-read files I just wrote.** I read + `docker/plex-docker-patch.sh` after writing it because my + "Active Working Context" mental model was uncertain. Trust the + context I have. Re-read only if I've genuinely lost track (e.g., + after a compaction or long turn gap). + +4. **Keep summaries concise and resume-actionable.** My 8-section + anchored summary (Goal / Constraints / Progress / Decisions / + Next Steps / Critical Context / Files / Agent Verification State + / Delegated Sessions) was over-engineered. The user wanted + "what did we do so far", not a meta-analysis of my own process. + Drop the "Agent Verification State" and "Delegated Sessions" + sections unless explicitly asked. + +5. **Don't include "Active Working Context" sections in summaries.** + They are a mental model, not the actual file content. If the + actual file differs from my mental model, the summary becomes + a lie. Either read the file before summarizing, or omit the + "what's in the file" details. + +6. **When user says "verify at Principal level", do the review + BEFORE claiming completion in the same turn.** I finished the + implementation, then ran the review in a later turn. Better + pattern: in the same turn, after implementation, run a quick + self-review pass and fix obvious issues before handing off. + Saves a turn. + +7. **When in-place rewrites are large (e.g., 337 → 552 lines), + state the scope at the top of the summary.** "Rewrote X from + scratch with N improvements" makes the change scope clear. + +8. **When the user says "update whatever you need so we can push + to github", do the pre-push audit as a checklist:** + - No secrets in any new file (`git grep -nE 'password|api[_-]?key|token|secret|bearer'`) + - No active git hooks (`.git/hooks/` all `.sample`?) + - `.gitignore` covers all sensitive patterns? + - Git identity set? + - `core.fileMode` and `core.autocrlf` consistent with repo? + - All changes intentional (`git status` matches plan)? + Then commit + push. Don't ask for re-confirmation. + +9. **Use `bash -n` + smoke tests in PARALLEL, not sequentially.** + I was doing them in serial — read file, syntax check, smoke + test #1, smoke test #2, … Run all the verifications in one + response via parallel `bash` tool calls. + +10. **PowerShell on Windows — common gotchas to remember:** + - `head` is not a valid command. Use `Get-Content -TotalCount N` + or `Select-Object -First N`. Or just don't truncate in the + bash command. + - `$?` is a **boolean** (success/failure), not an exit code. + Use `$LASTEXITCODE` for the numeric exit code. + - `ls -la` is not valid. Use `Get-ChildItem -LiteralPath X`. + - Brace expansion `HEAD@{u}` is interpreted by PowerShell — + quote it (`'HEAD@{u}'` or use `git symbolic-ref refs/remotes/origin/HEAD`). + - `bash -n C:\path\file` fails on Windows paths. Use a + relative path with the `workdir` parameter. + +## 4 · Patterns that worked (keep doing these) + +- **Arg-parser with single `case` loop, mutual-exclusion checks + at the end.** Cleaner than scattered checks per-flag. +- **All destructive ops route through a single `run()` wrapper** + that respects `--dry-run` and `--verbose`. Single place to see + side effects. The pattern from `plex-docker-patch.sh` is good — + reuse it for future scripts. +- **Container shell commands use `docker exec sh -c '...' _ "${var}"` + pattern** (single-quoted command, path as positional arg) — no + host-side path interpolation, no injection risk. +- **Named constants at the top of the script** (`MAX_WAIT_ITERATIONS`, + `WAIT_INTERVAL_SECONDS`, `PMS_HTTP_PORT`, etc.) — easier to tune, + easier to read. +- **Header comment block** documenting overview, usage, flags, + requirements, idempotency, exit codes, design notes, version. + This is the "enterprise README inline" pattern. +- **Idempotency statement in the header.** "install: safe to + re-run. The .orig is preserved across re-installs, the .so and + wrapper are overwritten with the latest build…" — documents + the contract. +- **Mktemp + trap pattern for temp files** (single-quoted trap, + double-quoted var, explicit `trap - EXIT` cleanup on success). +- **Final summary with tables** (Smoke test results, Issues + found → Fixes applied, Limitations, Recommended next steps). + The user read and engaged with this format. +- **Pre-existing failures explicitly noted.** "Done. Note: N + pre-existing errors unrelated to my changes." — distinguishes + my work from prior state. + +## 5 · Project context (resume faster next time) + +- **Project:** Plex_Patch (fork: **Freeloader**). +- **Remote:** `https://github.com/authrequest/Freeloader.git`. +- **Git identity (already configured, do not change):** + `authrequest `. +- **`core.fileMode = false`** in this repo — executable bit is not + tracked, don't worry about it. +- **`core.autocrlf = true`** — Windows line endings are normal. +- **Target:** Plex Media Server on Linux x86_64. +- **Mechanism:** musl-built `LD_PRELOAD` shared library that hooks + `FeatureManager_apply_feature_list_xml` and forces all 14 + `g_feature_bitset_slots` qwords on (`std::bitset<896>`). +- **Build:** zig 0.13.0 cross-compile to `x86_64-linux-musl`. + `bash build.sh` from the project root. Build artifact: + `build/plexmediaserver_crack.so`. +- **Injection (native):** `LD_PRELOAD` via `scripts/plex-crack-wrapper.sh` + + systemd drop-in. **Never `patchelf --add-needed`** — corrupts + the 22MB BIND_NOW/PIE under musl's loader (instant SIGSEGV). +- **Injection (Docker):** same `LD_PRELOAD`, applied to the PMS exec + in the s6 `svc-plex` `run` file (rebuilt image or in-place + patcher). The .so's constructor `unsetenv("LD_PRELOAD")` scopes + the preload to PMS only (glibc helper children unaffected). +- **Languages:** C++20, bash, Python (plex_relay / plex-tailnet). +- **Vendored:** Zydis disassembler in `third_party/zydis/` (MIT). +- **Layout:** + - `src/` — `main.cpp` (constructor), `hook.cpp` / `hook.hpp` + (signature scan, Zydis-disassembled trampoline) + - `build.sh` — musl build + ABI sanity gate + - `scripts/` — `plex-crack-wrapper.sh` (native systemd), + `readbitset.py` (live verifier), `plex-tailnet/` + - `docker/` — `Dockerfile.plexinc`, `Dockerfile.linuxserver`, + `wrapper.sh`, `plex-docker-patch.sh`, `README.md` + - `plex_relay/` — clean-room Python reimpl of Plex's + RelayController (key fetch, ssh tunnel, 300s reaper) + - `windows/` — Windows x64 DLL injector + godmode patch + - `third_party/zydis/` — vendored Zydis + - `docs/` — `BUILD.md`, `DOCKER.md`, `WINDOWS.md` + - `AGENTS.md` — architecture / RE notes + - `experimental/debug_hook.c` — legacy alternate hook + - `LICENSE` — AGPL-3.0-or-later +- **Git-ignored:** `Plex Media Server` binary, `libsoci_core.so`, + `*.i64` / `*.idb` IDA DBs, `build/`, `toolchain/`, `.mcp.json`, + `.env*`, `*.pem`, `*.key`, `id_*` SSH keys. +- **Docker images patched (turn 1):** + - `plexinc/pms-docker` — official + - `lscr.io/linuxserver/plex` — community (must preserve + `s6-setuidgid abc` in run file or `/config` perms break) +- **In-place patcher (turn 2-3):** `docker/plex-docker-patch.sh` + v1.0.0 with install/uninstall/status subcommands + full flag + surface. See the file's header for the design notes. +- **Limitations:** x86_64 only (no arm64 PMS Docker image today). + LSIO requires preserving `s6-setuidgid abc`. Plex bundles its + own musl libc + libgcompat — glibc `.so` cannot be loaded. + +## 6 · Templates (re-use these) + +### Pre-push audit checklist +``` +[ ] git status — all changes intentional, no unexpected files +[ ] git diff --stat — sizes look right +[ ] git grep -nE 'password|api[_-]?key|token|secret|bearer' -- +[ ] .git/hooks/ — all *.sample, no active hooks +[ ] git config --get user.{name,email} — set +[ ] git remote -v — right remote +[ ] core.fileMode / core.autocrlf — match repo +[ ] bash -n — passes +[ ] commit message — matches repo style (capital verb, no conventional prefix) +``` + +### Bash script header template (from plex-docker-patch.sh) +```bash +#!/usr/bin/env bash +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# +# +# +# ── Overview ────────────────────────────────────────────────────── +# +# +# ── Usage ───────────────────────────────────────────────────────── +#