This commit is contained in:
bbldrizzy committed 2026-10-23 00:28:57 +01:00
1 parent 83aea53e57
commit aa4f6b6ddc
842 files changed
+47284 -91563

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
[package]
name = "prestige-injector"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "PrestigeInjector"
path = "src/main.rs"
+280
View File
@@ -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();
}
+278
View File
@@ -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".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 }
}