Build offline OpenPrestige release bundles

Rename the injector and package it with an embedded payload for Windows, Linux, and macOS x64/ARM64. Add a core-only ImGui native build, multi-platform CI and release packaging, and shared per-user storage with legacy config migration.
This commit is contained in:
bbldrizzy committed 2026-11-03 18:45:13 +00:00
1 parent 5152e6e0a6
commit 2d6b2a3cd1
37 files changed
+1284 -451

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
[package]
name = "prestige-injector"
name = "openprestige-injector"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "PrestigeInjector"
name = "OpenPrestigeInjector"
path = "src/main.rs"
+11
View File
@@ -0,0 +1,11 @@
fn main() {
let properties = "../../../gradle.properties";
println!("cargo:rerun-if-changed={properties}");
let contents = std::fs::read_to_string(properties).expect("read gradle.properties");
let version = contents
.lines()
.find_map(|line| line.strip_prefix("mod_version="))
.expect("mod_version is required")
.trim();
println!("cargo:rustc-env=PRESTIGE_VERSION={version}");
}
+160 -79
View File
@@ -1,10 +1,12 @@
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
//! OpenPrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
//! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW
//! thread on Windows, the HotSpot attach protocol on Linux and macOS. The library does everything else once it is inside
//! the JVM; the injector tails its log so every stage (transforming, injecting, initialising) is visible.
//!
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc)
//! PrestigeInjector <pid> [lib path] non-interactive
//! OpenPrestigeInjector interactive process picker (Up/Down, Enter, Esc)
//! OpenPrestigeInjector <pid> [lib path] non-interactive
#[path = "../../paths.rs"]
mod paths;
mod platform;
use std::fs;
@@ -15,7 +17,6 @@ use std::time::{Duration, Instant};
/// The one credit line, shown once at the top of the screen.
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg";
/// How long to follow the DLL's progress after it has been loaded.
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
@@ -26,11 +27,15 @@ const GREEN: &str = "\x1b[92m";
const YELLOW: &str = "\x1b[93m";
const RED: &str = "\x1b[91m";
fn default_dll() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
fn prepare_runtime(_offline: bool, _ask_for_updates: bool) -> Result<PathBuf, String> {
let path = std::env::current_exe()
.map_err(|error| error.to_string())?
.with_file_name(platform::LIB_NAME);
if path.is_file() {
Ok(path)
} else {
Err(format!("OpenPrestige requires {} beside the CLI executable. Extract the complete offline bundle.", platform::LIB_NAME))
}
}
fn banner() -> String {
@@ -38,26 +43,53 @@ fn banner() -> String {
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"));
out.push_str(&format!(
" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"
));
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
out.push_str("OpenPrestige - offline CLI\n\n");
out
}
fn step(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", clock());
println!(
"{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}",
clock()
);
log_launcher(label, message);
}
fn ok(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", clock());
println!(
"{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}",
clock()
);
log_launcher(label, message);
}
fn fail(label: &str, message: &str) {
println!("{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}", clock());
println!(
"{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}",
clock()
);
log_launcher(label, message);
}
fn log_launcher(label: &str, message: &str) {
let path = paths::root()
.join("logs")
.join(format!("launcher-{}.log", std::process::id()));
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "[{}] {label} {message}", clock());
}
}
/// Seconds since the injector started, for the log prefix.
fn clock() -> String {
use std::sync::OnceLock;
static START: OnceLock<Instant> = OnceLock::new();
format!("{:.1}s", START.get_or_init(Instant::now).elapsed().as_secs_f32())
format!(
"{:.1}s",
START.get_or_init(Instant::now).elapsed().as_secs_f32()
)
}
fn main() -> ExitCode {
@@ -70,27 +102,47 @@ fn real_main() -> ExitCode {
platform::enable_ansi();
let _ = clock();
let args: Vec<String> = std::env::args().skip(1).collect();
let interactive = args.is_empty();
if let Err(error) = paths::initialize() {
fail("storage", &error.to_string());
return ExitCode::from(1);
}
let interactive = args.is_empty()
|| matches!(args.as_slice(), [flag] if flag == "--console" || flag == "--offline");
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
[] => {
open_invite_once();
(pick_process(), default_dll())
let (pid, dll): (Option<u32>, Result<PathBuf, String>) = match args.as_slice() {
_ if interactive => {
let library = prepare_runtime(args.iter().any(|arg| arg == "--offline"), true);
if let Err(error) = &library {
fail("runtime", error);
return ExitCode::from(1);
}
(pick_process(), library)
}
[pid] => (parse_pid(pid), default_dll()),
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
[pid] => (parse_pid(pid), prepare_runtime(true, false)),
[pid, dll] => (parse_pid(pid), Ok(PathBuf::from(dll))),
_ => {
eprintln!("usage: PrestigeInjector [<pid> [lib path]]");
eprintln!("usage: OpenPrestigeInjector [--console | --offline | <pid> [lib path]]");
return ExitCode::from(2);
}
};
let Some(pid) = pid else {
// interactive mode quit with Esc, or an unparsable pid was given
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
return if interactive {
ExitCode::SUCCESS
} else {
ExitCode::from(2)
};
};
if !interactive {
print!("{}", banner());
}
let dll = match dll {
Ok(path) => path,
Err(error) => {
fail("runtime", &error);
return ExitCode::from(1);
}
};
let code = run(pid, &dll);
if interactive {
@@ -121,35 +173,58 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
step("loader", &dll_display);
// The library appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
let log_path = dll.with_file_name("prestige-native.log");
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
let log_path = paths::native_log(pid);
let logs = [log_path.clone(), dll.with_file_name("prestige-native.log")];
let starts = logs
.each_ref()
.map(|path| fs::metadata(path).map(|m| m.len()).unwrap_or(0));
step("checking", "64-bit JVM, process access, library not already loaded");
step(
"checking",
"64-bit JVM, process access, library not already loaded",
);
step("injecting", "loading the native library into the JVM");
match platform::inject(pid, &dll) {
Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)),
Ok(()) => ok(
"injected",
&format!("{} is loaded in the game process", platform::LIB_NAME),
),
Err(e) => {
fail("failed", &e);
return ExitCode::from(1);
}
}
step("transforming", "following the loader log (attach, class loader, hook transformer)...");
match follow_log(&log_path, log_start) {
step(
"transforming",
"following the loader log (attach, class loader, hook transformer)...",
);
match follow_log(&logs, starts) {
Outcome::Done => {
println!();
ok("success", "successfully injected - Prestige is running in the game");
ok(
"success",
"successfully injected - Prestige is running in the game",
);
ExitCode::SUCCESS
}
Outcome::Failed(line) => {
fail("failed", &format!("injection did not complete: {line}"));
println!("{DIM} details: {} and the game's latest.log{RESET}", log_path.display());
println!(
"{DIM} details: {} and the game's latest.log{RESET}",
log_path.display()
);
ExitCode::from(1)
}
Outcome::TimedOut => {
fail("timeout", "no completion message from the loader within 3 minutes");
println!("{DIM} check {} and the game's latest.log{RESET}", log_path.display());
fail(
"timeout",
"no completion message from the loader within 3 minutes",
);
println!(
"{DIM} check {} and the game's latest.log{RESET}",
log_path.display()
);
ExitCode::from(1)
}
}
@@ -162,60 +237,61 @@ enum Outcome {
}
/// Prints the lines the loader/bootstrap append to the log until they report success or failure.
fn follow_log(path: &Path, start: u64) -> Outcome {
fn follow_log(paths: &[PathBuf; 2], mut offsets: [u64; 2]) -> Outcome {
let deadline = Instant::now() + FOLLOW_TIMEOUT;
let mut offset = start;
let mut pending = String::new();
let mut pending = [String::new(), String::new()];
while Instant::now() < deadline {
if let Ok(mut file) = fs::File::open(path) {
if file.seek(SeekFrom::Start(offset)).is_ok() {
let mut chunk = String::new();
if let Ok(n) = file.read_to_string(&mut chunk) {
offset += n as u64;
pending.push_str(&chunk);
for (index, path) in paths.iter().enumerate() {
let offset = &mut offsets[index];
let pending = &mut pending[index];
if let Ok(mut file) = fs::File::open(path) {
if file.seek(SeekFrom::Start(*offset)).is_ok() {
let mut chunk = String::new();
if let Ok(n) = file.read_to_string(&mut chunk) {
*offset += n as u64;
pending.push_str(&chunk);
}
}
}
}
while let Some(end) = pending.find('\n') {
let line: String = pending.drain(..=end).collect();
let line = line.trim_end();
// lines look like "[unix seconds] message"
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
let lower = message.to_ascii_lowercase();
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
let fatal = message.starts_with("FAILED:") || lower.contains("start failed") || lower.contains("client init failed") || lower.contains("could not hook");
if fatal {
fail("loader", message);
return Outcome::Failed(message.to_string());
while let Some(end) = pending.find('\n') {
let line: String = pending.drain(..=end).collect();
// Keep the output from older runtime bundles in the shared log too.
if index == 1 {
if let Ok(mut log) = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&paths[0])
{
if log.write_all(line.as_bytes()).is_ok() {
offsets[0] += line.len() as u64;
}
}
}
let line = line.trim_end();
// lines look like "[unix seconds] message"
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
let lower = message.to_ascii_lowercase();
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
let fatal = message.starts_with("FAILED:")
|| lower.contains("start failed")
|| lower.contains("client init failed")
|| lower.contains("could not hook");
if fatal {
fail("loader", message);
return Outcome::Failed(message.to_string());
}
if message.contains("client initialised") {
ok("loader", message);
return Outcome::Done;
}
step("loader", message);
}
if message.contains("client initialised") {
ok("loader", message);
return Outcome::Done;
}
step("loader", message);
}
std::thread::sleep(Duration::from_millis(200));
}
Outcome::TimedOut
}
fn config_path() -> Option<PathBuf> {
platform::config_dir().map(|dir| dir.join("PrestigeInjector").join("settings.txt"))
}
/// Opens the Discord invite on the very first launch only (no prompt); afterwards only the credit line shows it.
fn open_invite_once() {
let Some(path) = config_path() else { return };
if path.exists() {
return;
}
let _ = platform::open_url(INVITE_URL);
if let Some(dir) = path.parent() {
let _ = fs::create_dir_all(dir);
}
let _ = fs::write(&path, "invite_opened=true\n");
}
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
fn pick_process() -> Option<u32> {
let mut selected = 0usize;
@@ -229,7 +305,9 @@ fn pick_process() -> Option<u32> {
let previous = selected;
match key {
platform::Key::Up => selected = selected.saturating_sub(1),
platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
platform::Key::Down => {
selected = (selected + 1).min(processes.len().saturating_sub(1))
}
platform::Key::Enter => {
if let Some(p) = processes.get(selected) {
return Some(p.pid);
@@ -264,7 +342,10 @@ fn draw(processes: &[platform::JavaProcess], selected: usize) {
}
for (i, p) in processes.iter().enumerate() {
let marker = if i == selected { ">" } else { " " };
out.push_str(&format!("{marker} [{:>6}] {} ({})\n", p.pid, p.title, p.exe));
out.push_str(&format!(
"{marker} [{:>6}] {} ({})\n",
p.pid, p.title, p.exe
));
}
print!("{out}");
let _ = io::stdout().flush();
+2 -22
View File
@@ -1,6 +1,5 @@
//! Platform backends of the injector. Each backend exposes the same functions:
//! enable_ansi, poll_key, java_processes, inject, open_url, restore_terminal.
use std::path::PathBuf;
//! enable_ansi, poll_key, java_processes, inject, restore_terminal.
/// A running Minecraft JVM shown in the picker.
#[derive(PartialEq)]
@@ -33,7 +32,7 @@ pub use unix_attach::inject;
#[cfg(unix)]
pub use unix_procs::java_processes;
#[cfg(unix)]
pub use unix_term::{enable_ansi, open_url, poll_key, restore_terminal};
pub use unix_term::{enable_ansi, poll_key, restore_terminal};
#[cfg(windows)]
pub const LIB_NAME: &str = "prestige_native.dll";
@@ -41,22 +40,3 @@ pub const LIB_NAME: &str = "prestige_native.dll";
pub const LIB_NAME: &str = "libprestige_native.dylib";
#[cfg(all(unix, not(target_os = "macos")))]
pub const LIB_NAME: &str = "libprestige_native.so";
/// Per-user configuration directory: APPDATA, $XDG_CONFIG_HOME or ~/.config, ~/Library/Application Support.
pub fn config_dir() -> Option<PathBuf> {
#[cfg(windows)]
{
std::env::var_os("APPDATA").map(PathBuf::from)
}
#[cfg(target_os = "macos")]
{
std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library").join("Application Support"))
}
#[cfg(all(unix, not(target_os = "macos")))]
{
match std::env::var_os("XDG_CONFIG_HOME") {
Some(dir) if !dir.is_empty() => Some(PathBuf::from(dir)),
_ => std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")),
}
}
}
@@ -1,4 +1,4 @@
//! Terminal input and URL opening on Linux and macOS. std only (no termios structs).
//! Terminal input on Linux and macOS. std only (no termios structs).
//!
//! Raw mode is entered lazily on the first `poll_key` call via `stty -icanon -echo min 1 time 0`
//! on /dev/tty (ISIG stays on, so Ctrl-C still raises SIGINT). A reader thread blocks on the tty
@@ -37,8 +37,15 @@ fn tty_stdin() -> Stdio {
}
fn stty(args: &[&str]) -> Option<String> {
let out = Command::new("stty").args(args).stdin(tty_stdin()).stderr(Stdio::null()).output().ok()?;
out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
let out = Command::new("stty")
.args(args)
.stdin(tty_stdin())
.stderr(Stdio::null())
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
}
/// Nothing to do on unix (terminals understand ANSI); raw mode is entered lazily by `poll_key`.
@@ -113,7 +120,9 @@ pub fn poll_key() -> Option<Key> {
27 => {
let ms30 = Duration::from_millis(30);
match rx.recv_timeout(ms30) {
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => return Some(Key::Escape),
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => {
return Some(Key::Escape)
}
Ok(b'[') | Ok(b'O') => match rx.recv_timeout(ms30) {
Ok(b'A') => return Some(Key::Up),
Ok(b'B') => return Some(Key::Down),
@@ -134,19 +143,3 @@ pub fn restore_terminal() {
stty(&[&s]);
}
}
/// Opens a URL in the default browser (xdg-open / open). Returns whether the spawn succeeded.
pub fn open_url(url: &str) -> bool {
#[cfg(target_os = "macos")]
let opener = "open";
#[cfg(not(target_os = "macos"))]
let opener = "xdg-open";
Command::new(opener)
.arg(url)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map(|_| true)
.unwrap_or(false)
}
+106 -30
View File
@@ -39,16 +39,43 @@ struct ModuleEntry32W {
extern "system" {
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
fn CloseHandle(h: Handle) -> i32;
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
fn VirtualAllocEx(
process: Handle,
address: *mut c_void,
size: usize,
alloc_type: u32,
protect: u32,
) -> *mut c_void;
fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32;
fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle;
fn WriteProcessMemory(
process: Handle,
address: *mut c_void,
buf: *const c_void,
size: usize,
written: *mut usize,
) -> i32;
fn CreateRemoteThread(
process: Handle,
attrs: *mut c_void,
stack: usize,
start: *mut c_void,
param: *mut c_void,
flags: u32,
id: *mut u32,
) -> Handle;
fn WaitForSingleObject(h: Handle, ms: u32) -> u32;
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
fn GetModuleHandleW(name: *const u16) -> Handle;
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
fn IsWow64Process2(process: Handle, process_machine: *mut u16, native_machine: *mut u16)
-> i32;
fn GetCurrentProcess() -> Handle;
fn QueryFullProcessImageNameW(
process: Handle,
flags: u32,
buf: *mut u16,
size: *mut u32,
) -> i32;
fn GetLastError() -> u32;
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
@@ -66,11 +93,6 @@ extern "system" {
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32;
}
#[link(name = "shell32")]
extern "system" {
fn ShellExecuteW(hwnd: Handle, verb: *const u16, file: *const u16, params: *const u16, dir: *const u16, show: i32) -> isize;
}
extern "C" {
fn _kbhit() -> i32;
fn _getch() -> i32;
@@ -109,7 +131,10 @@ pub fn poll_key() -> Option<Key> {
}
fn wide(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
path.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn image_name(pid: u32) -> Option<String> {
@@ -156,7 +181,9 @@ pub fn java_processes() -> Vec<JavaProcess> {
if out.iter().any(|w| w.pid == pid) {
continue;
}
let Some(path) = image_name(pid) else { continue };
let Some(path) = image_name(pid) else {
continue;
};
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
out.push(JavaProcess { pid, title, exe });
@@ -181,7 +208,11 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
let mut found = false;
let mut ok = Module32FirstW(snapshot, &mut entry);
while ok != 0 {
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
let len = entry
.sz_module
.iter()
.position(|&c| c == 0)
.unwrap_or(entry.sz_module.len());
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
found = true;
break;
@@ -196,10 +227,16 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
unsafe {
let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
let access = PROCESS_CREATE_THREAD
| PROCESS_QUERY_INFORMATION
| PROCESS_VM_OPERATION
| PROCESS_VM_WRITE
| PROCESS_VM_READ;
let process = OpenProcess(access, 0, pid);
if process.is_null() {
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
return Err(last_error(
"OpenProcess (run as the same user, or as administrator)",
));
}
let result = inject_into(process, pid, dll);
CloseHandle(process);
@@ -208,18 +245,50 @@ pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
}
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
let mut wow64 = 0;
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
unsafe fn machine(process: Handle) -> Result<u16, String> {
let mut emulated = 0;
let mut native = 0;
if IsWow64Process2(process, &mut emulated, &mut native) == 0 {
return Err(last_error("IsWow64Process2"));
}
Ok(if emulated == 0 { native } else { emulated })
}
let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
let target = machine(process)?;
let injector = machine(GetCurrentProcess())?;
if target != injector {
return Err(format!("JVM architecture {target:#06x} differs from injector {injector:#06x}. Use matching x64 or ARM64 builds."));
}
let image = std::fs::read(dll).map_err(|error| format!("read loader: {error}"))?;
let pe = image
.get(0x3c..0x40)
.and_then(|bytes| bytes.try_into().ok())
.map(u32::from_le_bytes)
.ok_or("invalid loader PE header")? as usize;
let library = image
.get(pe + 4..pe + 6)
.and_then(|bytes| bytes.try_into().ok())
.map(u16::from_le_bytes)
.ok_or("invalid loader machine")?;
if library != target {
return Err("The loader DLL and JVM architectures differ.".into());
}
let file_name = dll
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
if module_loaded(pid, &file_name) {
return Err(format!("{file_name} is already loaded in process {pid}"));
}
let path = wide(dll);
let bytes = path.len() * 2;
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
let remote = VirtualAllocEx(
process,
std::ptr::null_mut(),
bytes,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
);
if remote.is_null() {
return Err(last_error("VirtualAllocEx"));
}
@@ -227,7 +296,14 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
Err(err)
};
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
if WriteProcessMemory(
process,
remote,
path.as_ptr() as *const c_void,
bytes,
std::ptr::null_mut(),
) == 0
{
return cleanup(last_error("WriteProcessMemory"));
}
@@ -237,7 +313,15 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
if load_library.is_null() {
return cleanup(last_error("resolve LoadLibraryW"));
}
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
let thread = CreateRemoteThread(
process,
std::ptr::null_mut(),
0,
load_library,
remote,
0,
std::ptr::null_mut(),
);
if thread.is_null() {
return cleanup(last_error("CreateRemoteThread"));
}
@@ -260,11 +344,3 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
}
Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)"))
}
/// Opens a URL in the default web browser.
pub fn open_url(url: &str) -> bool {
let verb: Vec<u16> = "open".encode_utf16().collect();
let target: Vec<u16> = url.encode_utf16().chain(std::iter::once(0)).collect();
// ShellExecute returns a value greater than 32 on success
unsafe { ShellExecuteW(std::ptr::null_mut(), verb.as_ptr(), target.as_ptr(), std::ptr::null(), std::ptr::null(), 1) > 32 }
}
+15 -4
View File
@@ -10,7 +10,10 @@ fn main() {
Err(_) => newest_remapped_jar(),
};
if !jar.is_file() {
panic!("payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)", jar.display());
panic!(
"payload jar not found: {} (run `gradlew remapJar` or set PRESTIGE_PAYLOAD)",
jar.display()
);
}
println!("cargo:rerun-if-changed={}", jar.display());
println!("cargo:rustc-env=PRESTIGE_PAYLOAD_JAR={}", jar.display());
@@ -22,14 +25,22 @@ fn newest_remapped_jar() -> PathBuf {
if let Ok(entries) = fs::read_dir(&libs) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with("prestige-") || !name.ends_with(".jar") || name.ends_with("-dev.jar") || name.ends_with("-sources.jar") {
if !name.starts_with("prestige-")
|| !name.ends_with(".jar")
|| name.ends_with("-dev.jar")
|| name.ends_with("-sources.jar")
{
continue;
}
let time = entry.metadata().and_then(|m| m.modified()).unwrap_or(std::time::UNIX_EPOCH);
let time = entry
.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::UNIX_EPOCH);
if best.as_ref().map_or(true, |(t, _)| time > *t) {
best = Some((time, entry.path()));
}
}
}
best.map(|(_, p)| p).unwrap_or_else(|| libs.join("prestige-0.1.0.jar"))
best.map(|(_, p)| p)
.unwrap_or_else(|| libs.join("prestige-0.1.0.jar"))
}
+2
View File
@@ -6,6 +6,8 @@
mod bridge;
mod jni;
mod log;
#[path = "../../paths.rs"]
mod paths;
mod worker;
use std::ffi::c_void;
+4 -46
View File
@@ -1,61 +1,19 @@
use std::ffi::c_void;
#[cfg(unix)]
use std::os::unix::ffi::OsStrExt;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::{SystemTime, UNIX_EPOCH};
#[cfg(windows)]
extern "system" {
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
}
#[cfg(unix)]
#[repr(C)]
struct DlInfo {
dli_fname: *const std::ffi::c_char,
dli_fbase: *mut c_void,
dli_sname: *const std::ffi::c_char,
dli_saddr: *mut c_void,
}
#[cfg(unix)]
extern "C" {
fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32;
}
#[cfg(unix)]
fn module_path() -> PathBuf {
fn anchor() {}
let mut info = DlInfo { dli_fname: std::ptr::null(), dli_fbase: std::ptr::null_mut(), dli_sname: std::ptr::null(), dli_saddr: std::ptr::null_mut() };
let ok = unsafe { dladdr(anchor as fn() as usize as *const c_void, &mut info) };
if ok != 0 && !info.dli_fname.is_null() {
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()))
} else {
std::env::temp_dir().join("libprestige_native")
}
}
#[cfg(windows)]
fn module_path() -> PathBuf {
let mut buf = [0u16; 1024];
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
PathBuf::from(String::from_utf16_lossy(&buf[..len]))
}
fn log_path() -> &'static PathBuf {
static PATH: OnceLock<PathBuf> = OnceLock::new();
PATH.get_or_init(|| {
module_path().with_file_name("prestige-native.log")
})
PATH.get_or_init(|| crate::paths::native_log(std::process::id()))
}
/// Appends a line to prestige-native.log beside the DLL.
pub fn log(message: &str) {
let secs = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
if let Some(parent) = log_path().parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_path()) {
let _ = writeln!(file, "[{secs}] {message}");
}
+4 -2
View File
@@ -29,6 +29,7 @@ pub fn run() {
}
unsafe fn install() -> Result<(), String> {
crate::paths::initialize().map_err(|error| format!("create OpenPrestige data directory: {error}"))?;
let vm = find_vm()?;
let env = attach_current_thread(vm)?;
struct Detach(VmPtr);
@@ -45,7 +46,7 @@ unsafe fn install() -> Result<(), String> {
log!("JVMTI ready");
let jar = write_payload()?;
log!("payload written to {}", jar.display());
log!("using payload at {}", jar.display());
let loader = add_to_knot(&jni, &jar)?;
log!("payload appended to the game class loader: {}", describe(&jni, loader));
@@ -103,7 +104,8 @@ unsafe fn find_vm() -> Result<VmPtr, String> {
}
fn write_payload() -> Result<PathBuf, String> {
let dir = std::env::temp_dir().join("PrestigeLoader");
let dir = crate::paths::initialize().map_err(|e| format!("create OpenPrestige data directory: {e}"))?
.join("runtime").join("payloads");
std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
let path = dir.join(format!("prestige-payload-{}.jar", std::process::id()));
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
+36
View File
@@ -0,0 +1,36 @@
use std::io;
use std::path::PathBuf;
fn env_path(name: &str) -> Option<PathBuf> {
std::env::var_os(name)
.filter(|path| !path.is_empty())
.map(PathBuf::from)
}
pub fn root() -> PathBuf {
#[cfg(windows)]
let base = env_path("APPDATA")
.or_else(|| env_path("USERPROFILE").map(|home| home.join("AppData/Roaming")));
#[cfg(target_os = "macos")]
let base = env_path("HOME").map(|home| home.join("Library/Application Support"));
#[cfg(all(unix, not(target_os = "macos")))]
let base = env_path("XDG_DATA_HOME")
.filter(|path| path.is_absolute())
.or_else(|| env_path("HOME").map(|home| home.join(".local/share")));
base.unwrap_or_else(std::env::temp_dir)
.join(".openprestige")
}
pub fn initialize() -> io::Result<PathBuf> {
let root = root();
for name in ["configs", "logs", "runtime"] {
std::fs::create_dir_all(root.join(name))?;
}
Ok(root)
}
pub fn native_log(pid: u32) -> PathBuf {
root()
.join("logs")
.join(format!("prestige-native-{pid}.log"))
}