- 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)
158 lines
5.5 KiB
C++
158 lines
5.5 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
//
|
|
// plex_inject.exe — Inject ``plex_patch.dll`` into Plex Media Server.
|
|
//
|
|
// Two modes:
|
|
// plex_inject.exe — find the running PMS, inject into it
|
|
// plex_inject.exe --launch PATH — spawn PMS suspended, inject, resume
|
|
//
|
|
// The DLL path is resolved relative to the injector's own location so the two
|
|
// files can live side by side anywhere on disk (no need to copy to Program Files).
|
|
//
|
|
// Elevation: the injector must run as the same user as PMS or as Administrator
|
|
// (OpenProcess needs PROCESS_ALL_ACCESS).
|
|
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#include <windows.h>
|
|
#include <tlhelp32.h>
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
|
|
// ---- helpers ---------------------------------------------------------------
|
|
|
|
static void err(const char* msg) {
|
|
std::fprintf(stderr, "[x] %s (GetLastError=%lu)\n", msg, ::GetLastError());
|
|
}
|
|
|
|
static bool get_own_dir(char* buf, size_t buflen) {
|
|
DWORD n = ::GetModuleFileNameA(nullptr, buf, static_cast<DWORD>(buflen));
|
|
if (n == 0 || n >= buflen) return false;
|
|
// Strip the exe name, keep trailing backslash.
|
|
char* last = std::strrchr(buf, '\\');
|
|
if (!last) last = std::strrchr(buf, '/');
|
|
if (last) *(last + 1) = '\0'; else buf[0] = '\0';
|
|
return true;
|
|
}
|
|
|
|
static DWORD find_process(const char* name) {
|
|
HANDLE snap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snap == INVALID_HANDLE_VALUE) return 0;
|
|
PROCESSENTRY32 pe{};
|
|
pe.dwSize = sizeof(pe);
|
|
DWORD pid = 0;
|
|
if (::Process32First(snap, &pe)) {
|
|
do {
|
|
if (_stricmp(pe.szExeFile, name) == 0) { pid = pe.th32ProcessID; break; }
|
|
} while (::Process32Next(snap, &pe));
|
|
}
|
|
::CloseHandle(snap);
|
|
return pid;
|
|
}
|
|
|
|
static bool inject_dll(HANDLE proc, const char* dll_path) {
|
|
const size_t path_len = std::strlen(dll_path) + 1;
|
|
|
|
// Allocate memory in the target for the DLL path string.
|
|
void* remote_buf = ::VirtualAllocEx(
|
|
proc, nullptr, path_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
if (!remote_buf) { err("VirtualAllocEx failed"); return false; }
|
|
|
|
if (!::WriteProcessMemory(proc, remote_buf, dll_path, path_len, nullptr)) {
|
|
err("WriteProcessMemory failed");
|
|
::VirtualFreeEx(proc, remote_buf, 0, MEM_RELEASE);
|
|
return false;
|
|
}
|
|
|
|
// LoadLibraryA is at the same address in every process (kernel32 is always
|
|
// mapped at its preferred base on Windows x64).
|
|
auto load_lib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
|
::GetProcAddress(::GetModuleHandleA("kernel32.dll"), "LoadLibraryA"));
|
|
if (!load_lib) { err("GetProcAddress(LoadLibraryA) failed"); return false; }
|
|
|
|
HANDLE thread = ::CreateRemoteThread(
|
|
proc, nullptr, 0, load_lib, remote_buf, 0, nullptr);
|
|
if (!thread) { err("CreateRemoteThread failed"); return false; }
|
|
|
|
::WaitForSingleObject(thread, 10000);
|
|
DWORD exit_code = 0;
|
|
::GetExitCodeThread(thread, &exit_code);
|
|
::CloseHandle(thread);
|
|
::VirtualFreeEx(proc, remote_buf, 0, MEM_RELEASE);
|
|
|
|
if (exit_code == 0) {
|
|
err("LoadLibraryA returned NULL in the target (DLL load failed)");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ---- main ------------------------------------------------------------------
|
|
|
|
int main(int argc, char** argv) {
|
|
std::printf("[*] plex_inject — Plex Media Server feature patch injector\n");
|
|
|
|
// Resolve the DLL path relative to the injector binary.
|
|
char dir[MAX_PATH]{};
|
|
if (!get_own_dir(dir, sizeof(dir))) { err("cannot determine own directory"); return 1; }
|
|
char dll_path[MAX_PATH]{};
|
|
std::snprintf(dll_path, sizeof(dll_path), "%splex_patch.dll", dir);
|
|
|
|
// Check the DLL exists before attempting injection.
|
|
if (::GetFileAttributesA(dll_path) == INVALID_FILE_ATTRIBUTES) {
|
|
std::fprintf(stderr, "[x] DLL not found: %s\n", dll_path);
|
|
return 1;
|
|
}
|
|
std::printf("[+] DLL: %s\n", dll_path);
|
|
|
|
bool launched = false;
|
|
HANDLE proc = nullptr;
|
|
HANDLE main_thread = nullptr;
|
|
DWORD pid = 0;
|
|
|
|
if (argc >= 3 && std::strcmp(argv[1], "--launch") == 0) {
|
|
// Spawn PMS suspended, inject before it runs.
|
|
STARTUPINFOA si{}; si.cb = sizeof(si);
|
|
PROCESS_INFORMATION pi{};
|
|
if (!::CreateProcessA(argv[2], nullptr, nullptr, nullptr, FALSE,
|
|
CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) {
|
|
err("CreateProcess failed");
|
|
return 1;
|
|
}
|
|
proc = pi.hProcess;
|
|
main_thread = pi.hThread;
|
|
pid = pi.dwProcessId;
|
|
launched = true;
|
|
std::printf("[+] launched PMS (pid %lu) suspended\n", pid);
|
|
} else {
|
|
// Attach to an already-running PMS.
|
|
pid = find_process("Plex Media Server.exe");
|
|
if (!pid) { err("Plex Media Server.exe not found (is it running?)"); return 1; }
|
|
proc = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
|
|
if (!proc) { err("OpenProcess failed (run as admin?)"); return 1; }
|
|
std::printf("[+] attached to PMS (pid %lu)\n", pid);
|
|
}
|
|
|
|
bool ok = inject_dll(proc, dll_path);
|
|
if (ok) {
|
|
std::printf("[+] plex_patch.dll injected into pid %lu\n", pid);
|
|
} else {
|
|
std::fprintf(stderr, "[x] injection failed\n");
|
|
}
|
|
|
|
if (launched) {
|
|
if (ok) {
|
|
::ResumeThread(main_thread);
|
|
std::printf("[+] PMS main thread resumed\n");
|
|
} else {
|
|
::TerminateProcess(proc, 1);
|
|
std::printf("[!] PMS terminated (injection failed)\n");
|
|
}
|
|
::CloseHandle(main_thread);
|
|
}
|
|
::CloseHandle(proc);
|
|
return ok ? 0 : 1;
|
|
}
|