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:
1 parent
5152e6e0a6
commit
2d6b2a3cd1
37 files changed
+1284
-451
No files matched your search
@@ -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"
|
||||
@@ -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}");
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 | ||||