Add Linux and macOS support, batch class transformations with a disk cache, and add cross-platform release packaging. Also improve runtime namespace handling, keybind editing, and startup behavior.
This commit is contained in:
bbldrizzy committed 2026-10-27 20:30:09 +00:00
1 parent 910a126f04
commit 38ff6d387c
29 files changed
+2234 -491

No files matched your search

+58 -72
View File
@@ -1,10 +1,11 @@
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads prestige_native.dll into the selected one
//! with the classic remote LoadLibraryW thread. The DLL does everything else once it is inside the JVM; the injector tails
//! the DLL's log so every stage (transforming, injecting, initialising) is visible.
//! PrestigeInjector: 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.exe interactive process picker (Up/Down, Enter, Esc)
//! PrestigeInjector.exe <pid> [dll path] non-interactive
mod win;
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc)
//! PrestigeInjector <pid> [lib path] non-interactive
mod platform;
use std::fs;
use std::io::{self, Read, Seek, SeekFrom, Write};
@@ -12,9 +13,9 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::{Duration, Instant};
const DLL_NAME: &str = "prestige_native.dll";
const WATERMARK: &str = "Cracked by Skid Solutions";
const INVITE_URL: &str = "https://discord.gg/M56rd2wwC5";
/// 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);
@@ -28,16 +29,18 @@ const RED: &str = "\x1b[91m";
fn default_dll() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|exe| exe.parent().map(|dir| dir.join(DLL_NAME)))
.unwrap_or_else(|| PathBuf::from(DLL_NAME))
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
}
fn banner() {
println!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ ");
println!(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|");
println!(" | _/ / _|\\__ \\ | | | | (_ | _| ");
println!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector");
println!("{YELLOW} {WATERMARK}{RESET}\n");
fn banner() -> String {
let mut out = String::new();
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!("{YELLOW} {CREDIT}{RESET}\n\n"));
out
}
fn step(label: &str, message: &str) {
@@ -58,21 +61,26 @@ fn clock() -> String {
}
fn main() -> ExitCode {
win::enable_ansi();
let code = real_main();
platform::restore_terminal();
code
}
fn real_main() -> ExitCode {
platform::enable_ansi();
let _ = clock();
let args: Vec<String> = std::env::args().skip(1).collect();
let interactive = args.is_empty();
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
[] => {
banner();
invite_prompt();
open_invite_once();
(pick_process(), default_dll())
}
[pid] => (parse_pid(pid), default_dll()),
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
_ => {
eprintln!("usage: PrestigeInjector.exe [<pid> [dll path]]");
eprintln!("usage: PrestigeInjector [<pid> [lib path]]");
return ExitCode::from(2);
}
};
@@ -81,7 +89,7 @@ fn main() -> ExitCode {
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
};
if !interactive {
banner();
print!("{}", banner());
}
let code = run(pid, &dll);
@@ -109,16 +117,18 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
}
};
let dll_display = dll.display().to_string();
step("loader", &dll_display.trim_start_matches(r"\\?\").to_string());
#[cfg(windows)]
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
step("loader", &dll_display);
// The DLL appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
// 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);
step("checking", "64-bit JVM, process access, DLL not already loaded");
step("injecting", "remote thread LoadLibraryW");
match win::inject(pid, &dll) {
Ok(()) => ok("injected", "prestige_native.dll is loaded in the game process"),
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)),
Err(e) => {
fail("failed", &e);
return ExitCode::from(1);
@@ -130,7 +140,6 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
Outcome::Done => {
println!();
ok("success", "successfully injected - Prestige is running in the game");
println!("{YELLOW} {WATERMARK}{RESET} {DIM}{INVITE_URL}{RESET}");
ExitCode::SUCCESS
}
Outcome::Failed(line) => {
@@ -173,7 +182,9 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
// lines look like "[unix seconds] message"
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
let lower = message.to_ascii_lowercase();
if lower.contains("failed") || lower.contains("not found") {
// 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());
}
@@ -189,51 +200,26 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
}
fn config_path() -> Option<PathBuf> {
std::env::var_os("APPDATA").map(|dir| PathBuf::from(dir).join("PrestigeInjector").join("settings.txt"))
platform::config_dir().map(|dir| dir.join("PrestigeInjector").join("settings.txt"))
}
/// First launch: open the Discord invite, then ask whether to open it again on later launches.
fn invite_prompt() {
/// 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 };
let saved = fs::read_to_string(&path).ok();
let show = match saved.as_deref() {
None => true, // first launch
Some(text) => !text.lines().any(|l| l.trim() == "show_invite=false"),
};
if !show {
if path.exists() {
return;
}
let first = saved.is_none();
step("discord", &format!("opening {INVITE_URL}"));
if !win::open_url(INVITE_URL) {
fail("discord", &format!("could not open a browser, visit {INVITE_URL} manually"));
}
let again = ask_yes_no(if first {
"Show the Discord invite again next time you start the injector? [Y/n] "
} else {
"Keep showing the Discord invite on launch? [Y/n] "
});
let _ = platform::open_url(INVITE_URL);
if let Some(dir) = path.parent() {
let _ = fs::create_dir_all(dir);
}
let _ = fs::write(&path, format!("show_invite={again}\n"));
println!();
}
fn ask_yes_no(question: &str) -> bool {
print!("{question}");
let _ = io::stdout().flush();
let mut line = String::new();
if io::stdin().read_line(&mut line).is_err() {
return true;
}
!matches!(line.trim().to_ascii_lowercase().as_str(), "n" | "no")
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;
let mut processes = win::java_windows();
let mut processes = platform::java_processes();
let mut last_refresh = Instant::now();
loop {
if selected >= processes.len() {
@@ -242,34 +228,34 @@ fn pick_process() -> Option<u32> {
draw(&processes, selected);
let deadline = Instant::now() + Duration::from_millis(50);
while Instant::now() < deadline {
if let Some(key) = win::poll_key() {
if let Some(key) = platform::poll_key() {
match key {
win::Key::Up => selected = selected.saturating_sub(1),
win::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
win::Key::Enter => {
platform::Key::Up => selected = selected.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);
}
}
win::Key::Escape => return None,
platform::Key::Escape => return None,
}
draw(&processes, selected);
}
std::thread::sleep(Duration::from_millis(10));
}
if last_refresh.elapsed() >= Duration::from_millis(750) {
processes = win::java_windows();
processes = platform::java_processes();
last_refresh = Instant::now();
}
}
}
fn draw(processes: &[win::JavaWindow], selected: usize) {
fn draw(processes: &[platform::JavaProcess], selected: usize) {
let mut out = String::from("\x1b[2J\x1b[H");
out.push_str(&format!("{CYAN}Prestige injector{RESET} - Minecraft 1.21.11 (Fabric, 64-bit) {YELLOW}{WATERMARK}{RESET}\n"));
out.push_str("Up/Down select, Enter inject, Esc quit\n\n");
out.push_str(&banner());
out.push_str("Minecraft 1.21.11 (Fabric, 64-bit) - Up/Down select, Enter inject, Esc quit\n\n");
if processes.is_empty() {
out.push_str(" waiting for a javaw.exe / java.exe window...\n");
out.push_str(" waiting for a Minecraft JVM (java/javaw)...\n");
}
for (i, p) in processes.iter().enumerate() {
let marker = if i == selected { ">" } else { " " };
@@ -0,0 +1,61 @@
//! 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;
/// A running Minecraft JVM shown in the picker.
pub struct JavaProcess {
pub pid: u32,
pub title: String,
pub exe: String,
}
pub enum Key {
Up,
Down,
Enter,
Escape,
}
#[cfg(windows)]
mod win;
#[cfg(windows)]
pub use win::*;
#[cfg(unix)]
mod unix_attach;
#[cfg(unix)]
mod unix_procs;
#[cfg(unix)]
mod unix_term;
#[cfg(unix)]
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};
#[cfg(windows)]
pub const LIB_NAME: &str = "prestige_native.dll";
#[cfg(target_os = "macos")]
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")),
}
}
}
@@ -0,0 +1,405 @@
//! HotSpot attach-protocol injection for Linux and macOS (what `jcmd <pid> JVMTI.agent_load` does).
//!
//! 1. Create `.attach_pid<pid>` where the target JVM looks for it and send SIGQUIT.
//! 2. Wait for the UNIX socket `.java_pid<pid>`.
//! 3. Send `1\0load\0<abs lib>\0true\0\0` and read the status reply.
//!
//! The JVM then dlopens the library and calls `Agent_OnAttach`.
#![allow(dead_code)]
use std::fs;
use std::io::{Read, Write};
use std::os::unix::fs::MetadataExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::{Duration, Instant};
extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
fn geteuid() -> u32;
}
const SIGQUIT: i32 = 3;
const SOCKET_WAIT: Duration = Duration::from_secs(10);
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const REPLY_TIMEOUT: Duration = Duration::from_secs(30);
const IS_MACOS: bool = cfg!(target_os = "macos");
pub fn inject(pid: u32, lib: &Path) -> Result<(), String> {
let lib = fs::canonicalize(lib)
.map_err(|e| format!("library '{}' not found: {e}", lib.display()))?;
let lib_str = lib
.to_str()
.ok_or_else(|| "library path is not valid UTF-8".to_string())?;
preflight_owner(pid)?;
if let Some(msg) = arch_mismatch(pid) {
return Err(msg);
}
let sockets = socket_candidates(pid);
let existing = sockets.iter().find(|p| is_socket(p)).cloned();
let sock_path = match existing {
Some(p) => p,
None => trigger_attach(pid, &sockets)?,
};
send_load(&sock_path, lib_str)
}
// ---------------------------------------------------------------- preflight
fn current_uid() -> u32 {
unsafe { geteuid() }
}
fn preflight_owner(pid: u32) -> Result<(), String> {
let uid = if IS_MACOS {
let out = Command::new("ps")
.args(["-o", "uid=", "-p", &pid.to_string()])
.output()
.map_err(|e| format!("failed to run ps: {e}"))?;
let text = String::from_utf8_lossy(&out.stdout);
let text = text.trim();
if text.is_empty() {
return Err(format!("process {pid} does not exist"));
}
text.parse::<u32>()
.map_err(|_| format!("could not parse owner of process {pid}: '{text}'"))?
} else {
match fs::metadata(format!("/proc/{pid}")) {
Ok(m) => m.uid(),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(format!("process {pid} does not exist"))
}
Err(e) => return Err(format!("cannot inspect process {pid}: {e}")),
}
};
let me = current_uid();
if uid != me {
return Err(format!(
"process {pid} belongs to uid {uid} but the injector runs as uid {me}; \
the attach protocol only works for the same user (run the injector as the user that started the game)"
));
}
Ok(())
}
fn host_arch() -> &'static str {
match std::env::consts::ARCH {
"aarch64" | "arm64" => "arm64",
other => other,
}
}
/// Some(message) only on a definite mismatch; None when matching or undeterminable.
fn arch_mismatch(pid: u32) -> Option<String> {
let target = target_arch(pid)?;
if target == host_arch() {
return None;
}
let hint = if IS_MACOS && target == "x86_64" && host_arch() == "arm64" {
" (Rosetta?)"
} else {
""
};
Some(format!(
"target JVM is {target}{hint} but this injector is {}; use a matching JVM or a matching injector build",
host_arch()
))
}
fn target_arch(pid: u32) -> Option<String> {
if IS_MACOS {
let out = Command::new("ps")
.args(["-o", "comm=", "-p", &pid.to_string()])
.output()
.ok()?;
let comm = String::from_utf8_lossy(&out.stdout).trim().to_string();
if comm.is_empty() {
return None;
}
let head = read_head(Path::new(&comm), 64)?;
macho_arch(&head).map(str::to_string)
} else {
// Prefer the libjvm mapped into the process, fall back to the executable.
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(maps) = fs::read_to_string(format!("/proc/{pid}/maps")) {
for line in maps.lines() {
if line.contains("libjvm.so") {
if let Some(path) = line.split_whitespace().nth(5) {
candidates.push(PathBuf::from(path));
break;
}
}
}
}
candidates.push(PathBuf::from(format!("/proc/{pid}/exe")));
for c in candidates {
if let Some(head) = read_head(&c, 64) {
if let Some(a) = elf_arch(&head) {
return Some(a.to_string());
}
}
}
None
}
}
fn read_head(path: &Path, n: usize) -> Option<Vec<u8>> {
let mut f = fs::File::open(path).ok()?;
let mut buf = vec![0u8; n];
let mut got = 0;
while got < n {
match f.read(&mut buf[got..]) {
Ok(0) => break,
Ok(k) => got += k,
Err(_) => return None,
}
}
buf.truncate(got);
Some(buf)
}
/// Architecture name ("x86_64", "arm64", ...) from an ELF header, None if not ELF/unknown.
fn elf_arch(h: &[u8]) -> Option<&'static str> {
if h.len() < 20 || &h[0..4] != b"\x7fELF" {
return None;
}
let machine = match h[5] {
1 => u16::from_le_bytes([h[18], h[19]]),
2 => u16::from_be_bytes([h[18], h[19]]),
_ => return None,
};
Some(match machine {
3 => "x86",
40 => "arm",
62 => "x86_64",
183 => "arm64",
243 => "riscv",
21 => "ppc64",
_ => return None,
})
}
/// Architecture from a thin 64-bit Mach-O header. Fat/universal binaries return None
/// (the running slice cannot be determined from the file alone).
fn macho_arch(h: &[u8]) -> Option<&'static str> {
if h.len() < 8 {
return None;
}
let magic = u32::from_le_bytes([h[0], h[1], h[2], h[3]]);
let cputype = match magic {
0xfeed_facf => u32::from_le_bytes([h[4], h[5], h[6], h[7]]),
0xcffa_edfe => u32::from_be_bytes([h[4], h[5], h[6], h[7]]),
_ => return None,
};
match cputype {
0x0100_0007 => Some("x86_64"),
0x0100_000c => Some("arm64"),
_ => None,
}
}
// ------------------------------------------------------------ attach trigger
fn socket_candidates(pid: u32) -> Vec<PathBuf> {
let name = format!(".java_pid{pid}");
let mut v = Vec::new();
if IS_MACOS {
if let Some(t) = std::env::var_os("TMPDIR") {
v.push(Path::new(&t).join(&name));
}
}
let tmp = Path::new("/tmp").join(&name);
if !v.contains(&tmp) {
v.push(tmp);
}
v
}
fn is_socket(p: &Path) -> bool {
use std::os::unix::fs::FileTypeExt;
fs::symlink_metadata(p)
.map(|m| m.file_type().is_socket())
.unwrap_or(false)
}
/// Removes the trigger files when dropped, on every path.
struct TriggerFiles(Vec<PathBuf>);
impl Drop for TriggerFiles {
fn drop(&mut self) {
for p in &self.0 {
let _ = fs::remove_file(p);
}
}
}
fn trigger_candidates(pid: u32) -> Vec<PathBuf> {
let name = format!(".attach_pid{pid}");
let mut v = Vec::new();
if !IS_MACOS {
if let Ok(cwd) = fs::read_link(format!("/proc/{pid}/cwd")) {
v.push(cwd.join(&name));
}
}
v.push(Path::new("/tmp").join(&name));
if IS_MACOS {
if let Some(t) = std::env::var_os("TMPDIR") {
let p = Path::new(&t).join(&name);
if !v.contains(&p) {
v.push(p);
}
}
}
v
}
fn trigger_attach(pid: u32, sockets: &[PathBuf]) -> Result<PathBuf, String> {
let mut created = Vec::new();
for p in trigger_candidates(pid) {
if fs::File::create(&p).is_ok() {
created.push(p);
}
}
if created.is_empty() {
return Err(format!(
"could not create .attach_pid{pid} in the target's working directory or /tmp"
));
}
let _guard = TriggerFiles(created);
if unsafe { kill(pid as i32, SIGQUIT) } != 0 {
return Err(format!(
"failed to send SIGQUIT to {pid}: {}",
std::io::Error::last_os_error()
));
}
let start = Instant::now();
while start.elapsed() < SOCKET_WAIT {
if let Some(p) = sockets.iter().find(|p| is_socket(p)) {
return Ok(p.clone());
}
std::thread::sleep(POLL_INTERVAL);
}
if let Some(msg) = arch_mismatch(pid) {
return Err(format!("attach socket never appeared; {msg}"));
}
Err(format!(
"the JVM did not create its attach socket within {}s. Possible causes: the JVM was started with \
-XX:+DisableAttachMechanism or -Xrs, the game runs in a sandbox (Flatpak/Snap) with a private /tmp, \
or it belongs to a different user",
SOCKET_WAIT.as_secs()
))
}
// ------------------------------------------------------------------ protocol
/// `1\0load\0<lib>\0true\0\0`: version, command, then three NUL-terminated args
/// (library path, "true" = absolute path, empty options).
fn build_request(lib: &str) -> Vec<u8> {
let mut v = Vec::with_capacity(lib.len() + 16);
v.extend_from_slice(b"1\0load\0");
v.extend_from_slice(lib.as_bytes());
v.extend_from_slice(b"\0true\0\0");
v
}
/// First line is the integer status (0 = success); the rest is message text.
fn parse_response(data: &[u8]) -> Result<(), String> {
let text = String::from_utf8_lossy(data);
let (first, rest) = match text.split_once('\n') {
Some((a, b)) => (a.trim(), b.trim()),
None => (text.trim(), ""),
};
if first.is_empty() {
return Err("JVM closed the attach connection without a reply".into());
}
match first.parse::<i32>() {
Ok(0) => Ok(()),
Ok(code) => {
if rest.is_empty() {
Err(format!("JVM rejected the load request (status {code})"))
} else {
Err(format!("JVM rejected the load request (status {code}): {rest}"))
}
}
Err(_) => Err(format!("unexpected reply from JVM: {}", text.trim())),
}
}
fn send_load(sock: &Path, lib: &str) -> Result<(), String> {
let mut s = UnixStream::connect(sock).map_err(|e| {
format!(
"could not connect to attach socket {}: {e} (stale socket, or the JVM runs as another user)",
sock.display()
)
})?;
let _ = s.set_read_timeout(Some(REPLY_TIMEOUT));
let _ = s.set_write_timeout(Some(REPLY_TIMEOUT));
s.write_all(&build_request(lib))
.map_err(|e| format!("failed to send attach request: {e}"))?;
let mut buf = Vec::new();
match s.read_to_end(&mut buf) {
Ok(_) => {}
// A partial reply is still parseable; a bare timeout with no data is not.
Err(e) if buf.is_empty() => return Err(format!("no reply from JVM: {e}")),
Err(_) => {}
}
parse_response(&buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_format() {
assert_eq!(
build_request("/opt/libprestige_native.so"),
b"1\0load\0/opt/libprestige_native.so\0true\0\0".to_vec()
);
}
#[test]
fn response_ok_and_errors() {
assert!(parse_response(b"0\n").is_ok());
assert!(parse_response(b"0").is_ok());
let e = parse_response(b"-1\nreturn code: 1\n").unwrap_err();
assert!(e.contains("-1") && e.contains("return code: 1"));
assert!(parse_response(b"").is_err());
assert!(parse_response(b"garbage").is_err());
}
#[test]
fn elf_headers() {
let mut h = vec![0u8; 64];
h[..4].copy_from_slice(b"\x7fELF");
h[5] = 1;
h[18..20].copy_from_slice(&62u16.to_le_bytes());
assert_eq!(elf_arch(&h), Some("x86_64"));
h[18..20].copy_from_slice(&183u16.to_le_bytes());
assert_eq!(elf_arch(&h), Some("arm64"));
assert_eq!(elf_arch(b"nope"), None);
}
#[test]
fn macho_headers() {
let mut h = vec![0xcf, 0xfa, 0xed, 0xfe];
h.extend_from_slice(&0x0100_000cu32.to_le_bytes());
assert_eq!(macho_arch(&h), Some("arm64"));
let mut h = vec![0xcf, 0xfa, 0xed, 0xfe];
h.extend_from_slice(&0x0100_0007u32.to_le_bytes());
assert_eq!(macho_arch(&h), Some("x86_64"));
// fat binary -> undeterminable
assert_eq!(macho_arch(&[0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 2]), None);
}
}
@@ -0,0 +1,158 @@
//! Lists running Minecraft (Fabric) JVMs on Linux and macOS. std only.
use super::JavaProcess;
/// Last path component, accepting both separators.
fn file_name(path: &str) -> &str {
path.trim_end_matches(['/', '\\']).rsplit(['/', '\\']).next().unwrap_or(path)
}
/// True when `argv0` names a java launcher and the arguments look like a Minecraft/Fabric JVM.
pub(crate) fn is_minecraft_jvm(args: &[String]) -> bool {
let Some(argv0) = args.first() else { return false };
if !file_name(argv0).to_ascii_lowercase().contains("java") {
return false;
}
args.iter().any(|a| {
let a = a.to_ascii_lowercase();
a.contains("net.fabricmc.loader") || a.contains("knotclient") || a.contains("minecraft")
})
}
/// Friendly instance label: value of --version, else the last component of --gameDir, else "Minecraft".
pub(crate) fn instance_label(args: &[String]) -> String {
fn value<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
let prefix = format!("{flag}=");
let mut it = args.iter();
while let Some(a) = it.next() {
if a == flag {
return it.next().map(String::as_str).filter(|v| !v.is_empty());
}
if let Some(v) = a.strip_prefix(&prefix) {
if !v.is_empty() {
return Some(v);
}
}
}
None
}
if let Some(v) = value(args, "--version") {
return v.to_string();
}
if let Some(dir) = value(args, "--gameDir") {
let name = file_name(dir);
if !name.is_empty() {
return name.to_string();
}
}
"Minecraft".to_string()
}
fn build(pid: u32, args: &[String]) -> Option<JavaProcess> {
if !is_minecraft_jvm(args) {
return None;
}
Some(JavaProcess { pid, title: instance_label(args), exe: file_name(&args[0]).to_string() })
}
/// Parses one `ps -axo pid=,uid=,command=` line into (pid, uid, argv).
/// ps joins argv with spaces, so argv[0] is taken as everything before the first " -" option.
#[cfg_attr(target_os = "linux", allow(dead_code))]
pub(crate) fn parse_ps_line(line: &str) -> Option<(u32, u32, Vec<String>)> {
let line = line.trim_start();
let (pid, rest) = line.split_once(char::is_whitespace)?;
let rest = rest.trim_start();
let (uid, cmd) = rest.split_once(char::is_whitespace)?;
let cmd = cmd.trim();
let (argv0, tail) = match cmd.find(" -") {
Some(i) => (&cmd[..i], &cmd[i..]),
None => (cmd, ""),
};
let mut args = vec![argv0.to_string()];
args.extend(tail.split_whitespace().map(str::to_string));
Some((pid.parse().ok()?, uid.parse().ok()?, args))
}
#[cfg(target_os = "linux")]
pub fn java_processes() -> Vec<JavaProcess> {
use std::os::unix::fs::MetadataExt;
let own_pid = std::process::id();
let own_uid = std::fs::metadata("/proc/self").map(|m| m.uid()).ok();
let mut out = Vec::new();
let Ok(dir) = std::fs::read_dir("/proc") else { return out };
for entry in dir.flatten() {
let Some(pid) = entry.file_name().to_str().and_then(|n| n.parse::<u32>().ok()) else { continue };
if pid == own_pid {
continue;
}
if let (Some(me), Ok(meta)) = (own_uid, entry.metadata()) {
if meta.uid() != me {
continue;
}
}
let Ok(raw) = std::fs::read(entry.path().join("cmdline")) else { continue };
let args: Vec<String> = raw
.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect();
if let Some(p) = build(pid, &args) {
out.push(p);
}
}
out.sort_by_key(|p| p.pid);
out
}
#[cfg(not(target_os = "linux"))]
pub fn java_processes() -> Vec<JavaProcess> {
let own_pid = std::process::id();
let Ok(output) = std::process::Command::new("ps").args(["-axo", "pid=,uid=,command="]).output() else {
return Vec::new();
};
let text = String::from_utf8_lossy(&output.stdout);
let rows: Vec<_> = text.lines().filter_map(parse_ps_line).collect();
let own_uid = rows.iter().find(|r| r.0 == own_pid).map(|r| r.1);
let mut out: Vec<JavaProcess> = rows
.iter()
.filter(|(pid, uid, _)| *pid != own_pid && own_uid.map_or(true, |u| u == *uid))
.filter_map(|(pid, _, args)| build(*pid, args))
.collect();
out.sort_by_key(|p| p.pid);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn v(s: &[&str]) -> Vec<String> {
s.iter().map(|x| x.to_string()).collect()
}
#[test]
fn filters_minecraft_jvms() {
assert!(is_minecraft_jvm(&v(&["/usr/lib/jvm/java-21/bin/java", "-cp", "x", "net.fabricmc.loader.impl.launch.knot.KnotClient"])));
assert!(is_minecraft_jvm(&v(&["java", "-Dminecraft.launcher.brand=x", "Main"])));
assert!(!is_minecraft_jvm(&v(&["/usr/bin/java", "-jar", "gradle.jar"])));
assert!(!is_minecraft_jvm(&v(&["/usr/bin/bash", "-c", "minecraft"])));
assert!(!is_minecraft_jvm(&[]));
}
#[test]
fn labels() {
assert_eq!(instance_label(&v(&["java", "--version", "fabric-1.21.11"])), "fabric-1.21.11");
assert_eq!(instance_label(&v(&["java", "--version=1.21"])), "1.21");
assert_eq!(instance_label(&v(&["java", "--gameDir", "/home/u/.minecraft/instances/Pack/"])), "Pack");
assert_eq!(instance_label(&v(&["java", "KnotClient"])), "Minecraft");
}
#[test]
fn ps_lines() {
let (pid, uid, args) = parse_ps_line(" 123 501 /Users/a/Library/Application Support/x/bin/java -Xmx2G --version 1.21 net.fabricmc.loader").unwrap();
assert_eq!((pid, uid), (123, 501));
assert_eq!(args[0], "/Users/a/Library/Application Support/x/bin/java");
assert!(is_minecraft_jvm(&args));
assert_eq!(instance_label(&args), "1.21");
assert!(parse_ps_line("garbage").is_none());
}
}
@@ -0,0 +1,152 @@
//! Terminal input and URL opening 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
//! and forwards bytes over a channel; `poll_key` parses them. The original settings (`stty -g`)
//! are restored by `restore_terminal()` and, for Ctrl-C, by a SIGINT handler that only writes to
//! a self-pipe (async-signal-safe); a helper thread then restores the terminal and exits 130.
use super::Key;
use std::fs::File;
use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
use std::sync::{Mutex, OnceLock};
use std::time::Duration;
extern "C" {
fn signal(signum: i32, handler: usize) -> usize;
fn pipe(fds: *mut i32) -> i32;
fn write(fd: i32, buf: *const u8, n: usize) -> isize;
fn read(fd: i32, buf: *mut u8, n: usize) -> isize;
}
const SIGINT: i32 = 2;
static SAVED: Mutex<Option<String>> = Mutex::new(None);
static INPUT: OnceLock<Mutex<Option<Receiver<u8>>>> = OnceLock::new();
static SIG_WRITE_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
fn input() -> &'static Mutex<Option<Receiver<u8>>> {
INPUT.get_or_init(|| Mutex::new(None))
}
fn tty_stdin() -> Stdio {
match File::open("/dev/tty") {
Ok(f) => Stdio::from(f),
Err(_) => Stdio::inherit(),
}
}
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())
}
/// Nothing to do on unix (terminals understand ANSI); raw mode is entered lazily by `poll_key`.
pub fn enable_ansi() {}
extern "C" fn on_sigint(_sig: i32) {
let fd = SIG_WRITE_FD.load(std::sync::atomic::Ordering::Relaxed);
if fd >= 0 {
let b = 1u8;
unsafe { write(fd, &b, 1) };
}
}
fn install_sigint() {
let mut fds = [0i32; 2];
if unsafe { pipe(fds.as_mut_ptr()) } != 0 {
return;
}
SIG_WRITE_FD.store(fds[1], std::sync::atomic::Ordering::Relaxed);
let rfd = fds[0];
std::thread::spawn(move || {
let mut b = 0u8;
loop {
let n = unsafe { read(rfd, &mut b, 1) };
if n == 1 {
restore_terminal();
println!();
std::process::exit(130);
}
if n == 0 {
return;
}
}
});
unsafe { signal(SIGINT, on_sigint as extern "C" fn(i32) as usize) };
}
fn enter_raw() -> Option<Receiver<u8>> {
let saved = stty(&["-g"])?;
*SAVED.lock().ok()? = Some(saved);
stty(&["-icanon", "-echo", "min", "1", "time", "0"])?;
let mut tty: Box<dyn Read + Send> = match File::open("/dev/tty") {
Ok(f) => Box::new(f),
Err(_) => Box::new(std::io::stdin()),
};
let (tx, rx) = channel();
std::thread::spawn(move || {
let mut b = [0u8; 1];
while let Ok(1) = tty.read(&mut b) {
if tx.send(b[0]).is_err() {
break;
}
}
});
install_sigint();
Some(rx)
}
/// Returns a pending key press, if any, without blocking (except ~30ms to disambiguate a lone Esc).
pub fn poll_key() -> Option<Key> {
let mut guard = input().lock().ok()?;
if guard.is_none() {
*guard = Some(enter_raw()?);
}
let rx = guard.as_ref()?;
loop {
let b = rx.try_recv().ok()?;
match b {
b'\n' | b'\r' => return Some(Key::Enter),
b'k' => return Some(Key::Up),
b'j' => return Some(Key::Down),
27 => {
let ms30 = Duration::from_millis(30);
match rx.recv_timeout(ms30) {
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),
_ => {}
},
Ok(_) => {}
}
}
_ => {}
}
}
}
/// Restores the terminal settings saved when raw mode was entered. Safe to call repeatedly.
pub fn restore_terminal() {
let saved = SAVED.lock().ok().and_then(|mut s| s.take());
if let Some(s) = saved {
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)
}
@@ -1,278 +1,270 @@
//! Win32 plumbing of the injector (raw bindings, no external crates).
use std::ffi::c_void;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::time::Duration;
type Handle = *mut c_void;
const PROCESS_CREATE_THREAD: u32 = 0x0002;
const PROCESS_VM_OPERATION: u32 = 0x0008;
const PROCESS_VM_WRITE: u32 = 0x0020;
const PROCESS_VM_READ: u32 = 0x0010;
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const MEM_COMMIT: u32 = 0x1000;
const MEM_RESERVE: u32 = 0x2000;
const MEM_RELEASE: u32 = 0x8000;
const PAGE_READWRITE: u32 = 0x04;
const TH32CS_SNAPMODULE: u32 = 0x8;
const TH32CS_SNAPMODULE32: u32 = 0x10;
const WAIT_OBJECT_0: u32 = 0;
#[repr(C)]
struct ModuleEntry32W {
size: u32,
module_id: u32,
process_id: u32,
glblcnt_usage: u32,
proccnt_usage: u32,
base_addr: *mut u8,
base_size: u32,
module: Handle,
sz_module: [u16; 256],
sz_exe_path: [u16; 260],
}
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 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 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 GetLastError() -> u32;
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
fn Module32NextW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
fn GetStdHandle(id: u32) -> Handle;
fn GetConsoleMode(h: Handle, mode: *mut u32) -> i32;
fn SetConsoleMode(h: Handle, mode: u32) -> i32;
}
#[link(name = "user32")]
extern "system" {
fn EnumWindows(callback: unsafe extern "system" fn(Handle, isize) -> i32, param: isize) -> i32;
fn IsWindowVisible(hwnd: Handle) -> i32;
fn GetWindowTextW(hwnd: Handle, buf: *mut u16, max: i32) -> i32;
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;
}
pub struct JavaWindow {
pub pid: u32,
pub title: String,
pub exe: String,
}
pub enum Key {
Up,
Down,
Enter,
Escape,
}
pub fn enable_ansi() {
unsafe {
let out = GetStdHandle(-11i32 as u32);
let mut mode = 0;
if GetConsoleMode(out, &mut mode) != 0 {
SetConsoleMode(out, mode | 0x0004);
}
}
}
/// Non-blocking key read: arrows arrive as 0xE0/0x00 followed by 72 (up) / 80 (down).
pub fn poll_key() -> Option<Key> {
unsafe {
if _kbhit() == 0 {
return None;
}
match _getch() {
13 => Some(Key::Enter),
27 => Some(Key::Escape),
0 | 0xE0 => match _getch() {
72 => Some(Key::Up),
80 => Some(Key::Down),
_ => None,
},
_ => None,
}
}
}
fn wide(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
}
fn image_name(pid: u32) -> Option<String> {
unsafe {
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if process.is_null() {
return None;
}
let mut buf = [0u16; 1024];
let mut len = buf.len() as u32;
let ok = QueryFullProcessImageNameW(process, 0, buf.as_mut_ptr(), &mut len);
CloseHandle(process);
if ok == 0 {
return None;
}
Some(String::from_utf16_lossy(&buf[..len as usize]))
}
}
unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
let out = &mut *(param as *mut Vec<(u32, String)>);
if IsWindowVisible(hwnd) == 0 {
return 1;
}
let mut buf = [0u16; 256];
let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
if len <= 0 {
return 1;
}
let mut pid = 0;
GetWindowThreadProcessId(hwnd, &mut pid);
out.push((pid, String::from_utf16_lossy(&buf[..len as usize])));
1
}
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
pub fn java_windows() -> Vec<JavaWindow> {
let mut windows: Vec<(u32, String)> = Vec::new();
unsafe {
EnumWindows(collect, &mut windows as *mut _ as isize);
}
let mut out: Vec<JavaWindow> = Vec::new();
for (pid, title) in windows {
if out.iter().any(|w| w.pid == pid) {
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(JavaWindow { pid, title, exe });
}
}
out
}
fn last_error(what: &str) -> String {
format!("{what} failed (error {})", unsafe { GetLastError() })
}
/// True when the module file name is loaded in the process.
fn module_loaded(pid: u32, file_name: &str) -> bool {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if snapshot.is_null() || snapshot as isize == -1 {
return false;
}
let mut entry: ModuleEntry32W = std::mem::zeroed();
entry.size = std::mem::size_of::<ModuleEntry32W>() as u32;
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());
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
found = true;
break;
}
ok = Module32NextW(snapshot, &mut entry);
}
CloseHandle(snapshot);
found
}
}
/// 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 process = OpenProcess(access, 0, pid);
if process.is_null() {
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
}
let result = inject_into(process, pid, dll);
CloseHandle(process);
result
}
}
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());
}
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);
if remote.is_null() {
return Err(last_error("VirtualAllocEx"));
}
let cleanup = |err: String| {
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 {
return cleanup(last_error("WriteProcessMemory"));
}
// kernel32 is mapped at the same base in every process of a boot session, so the local address is valid remotely.
let kernel32 = GetModuleHandleW(wide(Path::new("kernel32.dll")).as_ptr());
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8);
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());
if thread.is_null() {
return cleanup(last_error("CreateRemoteThread"));
}
let wait = WaitForSingleObject(thread, 30_000);
let mut exit = 0;
GetExitCodeThread(thread, &mut exit);
CloseHandle(thread);
if wait != WAIT_OBJECT_0 {
// The thread may still be running inside the target; leave the buffer alone.
return Err("remote LoadLibraryW did not finish within 30 seconds".into());
}
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
// LoadLibraryW's return value is truncated to 32 bits by GetExitCodeThread, so confirm through the module list.
for _ in 0..20 {
if module_loaded(pid, &file_name) {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
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 }
}
//! Win32 plumbing of the injector (raw bindings, no external crates).
use std::ffi::c_void;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use std::time::Duration;
use super::{JavaProcess, Key};
type Handle = *mut c_void;
const PROCESS_CREATE_THREAD: u32 = 0x0002;
const PROCESS_VM_OPERATION: u32 = 0x0008;
const PROCESS_VM_WRITE: u32 = 0x0020;
const PROCESS_VM_READ: u32 = 0x0010;
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const MEM_COMMIT: u32 = 0x1000;
const MEM_RESERVE: u32 = 0x2000;
const MEM_RELEASE: u32 = 0x8000;
const PAGE_READWRITE: u32 = 0x04;
const TH32CS_SNAPMODULE: u32 = 0x8;
const TH32CS_SNAPMODULE32: u32 = 0x10;
const WAIT_OBJECT_0: u32 = 0;
#[repr(C)]
struct ModuleEntry32W {
size: u32,
module_id: u32,
process_id: u32,
glblcnt_usage: u32,
proccnt_usage: u32,
base_addr: *mut u8,
base_size: u32,
module: Handle,
sz_module: [u16; 256],
sz_exe_path: [u16; 260],
}
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 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 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 GetLastError() -> u32;
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
fn Module32NextW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
fn GetStdHandle(id: u32) -> Handle;
fn GetConsoleMode(h: Handle, mode: *mut u32) -> i32;
fn SetConsoleMode(h: Handle, mode: u32) -> i32;
}
#[link(name = "user32")]
extern "system" {
fn EnumWindows(callback: unsafe extern "system" fn(Handle, isize) -> i32, param: isize) -> i32;
fn IsWindowVisible(hwnd: Handle) -> i32;
fn GetWindowTextW(hwnd: Handle, buf: *mut u16, max: i32) -> i32;
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;
}
/// Nothing to restore on Windows (keys are read with _getch).
pub fn restore_terminal() {}
pub fn enable_ansi() {
unsafe {
let out = GetStdHandle(-11i32 as u32);
let mut mode = 0;
if GetConsoleMode(out, &mut mode) != 0 {
SetConsoleMode(out, mode | 0x0004);
}
}
}
/// Non-blocking key read: arrows arrive as 0xE0/0x00 followed by 72 (up) / 80 (down).
pub fn poll_key() -> Option<Key> {
unsafe {
if _kbhit() == 0 {
return None;
}
match _getch() {
13 => Some(Key::Enter),
27 => Some(Key::Escape),
0 | 0xE0 => match _getch() {
72 => Some(Key::Up),
80 => Some(Key::Down),
_ => None,
},
_ => None,
}
}
}
fn wide(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
}
fn image_name(pid: u32) -> Option<String> {
unsafe {
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if process.is_null() {
return None;
}
let mut buf = [0u16; 1024];
let mut len = buf.len() as u32;
let ok = QueryFullProcessImageNameW(process, 0, buf.as_mut_ptr(), &mut len);
CloseHandle(process);
if ok == 0 {
return None;
}
Some(String::from_utf16_lossy(&buf[..len as usize]))
}
}
unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
let out = &mut *(param as *mut Vec<(u32, String)>);
if IsWindowVisible(hwnd) == 0 {
return 1;
}
let mut buf = [0u16; 256];
let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
if len <= 0 {
return 1;
}
let mut pid = 0;
GetWindowThreadProcessId(hwnd, &mut pid);
out.push((pid, String::from_utf16_lossy(&buf[..len as usize])));
1
}
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
pub fn java_processes() -> Vec<JavaProcess> {
let mut windows: Vec<(u32, String)> = Vec::new();
unsafe {
EnumWindows(collect, &mut windows as *mut _ as isize);
}
let mut out: Vec<JavaProcess> = Vec::new();
for (pid, title) in windows {
if out.iter().any(|w| w.pid == pid) {
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 });
}
}
out
}
fn last_error(what: &str) -> String {
format!("{what} failed (error {})", unsafe { GetLastError() })
}
/// True when the module file name is loaded in the process.
fn module_loaded(pid: u32, file_name: &str) -> bool {
unsafe {
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if snapshot.is_null() || snapshot as isize == -1 {
return false;
}
let mut entry: ModuleEntry32W = std::mem::zeroed();
entry.size = std::mem::size_of::<ModuleEntry32W>() as u32;
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());
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
found = true;
break;
}
ok = Module32NextW(snapshot, &mut entry);
}
CloseHandle(snapshot);
found
}
}
/// 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 process = OpenProcess(access, 0, pid);
if process.is_null() {
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
}
let result = inject_into(process, pid, dll);
CloseHandle(process);
result
}
}
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());
}
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);
if remote.is_null() {
return Err(last_error("VirtualAllocEx"));
}
let cleanup = |err: String| {
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 {
return cleanup(last_error("WriteProcessMemory"));
}
// kernel32 is mapped at the same base in every process of a boot session, so the local address is valid remotely.
let kernel32 = GetModuleHandleW(wide(Path::new("kernel32.dll")).as_ptr());
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8);
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());
if thread.is_null() {
return cleanup(last_error("CreateRemoteThread"));
}
let wait = WaitForSingleObject(thread, 30_000);
let mut exit = 0;
GetExitCodeThread(thread, &mut exit);
CloseHandle(thread);
if wait != WAIT_OBJECT_0 {
// The thread may still be running inside the target; leave the buffer alone.
return Err("remote LoadLibraryW did not finish within 30 seconds".into());
}
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
// LoadLibraryW's return value is truncated to 32 bits by GetExitCodeThread, so confirm through the module list.
for _ in 0..20 {
if module_loaded(pid, &file_name) {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
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 }
}