fixes
This commit is contained in:
1 parent
83aea53e57
commit
aa4f6b6ddc
842 files changed
+47284
-91563
No files matched your search
Generated
+11
@@ -0,0 +1,11 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "prestige-injector"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "prestige_native"
|
||||
version = "0.1.0"
|
||||
@@ -0,0 +1,10 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["crates/loader", "crates/injector"]
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
strip = true
|
||||
@@ -0,0 +1,32 @@
|
||||
# Prestige injector (Rust)
|
||||
|
||||
`PrestigeInjector.exe` loads `prestige_native.dll` into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x,
|
||||
Fabric API installed). It uses the same overall method as the openvape bundle, written from scratch in Rust with no external crates.
|
||||
|
||||
## How it works
|
||||
|
||||
1. **Injector** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
|
||||
process through `CreateRemoteThread` and confirms the module is mapped.
|
||||
2. **`prestige_native.dll`** (`DllMain` only spawns a worker thread):
|
||||
- waits for `jvm.dll`, `JNI_GetCreatedJavaVMs`, attaches, acquires the JVMTI redefine/retransform capabilities;
|
||||
- writes the embedded payload jar (the remapped `prestige-*.jar`) to `%TEMP%\PrestigeLoader`;
|
||||
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader;
|
||||
- registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`.
|
||||
3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes
|
||||
(JVMTI retransform capture), runs the hooks through `InPlaceTransformer` and redefines the class. The bundled ClassTransform
|
||||
inlines hook handlers into the target methods, so normally nothing is added to the class and a plain redefine works. If a
|
||||
transformer ever does add methods (a running JVM cannot), they are moved into generated `dev.zprestige.prestige.hooks.H*`
|
||||
helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles.
|
||||
Finally the Prestige and ClickGUI entrypoints run on the client thread.
|
||||
|
||||
Log: `prestige-native.log` beside the DLL, plus `[Prestige-Inject]` lines in the game log.
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
.\build-injector.ps1 # gradlew remapJar, cargo build --release, copies to dist\
|
||||
```
|
||||
|
||||
## Use
|
||||
|
||||
Start a Fabric 1.21.11 instance, then run `dist\PrestigeInjector.exe` (or `PrestigeInjector.exe <pid> [dll]`).
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "prestige-injector"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "PrestigeInjector"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,280 @@
|
||||
//! 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.exe interactive process picker (Up/Down, Enter, Esc)
|
||||
//! PrestigeInjector.exe <pid> [dll path] non-interactive
|
||||
mod win;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
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";
|
||||
/// How long to follow the DLL's progress after it has been loaded.
|
||||
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
|
||||
|
||||
const RESET: &str = "\x1b[0m";
|
||||
const DIM: &str = "\x1b[90m";
|
||||
const CYAN: &str = "\x1b[96m";
|
||||
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(DLL_NAME)))
|
||||
.unwrap_or_else(|| PathBuf::from(DLL_NAME))
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ ");
|
||||
println!(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|");
|
||||
println!(" | _/ / _|\\__ \\ | | | | (_ | _| ");
|
||||
println!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector");
|
||||
println!("{YELLOW} {WATERMARK}{RESET}\n");
|
||||
}
|
||||
|
||||
fn step(label: &str, message: &str) {
|
||||
println!("{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", clock());
|
||||
}
|
||||
fn ok(label: &str, message: &str) {
|
||||
println!("{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", clock());
|
||||
}
|
||||
fn fail(label: &str, message: &str) {
|
||||
println!("{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {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())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
win::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();
|
||||
(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]]");
|
||||
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) };
|
||||
};
|
||||
if !interactive {
|
||||
banner();
|
||||
}
|
||||
|
||||
let code = run(pid, &dll);
|
||||
if interactive {
|
||||
print!("\nPress Enter to close...");
|
||||
let _ = io::stdout().flush();
|
||||
let mut line = String::new();
|
||||
let _ = io::stdin().read_line(&mut line);
|
||||
}
|
||||
code
|
||||
}
|
||||
|
||||
fn parse_pid(text: &str) -> Option<u32> {
|
||||
text.parse().ok()
|
||||
}
|
||||
|
||||
fn run(pid: u32, dll: &Path) -> ExitCode {
|
||||
println!();
|
||||
step("target", &format!("process {pid}"));
|
||||
let dll = match dll.canonicalize() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
fail("error", &format!("cannot find {}: {e}", dll.display()));
|
||||
return ExitCode::from(1);
|
||||
}
|
||||
};
|
||||
let dll_display = dll.display().to_string();
|
||||
step("loader", &dll_display.trim_start_matches(r"\\?\").to_string());
|
||||
|
||||
// The DLL 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"),
|
||||
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) {
|
||||
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) => {
|
||||
fail("failed", &format!("injection did not complete: {line}"));
|
||||
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());
|
||||
ExitCode::from(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Outcome {
|
||||
Done,
|
||||
Failed(String),
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
/// Prints the lines the loader/bootstrap append to the log until they report success or failure.
|
||||
fn follow_log(path: &Path, start: u64) -> Outcome {
|
||||
let deadline = Instant::now() + FOLLOW_TIMEOUT;
|
||||
let mut offset = start;
|
||||
let mut pending = 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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();
|
||||
if lower.contains("failed") || lower.contains("not found") {
|
||||
fail("loader", message);
|
||||
return Outcome::Failed(message.to_string());
|
||||
}
|
||||
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> {
|
||||
std::env::var_os("APPDATA").map(|dir| PathBuf::from(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() {
|
||||
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 {
|
||||
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] "
|
||||
});
|
||||
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")
|
||||
}
|
||||
|
||||
/// 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 last_refresh = Instant::now();
|
||||
loop {
|
||||
if selected >= processes.len() {
|
||||
selected = processes.len().saturating_sub(1);
|
||||
}
|
||||
draw(&processes, selected);
|
||||
let deadline = Instant::now() + Duration::from_millis(50);
|
||||
while Instant::now() < deadline {
|
||||
if let Some(key) = win::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 => {
|
||||
if let Some(p) = processes.get(selected) {
|
||||
return Some(p.pid);
|
||||
}
|
||||
}
|
||||
win::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();
|
||||
last_refresh = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(processes: &[win::JavaWindow], 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");
|
||||
if processes.is_empty() {
|
||||
out.push_str(" waiting for a javaw.exe / java.exe window...\n");
|
||||
}
|
||||
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));
|
||||
}
|
||||
print!("{out}");
|
||||
let _ = io::stdout().flush();
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
//! 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 | ||||