UPDATE
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:
1 parent
910a126f04
commit
38ff6d387c
29 files changed
+2234
-491
No files matched your search
@@ -0,0 +1,86 @@
|
|||||||
|
name: Injector
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "injector/**"
|
||||||
|
- "src/**"
|
||||||
|
- ".github/workflows/injector.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
payload:
|
||||||
|
name: Payload jar
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "21"
|
||||||
|
- name: Build remapped jar
|
||||||
|
run: |
|
||||||
|
chmod +x ./gradlew
|
||||||
|
./gradlew remapJar --console=plain
|
||||||
|
- name: Upload jar
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: payload-jar # also the Fabric mod: drop prestige-*.jar into mods/
|
||||||
|
path: |
|
||||||
|
build/libs/prestige-*.jar
|
||||||
|
!build/libs/*-dev.jar
|
||||||
|
!build/libs/*-sources.jar
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
injector:
|
||||||
|
name: Injector (${{ matrix.target }})
|
||||||
|
needs: payload
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
name: windows-x86_64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector.exe
|
||||||
|
injector/target/release/prestige_native.dll
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
name: linux-x86_64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.so
|
||||||
|
- os: macos-13
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
name: macos-x86_64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.dylib
|
||||||
|
- os: macos-14
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
name: macos-aarch64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.dylib
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
- name: Download payload jar
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: payload-jar
|
||||||
|
path: build/libs
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --manifest-path injector/Cargo.toml
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --manifest-path injector/Cargo.toml
|
||||||
|
- name: Upload injector
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: injector-${{ matrix.name }}
|
||||||
|
path: ${{ matrix.files }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -18,3 +18,4 @@ openvape.zip
|
|||||||
|
|
||||||
/dist/
|
/dist/
|
||||||
/injector/target/
|
/injector/target/
|
||||||
|
/tools
|
||||||
@@ -18,7 +18,7 @@ A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Presti
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
Cracked by Skid Solutions.
|
Cracked by Skid Solutions.
|
||||||
Join the Discord: https://discord.gg/skidsolutions
|
Join the Discord: https://discord.gg/x2FJyGrDNg
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
zPrestige couldnt give a fuck it seems about his client. AI slop all over, no time nor thought it seems goes into testing if things actually work. banned in 2 games with the most popular CAS config on mcpvp with countless modules flagging grim on actest.
|
zPrestige couldnt give a fuck it seems about his client. AI slop all over, no time nor thought it seems goes into testing if things actually work. banned in 2 games with the most popular CAS config on mcpvp with countless modules flagging grim on actest.
|
||||||
|
|||||||
+2
-1
@@ -10,4 +10,5 @@ cargo build --release --manifest-path injector\Cargo.toml
|
|||||||
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }
|
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }
|
||||||
New-Item -ItemType Directory -Force dist | Out-Null
|
New-Item -ItemType Directory -Force dist | Out-Null
|
||||||
Copy-Item injector\target\release\PrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
|
Copy-Item injector\target\release\PrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
|
||||||
Write-Host 'Done: dist\PrestigeInjector.exe + dist\prestige_native.dll'
|
Get-ChildItem build\libs\prestige-*.jar | Where-Object { $_.Name -notmatch '-(dev|sources)\.jar$' } | Copy-Item -Destination dist\ -Force
|
||||||
|
Write-Host 'Done: dist\PrestigeInjector.exe + dist\prestige_native.dll + dist\prestige-*.jar (Fabric mod)'
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader
|
||||||
|
# library, and collects PrestigeInjector + libprestige_native.{so,dylib} in dist/.
|
||||||
|
# Optional: set JAVA_HOME to pick the JDK used by Gradle.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) LIB=libprestige_native.so ;;
|
||||||
|
Darwin) LIB=libprestige_native.dylib ;;
|
||||||
|
*) echo "unsupported OS: $(uname -s) (use build-injector.ps1 on Windows)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "${JAVA_HOME:-}" ]; then
|
||||||
|
export JAVA_HOME
|
||||||
|
PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
fi
|
||||||
|
|
||||||
|
./gradlew remapJar --console=plain
|
||||||
|
cargo build --release --manifest-path injector/Cargo.toml
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp -f "injector/target/release/PrestigeInjector" "injector/target/release/$LIB" dist/
|
||||||
|
chmod +x dist/PrestigeInjector
|
||||||
|
|
||||||
|
# the same remapped jar is the Fabric mod (drop it into mods/)
|
||||||
|
for jar in build/libs/prestige-*.jar; do
|
||||||
|
case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac
|
||||||
|
cp -f "$jar" dist/
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done: dist/PrestigeInjector + dist/$LIB + dist/prestige-*.jar (Fabric mod)"
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@echo off
|
||||||
|
rem Builds release zips for every platform into dist\release\ (run on Windows):
|
||||||
|
rem prestige-<ver>-windows-x86_64.zip PrestigeInjector.exe + prestige_native.dll + mod jar
|
||||||
|
rem prestige-<ver>-linux-x86_64.zip PrestigeInjector + libprestige_native.so + mod jar (glibc 2.17+, any distro)
|
||||||
|
rem prestige-<ver>-macos-universal.zip PrestigeInjector + libprestige_native.dylib + mod jar (Intel + Apple Silicon)
|
||||||
|
rem prestige-<ver>.jar the Fabric mod on its own
|
||||||
|
rem SHA256SUMS.txt
|
||||||
|
rem Linux and macOS are cross-compiled with cargo-zigbuild + zig; both are installed on first run if missing.
|
||||||
|
rem Requires: Rust (rustup), Python 3, JDK 21.
|
||||||
|
setlocal EnableExtensions
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "SELF=%~f0"
|
||||||
|
|
||||||
|
set "OUT=%CD%\dist\release"
|
||||||
|
if exist "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot" set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
|
||||||
|
|
||||||
|
for /f "tokens=1,* delims==" %%a in ('findstr /b /c:"mod_version=" gradle.properties') do set "VERSION=%%b"
|
||||||
|
if not defined VERSION set "VERSION=dev"
|
||||||
|
echo === Prestige %VERSION% release build
|
||||||
|
|
||||||
|
rem ---- tools -------------------------------------------------------------
|
||||||
|
where cargo >nul 2>&1 || (echo cargo not found: install Rust from https://rustup.rs & goto :fail)
|
||||||
|
where python >nul 2>&1 || (echo python not found: install Python 3 from https://python.org & goto :fail)
|
||||||
|
python -m ziglang version >nul 2>&1 || (
|
||||||
|
echo === Installing zig ^(pip ziglang^)
|
||||||
|
python -m pip install --user ziglang || goto :fail
|
||||||
|
)
|
||||||
|
where cargo-zigbuild >nul 2>&1 || (
|
||||||
|
echo === Installing cargo-zigbuild
|
||||||
|
cargo install --locked cargo-zigbuild || goto :fail
|
||||||
|
)
|
||||||
|
rustup target add x86_64-pc-windows-msvc x86_64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin || goto :fail
|
||||||
|
|
||||||
|
rem zigbuild's linker wrappers break on paths with spaces, so keep its cache on the 8.3 short path
|
||||||
|
if not exist "%LOCALAPPDATA%\cargo-zigbuild" mkdir "%LOCALAPPDATA%\cargo-zigbuild"
|
||||||
|
for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI"
|
||||||
|
|
||||||
|
rem ---- payload jar -------------------------------------------------------
|
||||||
|
echo === Building payload jar
|
||||||
|
call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail
|
||||||
|
set "JAR="
|
||||||
|
for /f "delims=" %%j in ('dir /b /o-d "build\libs\prestige-*.jar" ^| findstr /v /i /c:"-dev.jar" /c:"-sources.jar"') do if not defined JAR set "JAR=%CD%\build\libs\%%j"
|
||||||
|
if not defined JAR (echo no remapped jar in build\libs & goto :fail)
|
||||||
|
rem every target embeds this exact jar
|
||||||
|
set "PRESTIGE_PAYLOAD=%JAR%"
|
||||||
|
|
||||||
|
if exist "%OUT%" rmdir /s /q "%OUT%"
|
||||||
|
mkdir "%OUT%" || goto :fail
|
||||||
|
|
||||||
|
rem ---- native builds -----------------------------------------------------
|
||||||
|
echo === Windows x86_64
|
||||||
|
cargo build --release --manifest-path injector\Cargo.toml --target x86_64-pc-windows-msvc || goto :fail
|
||||||
|
call :package windows-x86_64 x86_64-pc-windows-msvc PrestigeInjector.exe prestige_native.dll || goto :fail
|
||||||
|
|
||||||
|
echo === Linux x86_64 (glibc 2.17+)
|
||||||
|
cargo zigbuild --release --manifest-path injector\Cargo.toml --target x86_64-unknown-linux-gnu.2.17 || goto :fail
|
||||||
|
call :package linux-x86_64 x86_64-unknown-linux-gnu PrestigeInjector libprestige_native.so || goto :fail
|
||||||
|
|
||||||
|
echo === macOS universal (x86_64 + arm64)
|
||||||
|
cargo zigbuild --release --manifest-path injector\Cargo.toml --target universal2-apple-darwin || goto :fail
|
||||||
|
call :package macos-universal universal2-apple-darwin PrestigeInjector libprestige_native.dylib || goto :fail
|
||||||
|
|
||||||
|
copy /y "%JAR%" "%OUT%\" >nul || goto :fail
|
||||||
|
python scripts\package_release.py checksums "%OUT%" || goto :fail
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo === Done: %OUT%
|
||||||
|
dir /b "%OUT%"
|
||||||
|
call :maybe_pause
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
rem %1 platform name, %2 cargo target dir, %3 injector file, %4 library file
|
||||||
|
:package
|
||||||
|
python scripts\package_release.py zip "%OUT%" "%VERSION%" %1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%"
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
|
|
||||||
|
:fail
|
||||||
|
echo.
|
||||||
|
echo *** Release build FAILED
|
||||||
|
call :maybe_pause
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
rem keep the window open when started by double-click
|
||||||
|
:maybe_pause
|
||||||
|
echo %cmdcmdline% | find /i "%SELF%" >nul && pause
|
||||||
|
exit /b 0
|
||||||
+4
-1
@@ -52,11 +52,14 @@ dependencies {
|
|||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||||
|
|
||||||
// Dear ImGui. The binding contains the Java classes, lwjgl3 the GL3/GLFW backend and
|
// Dear ImGui. The binding contains the Java classes, lwjgl3 the GL3/GLFW backend and
|
||||||
// natives-windows the imgui-java64.dll (extracted from the jar at runtime by imgui-java).
|
// natives-* the imgui-java64 libraries (extracted from the jar at runtime by imgui-java).
|
||||||
|
// Linux natives are x86_64 only; the macOS dylib is universal (x86_64 + arm64).
|
||||||
shade "io.github.spair:imgui-java-binding:${project.imgui_version}"
|
shade "io.github.spair:imgui-java-binding:${project.imgui_version}"
|
||||||
// imgui-java-lwjgl3 pulls LWJGL 3.4.1 transitively, which replaces Minecraft's 3.3.3 jars and breaks world saving
|
// imgui-java-lwjgl3 pulls LWJGL 3.4.1 transitively, which replaces Minecraft's 3.3.3 jars and breaks world saving
|
||||||
shade("io.github.spair:imgui-java-lwjgl3:${project.imgui_version}") { exclude group: 'org.lwjgl' }
|
shade("io.github.spair:imgui-java-lwjgl3:${project.imgui_version}") { exclude group: 'org.lwjgl' }
|
||||||
shade "io.github.spair:imgui-java-natives-windows:${project.imgui_version}"
|
shade "io.github.spair:imgui-java-natives-windows:${project.imgui_version}"
|
||||||
|
shade "io.github.spair:imgui-java-natives-linux:${project.imgui_version}"
|
||||||
|
shade "io.github.spair:imgui-java-natives-macos:${project.imgui_version}"
|
||||||
|
|
||||||
// ClassTransform (net.lenni0451, customised build); engine/EnginePlugin uses it to apply the client's @CTransformer hooks.
|
// ClassTransform (net.lenni0451, customised build); engine/EnginePlugin uses it to apply the client's @CTransformer hooks.
|
||||||
shade files('libs/classtransform-bundled.jar')
|
shade files('libs/classtransform-bundled.jar')
|
||||||
|
|||||||
+75
-11
@@ -1,15 +1,34 @@
|
|||||||
# Prestige injector (Rust)
|
# 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,
|
`PrestigeInjector` loads the native loader library (`prestige_native.dll` / `libprestige_native.so` / `libprestige_native.dylib`)
|
||||||
Fabric API installed). It uses the same overall method as the openvape bundle, written from scratch in Rust with no external crates.
|
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.
|
||||||
|
|
||||||
|
## Supported platforms
|
||||||
|
|
||||||
|
| OS | Architectures | Injector binary | Loader library |
|
||||||
|
|---------|--------------------|-----------------------|-----------------------------|
|
||||||
|
| Windows | x86_64 | `PrestigeInjector.exe`| `prestige_native.dll` |
|
||||||
|
| Linux | x86_64 | `PrestigeInjector` | `libprestige_native.so` |
|
||||||
|
| macOS | x86_64, arm64 | `PrestigeInjector` | `libprestige_native.dylib` |
|
||||||
|
|
||||||
|
The injector and library must be built for the same architecture as the target JVM.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. **Injector** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
|
1. **Injector** lists the running Fabric client JVMs, then loads the library into the chosen process.
|
||||||
process through `CreateRemoteThread` and confirms the module is mapped.
|
- **Windows:** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
|
||||||
2. **`prestige_native.dll`** (`DllMain` only spawns a worker thread):
|
process through `CreateRemoteThread` and confirms the module is mapped.
|
||||||
- waits for `jvm.dll`, `JNI_GetCreatedJavaVMs`, attaches, acquires the JVMTI redefine/retransform capabilities;
|
- **Linux / macOS:** uses the HotSpot attach protocol (the same mechanism as `jcmd <pid> JVMTI.agent_load`). The injector
|
||||||
- writes the embedded payload jar (the remapped `prestige-*.jar`) to `%TEMP%\PrestigeLoader`;
|
creates `.attach_pid<pid>` and sends `SIGQUIT` to the JVM, connects to its attach socket (`/tmp/.java_pid<pid>`, under
|
||||||
|
`$TMPDIR` on macOS) and asks it to load the library. The JVM `dlopen`s `libprestige_native` and calls its
|
||||||
|
`Agent_OnAttach` entry point. No ptrace, no root, no per-architecture shellcode.
|
||||||
|
Processes are found via `/proc` (Linux) or `ps` (macOS); there are no window titles, so the picker shows the pid and
|
||||||
|
instance name.
|
||||||
|
2. **The native library** only spawns a worker thread from its entry point (`DllMain` on Windows, `Agent_OnAttach` on
|
||||||
|
unix, which also hands over the `JavaVM*`). The worker:
|
||||||
|
- waits for the JVM (Windows: `jvm.dll`, `JNI_GetCreatedJavaVMs`), attaches, acquires the JVMTI redefine/retransform capabilities;
|
||||||
|
- writes the embedded payload jar (the remapped `prestige-*.jar`) to the temp directory under `PrestigeLoader`;
|
||||||
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader;
|
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader;
|
||||||
- registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`.
|
- 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
|
3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes
|
||||||
@@ -19,14 +38,59 @@ Fabric API installed). It uses the same overall method as the openvape bundle, w
|
|||||||
helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles.
|
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.
|
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
|
## Build
|
||||||
|
|
||||||
|
The payload jar is embedded with `include_bytes!`, so each target needs its own build. Both scripts below build for the host OS only and run
|
||||||
|
`gradlew remapJar`, `cargo build --release`, and copy the results to `dist/`.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\build-injector.ps1 # gradlew remapJar, cargo build --release, copies to dist\
|
.\build-injector.ps1 # Windows: dist\PrestigeInjector.exe + dist\prestige_native.dll
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build-injector.sh # Linux/macOS: dist/PrestigeInjector + dist/libprestige_native.{so,dylib}
|
||||||
|
```
|
||||||
|
|
||||||
|
To build release zips for every platform from Windows, run `build-release.bat`. It cross-compiles Linux (x86_64,
|
||||||
|
glibc 2.17+, so one build covers every mainstream distro) and macOS (universal x86_64 + arm64) with `cargo-zigbuild`, installing
|
||||||
|
zig and cargo-zigbuild on first run, and writes `dist\release\prestige-<ver>-{windows-x86_64,linux-x86_64,macos-universal}.zip`,
|
||||||
|
the bare mod jar and `SHA256SUMS.txt`.
|
||||||
|
|
||||||
|
Set `JAVA_HOME` beforehand to choose the JDK for Gradle (`build-injector.sh` honours it). The GitHub Actions workflow
|
||||||
|
`.github/workflows/injector.yml` builds and tests all four targets (Windows x86_64, Linux x86_64, macOS x86_64, macOS arm64)
|
||||||
|
and uploads them as artifacts.
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
Start a Fabric 1.21.11 instance, then run `dist\PrestigeInjector.exe` (or `PrestigeInjector.exe <pid> [dll]`).
|
Start a Fabric 1.21.11 instance, then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PrestigeInjector # interactive: pick the game from the list
|
||||||
|
PrestigeInjector <pid> [lib path] # inject into a specific pid, optionally with an explicit library path
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows this is `PrestigeInjector.exe`. By default the library is looked up beside the injector.
|
||||||
|
|
||||||
|
## Install as a mod instead
|
||||||
|
|
||||||
|
The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the
|
||||||
|
`mods/` folder of a Fabric Loader 0.19+ / Fabric API / Minecraft 1.21.11 instance; no injector is needed, on any OS.
|
||||||
|
Do not use both: if the mod is already loaded, the injector's bootstrap logs "already loaded as a Fabric mod" and skips.
|
||||||
|
|
||||||
|
## Requirements (Linux / macOS)
|
||||||
|
|
||||||
|
- Run the injector as the **same user** as the game.
|
||||||
|
- The attach mechanism must be enabled: the game must not be started with `-XX:+DisableAttachMechanism`.
|
||||||
|
- The JVM architecture must match the injector build. On Apple Silicon use an arm64 JVM, or use the x86_64 build for JVMs
|
||||||
|
running under Rosetta.
|
||||||
|
- Sandboxed launchers (Flatpak, Snap) hide the attach socket and are not supported.
|
||||||
|
|
||||||
|
## Known limits
|
||||||
|
|
||||||
|
- On Linux/macOS the library stays loaded after self-destruct: hooks are removed and natives unregistered, but the library is
|
||||||
|
not `dlclose`d (unmapping code the thread is still returning into would crash the JVM). On Windows the library is freed.
|
||||||
|
- Linux and macOS support is compile-checked in CI; test on a real host before relying on it.
|
||||||
|
|
||||||
|
## Log
|
||||||
|
|
||||||
|
`prestige-native.log` is written beside the loader library, plus `[Prestige-Inject]` lines in the game log.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads prestige_native.dll into the selected one
|
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
|
||||||
//! with the classic remote LoadLibraryW thread. The DLL does everything else once it is inside the JVM; the injector tails
|
//! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW
|
||||||
//! the DLL's log so every stage (transforming, injecting, initialising) is visible.
|
//! 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 interactive process picker (Up/Down, Enter, Esc)
|
||||||
//! PrestigeInjector.exe <pid> [dll path] non-interactive
|
//! PrestigeInjector <pid> [lib path] non-interactive
|
||||||
mod win;
|
mod platform;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
@@ -12,9 +13,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const DLL_NAME: &str = "prestige_native.dll";
|
/// The one credit line, shown once at the top of the screen.
|
||||||
const WATERMARK: &str = "Cracked by Skid Solutions";
|
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
|
||||||
const INVITE_URL: &str = "https://discord.gg/M56rd2wwC5";
|
const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg";
|
||||||
/// How long to follow the DLL's progress after it has been loaded.
|
/// How long to follow the DLL's progress after it has been loaded.
|
||||||
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
|
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
|
||||||
|
|
||||||
@@ -28,16 +29,18 @@ const RED: &str = "\x1b[91m";
|
|||||||
fn default_dll() -> PathBuf {
|
fn default_dll() -> PathBuf {
|
||||||
std::env::current_exe()
|
std::env::current_exe()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|exe| exe.parent().map(|dir| dir.join(DLL_NAME)))
|
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
|
||||||
.unwrap_or_else(|| PathBuf::from(DLL_NAME))
|
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn banner() {
|
fn banner() -> String {
|
||||||
println!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ ");
|
let mut out = String::new();
|
||||||
println!(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|");
|
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
|
||||||
println!(" | _/ / _|\\__ \\ | | | | (_ | _| ");
|
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
|
||||||
println!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector");
|
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
|
||||||
println!("{YELLOW} {WATERMARK}{RESET}\n");
|
out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"));
|
||||||
|
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step(label: &str, message: &str) {
|
fn step(label: &str, message: &str) {
|
||||||
@@ -58,21 +61,26 @@ fn clock() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
win::enable_ansi();
|
let code = real_main();
|
||||||
|
platform::restore_terminal();
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn real_main() -> ExitCode {
|
||||||
|
platform::enable_ansi();
|
||||||
let _ = clock();
|
let _ = clock();
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let interactive = args.is_empty();
|
let interactive = args.is_empty();
|
||||||
|
|
||||||
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
|
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
|
||||||
[] => {
|
[] => {
|
||||||
banner();
|
open_invite_once();
|
||||||
invite_prompt();
|
|
||||||
(pick_process(), default_dll())
|
(pick_process(), default_dll())
|
||||||
}
|
}
|
||||||
[pid] => (parse_pid(pid), default_dll()),
|
[pid] => (parse_pid(pid), default_dll()),
|
||||||
[pid, dll] => (parse_pid(pid), PathBuf::from(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);
|
return ExitCode::from(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -81,7 +89,7 @@ fn main() -> ExitCode {
|
|||||||
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
||||||
};
|
};
|
||||||
if !interactive {
|
if !interactive {
|
||||||
banner();
|
print!("{}", banner());
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = run(pid, &dll);
|
let code = run(pid, &dll);
|
||||||
@@ -109,16 +117,18 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let dll_display = dll.display().to_string();
|
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_path = dll.with_file_name("prestige-native.log");
|
||||||
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
|
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("checking", "64-bit JVM, process access, library not already loaded");
|
||||||
step("injecting", "remote thread LoadLibraryW");
|
step("injecting", "loading the native library into the JVM");
|
||||||
match win::inject(pid, &dll) {
|
match platform::inject(pid, &dll) {
|
||||||
Ok(()) => ok("injected", "prestige_native.dll is loaded in the game process"),
|
Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail("failed", &e);
|
fail("failed", &e);
|
||||||
return ExitCode::from(1);
|
return ExitCode::from(1);
|
||||||
@@ -130,7 +140,6 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
|||||||
Outcome::Done => {
|
Outcome::Done => {
|
||||||
println!();
|
println!();
|
||||||
ok("success", "successfully injected - Prestige is running in the game");
|
ok("success", "successfully injected - Prestige is running in the game");
|
||||||
println!("{YELLOW} {WATERMARK}{RESET} {DIM}{INVITE_URL}{RESET}");
|
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
Outcome::Failed(line) => {
|
Outcome::Failed(line) => {
|
||||||
@@ -173,7 +182,9 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
|
|||||||
// lines look like "[unix seconds] message"
|
// lines look like "[unix seconds] message"
|
||||||
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
||||||
let lower = message.to_ascii_lowercase();
|
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);
|
fail("loader", message);
|
||||||
return Outcome::Failed(message.to_string());
|
return Outcome::Failed(message.to_string());
|
||||||
}
|
}
|
||||||
@@ -189,51 +200,26 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn config_path() -> Option<PathBuf> {
|
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.
|
/// Opens the Discord invite on the very first launch only (no prompt); afterwards only the credit line shows it.
|
||||||
fn invite_prompt() {
|
fn open_invite_once() {
|
||||||
let Some(path) = config_path() else { return };
|
let Some(path) = config_path() else { return };
|
||||||
let saved = fs::read_to_string(&path).ok();
|
if path.exists() {
|
||||||
let show = match saved.as_deref() {
|
|
||||||
None => true, // first launch
|
|
||||||
Some(text) => !text.lines().any(|l| l.trim() == "show_invite=false"),
|
|
||||||
};
|
|
||||||
if !show {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let first = saved.is_none();
|
let _ = platform::open_url(INVITE_URL);
|
||||||
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() {
|
if let Some(dir) = path.parent() {
|
||||||
let _ = fs::create_dir_all(dir);
|
let _ = fs::create_dir_all(dir);
|
||||||
}
|
}
|
||||||
let _ = fs::write(&path, format!("show_invite={again}\n"));
|
let _ = fs::write(&path, "invite_opened=true\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.
|
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
|
||||||
fn pick_process() -> Option<u32> {
|
fn pick_process() -> Option<u32> {
|
||||||
let mut selected = 0usize;
|
let mut selected = 0usize;
|
||||||
let mut processes = win::java_windows();
|
let mut processes = platform::java_processes();
|
||||||
let mut last_refresh = Instant::now();
|
let mut last_refresh = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
if selected >= processes.len() {
|
if selected >= processes.len() {
|
||||||
@@ -242,34 +228,34 @@ fn pick_process() -> Option<u32> {
|
|||||||
draw(&processes, selected);
|
draw(&processes, selected);
|
||||||
let deadline = Instant::now() + Duration::from_millis(50);
|
let deadline = Instant::now() + Duration::from_millis(50);
|
||||||
while Instant::now() < deadline {
|
while Instant::now() < deadline {
|
||||||
if let Some(key) = win::poll_key() {
|
if let Some(key) = platform::poll_key() {
|
||||||
match key {
|
match key {
|
||||||
win::Key::Up => selected = selected.saturating_sub(1),
|
platform::Key::Up => selected = selected.saturating_sub(1),
|
||||||
win::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
||||||
win::Key::Enter => {
|
platform::Key::Enter => {
|
||||||
if let Some(p) = processes.get(selected) {
|
if let Some(p) = processes.get(selected) {
|
||||||
return Some(p.pid);
|
return Some(p.pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
win::Key::Escape => return None,
|
platform::Key::Escape => return None,
|
||||||
}
|
}
|
||||||
draw(&processes, selected);
|
draw(&processes, selected);
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
if last_refresh.elapsed() >= Duration::from_millis(750) {
|
if last_refresh.elapsed() >= Duration::from_millis(750) {
|
||||||
processes = win::java_windows();
|
processes = platform::java_processes();
|
||||||
last_refresh = Instant::now();
|
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");
|
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(&banner());
|
||||||
out.push_str("Up/Down select, Enter inject, Esc quit\n\n");
|
out.push_str("Minecraft 1.21.11 (Fabric, 64-bit) - Up/Down select, Enter inject, Esc quit\n\n");
|
||||||
if processes.is_empty() {
|
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() {
|
for (i, p) in processes.iter().enumerate() {
|
||||||
let marker = if i == selected { ">" } else { " " };
|
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)
|
||||||
|
}
|
||||||
+270
-278
@@ -1,278 +1,270 @@
|
|||||||
//! Win32 plumbing of the injector (raw bindings, no external crates).
|
//! Win32 plumbing of the injector (raw bindings, no external crates).
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
use std::os::windows::ffi::OsStrExt;
|
use std::os::windows::ffi::OsStrExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
type Handle = *mut c_void;
|
use super::{JavaProcess, Key};
|
||||||
|
|
||||||
const PROCESS_CREATE_THREAD: u32 = 0x0002;
|
type Handle = *mut c_void;
|
||||||
const PROCESS_VM_OPERATION: u32 = 0x0008;
|
|
||||||
const PROCESS_VM_WRITE: u32 = 0x0020;
|
const PROCESS_CREATE_THREAD: u32 = 0x0002;
|
||||||
const PROCESS_VM_READ: u32 = 0x0010;
|
const PROCESS_VM_OPERATION: u32 = 0x0008;
|
||||||
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
|
const PROCESS_VM_WRITE: u32 = 0x0020;
|
||||||
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
|
const PROCESS_VM_READ: u32 = 0x0010;
|
||||||
const MEM_COMMIT: u32 = 0x1000;
|
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
|
||||||
const MEM_RESERVE: u32 = 0x2000;
|
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
|
||||||
const MEM_RELEASE: u32 = 0x8000;
|
const MEM_COMMIT: u32 = 0x1000;
|
||||||
const PAGE_READWRITE: u32 = 0x04;
|
const MEM_RESERVE: u32 = 0x2000;
|
||||||
const TH32CS_SNAPMODULE: u32 = 0x8;
|
const MEM_RELEASE: u32 = 0x8000;
|
||||||
const TH32CS_SNAPMODULE32: u32 = 0x10;
|
const PAGE_READWRITE: u32 = 0x04;
|
||||||
const WAIT_OBJECT_0: u32 = 0;
|
const TH32CS_SNAPMODULE: u32 = 0x8;
|
||||||
|
const TH32CS_SNAPMODULE32: u32 = 0x10;
|
||||||
#[repr(C)]
|
const WAIT_OBJECT_0: u32 = 0;
|
||||||
struct ModuleEntry32W {
|
|
||||||
size: u32,
|
#[repr(C)]
|
||||||
module_id: u32,
|
struct ModuleEntry32W {
|
||||||
process_id: u32,
|
size: u32,
|
||||||
glblcnt_usage: u32,
|
module_id: u32,
|
||||||
proccnt_usage: u32,
|
process_id: u32,
|
||||||
base_addr: *mut u8,
|
glblcnt_usage: u32,
|
||||||
base_size: u32,
|
proccnt_usage: u32,
|
||||||
module: Handle,
|
base_addr: *mut u8,
|
||||||
sz_module: [u16; 256],
|
base_size: u32,
|
||||||
sz_exe_path: [u16; 260],
|
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;
|
extern "system" {
|
||||||
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
|
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
|
||||||
fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
|
fn CloseHandle(h: Handle) -> i32;
|
||||||
fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32;
|
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
|
||||||
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 VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
|
||||||
fn WaitForSingleObject(h: Handle, ms: u32) -> u32;
|
fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32;
|
||||||
fn GetExitCodeThread(h: Handle, code: *mut u32) -> 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 GetModuleHandleW(name: *const u16) -> Handle;
|
fn WaitForSingleObject(h: Handle, ms: u32) -> u32;
|
||||||
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
|
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
|
||||||
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
|
fn GetModuleHandleW(name: *const u16) -> Handle;
|
||||||
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
|
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
|
||||||
fn GetLastError() -> u32;
|
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
|
||||||
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
|
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
|
||||||
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
fn GetLastError() -> u32;
|
||||||
fn Module32NextW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
|
||||||
fn GetStdHandle(id: u32) -> Handle;
|
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
||||||
fn GetConsoleMode(h: Handle, mode: *mut u32) -> i32;
|
fn Module32NextW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
||||||
fn SetConsoleMode(h: Handle, mode: u32) -> 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;
|
#[link(name = "user32")]
|
||||||
fn IsWindowVisible(hwnd: Handle) -> i32;
|
extern "system" {
|
||||||
fn GetWindowTextW(hwnd: Handle, buf: *mut u16, max: i32) -> i32;
|
fn EnumWindows(callback: unsafe extern "system" fn(Handle, isize) -> i32, param: isize) -> i32;
|
||||||
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32;
|
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;
|
#[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;
|
extern "C" {
|
||||||
}
|
fn _kbhit() -> i32;
|
||||||
|
fn _getch() -> i32;
|
||||||
pub struct JavaWindow {
|
}
|
||||||
pub pid: u32,
|
|
||||||
pub title: String,
|
/// Nothing to restore on Windows (keys are read with _getch).
|
||||||
pub exe: String,
|
pub fn restore_terminal() {}
|
||||||
}
|
|
||||||
|
pub fn enable_ansi() {
|
||||||
pub enum Key {
|
unsafe {
|
||||||
Up,
|
let out = GetStdHandle(-11i32 as u32);
|
||||||
Down,
|
let mut mode = 0;
|
||||||
Enter,
|
if GetConsoleMode(out, &mut mode) != 0 {
|
||||||
Escape,
|
SetConsoleMode(out, mode | 0x0004);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pub fn enable_ansi() {
|
}
|
||||||
unsafe {
|
|
||||||
let out = GetStdHandle(-11i32 as u32);
|
/// Non-blocking key read: arrows arrive as 0xE0/0x00 followed by 72 (up) / 80 (down).
|
||||||
let mut mode = 0;
|
pub fn poll_key() -> Option<Key> {
|
||||||
if GetConsoleMode(out, &mut mode) != 0 {
|
unsafe {
|
||||||
SetConsoleMode(out, mode | 0x0004);
|
if _kbhit() == 0 {
|
||||||
}
|
return None;
|
||||||
}
|
}
|
||||||
}
|
match _getch() {
|
||||||
|
13 => Some(Key::Enter),
|
||||||
/// Non-blocking key read: arrows arrive as 0xE0/0x00 followed by 72 (up) / 80 (down).
|
27 => Some(Key::Escape),
|
||||||
pub fn poll_key() -> Option<Key> {
|
0 | 0xE0 => match _getch() {
|
||||||
unsafe {
|
72 => Some(Key::Up),
|
||||||
if _kbhit() == 0 {
|
80 => Some(Key::Down),
|
||||||
return None;
|
_ => None,
|
||||||
}
|
},
|
||||||
match _getch() {
|
_ => None,
|
||||||
13 => Some(Key::Enter),
|
}
|
||||||
27 => Some(Key::Escape),
|
}
|
||||||
0 | 0xE0 => match _getch() {
|
}
|
||||||
72 => Some(Key::Up),
|
|
||||||
80 => Some(Key::Down),
|
fn wide(path: &Path) -> Vec<u16> {
|
||||||
_ => None,
|
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
|
||||||
},
|
}
|
||||||
_ => None,
|
|
||||||
}
|
fn image_name(pid: u32) -> Option<String> {
|
||||||
}
|
unsafe {
|
||||||
}
|
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
||||||
|
if process.is_null() {
|
||||||
fn wide(path: &Path) -> Vec<u16> {
|
return None;
|
||||||
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
|
}
|
||||||
}
|
let mut buf = [0u16; 1024];
|
||||||
|
let mut len = buf.len() as u32;
|
||||||
fn image_name(pid: u32) -> Option<String> {
|
let ok = QueryFullProcessImageNameW(process, 0, buf.as_mut_ptr(), &mut len);
|
||||||
unsafe {
|
CloseHandle(process);
|
||||||
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
if ok == 0 {
|
||||||
if process.is_null() {
|
return None;
|
||||||
return None;
|
}
|
||||||
}
|
Some(String::from_utf16_lossy(&buf[..len as usize]))
|
||||||
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);
|
unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
|
||||||
if ok == 0 {
|
let out = &mut *(param as *mut Vec<(u32, String)>);
|
||||||
return None;
|
if IsWindowVisible(hwnd) == 0 {
|
||||||
}
|
return 1;
|
||||||
Some(String::from_utf16_lossy(&buf[..len as usize]))
|
}
|
||||||
}
|
let mut buf = [0u16; 256];
|
||||||
}
|
let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
|
||||||
|
if len <= 0 {
|
||||||
unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
|
return 1;
|
||||||
let out = &mut *(param as *mut Vec<(u32, String)>);
|
}
|
||||||
if IsWindowVisible(hwnd) == 0 {
|
let mut pid = 0;
|
||||||
return 1;
|
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||||
}
|
out.push((pid, String::from_utf16_lossy(&buf[..len as usize])));
|
||||||
let mut buf = [0u16; 256];
|
1
|
||||||
let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
|
}
|
||||||
if len <= 0 {
|
|
||||||
return 1;
|
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
|
||||||
}
|
pub fn java_processes() -> Vec<JavaProcess> {
|
||||||
let mut pid = 0;
|
let mut windows: Vec<(u32, String)> = Vec::new();
|
||||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
unsafe {
|
||||||
out.push((pid, String::from_utf16_lossy(&buf[..len as usize])));
|
EnumWindows(collect, &mut windows as *mut _ as isize);
|
||||||
1
|
}
|
||||||
}
|
let mut out: Vec<JavaProcess> = Vec::new();
|
||||||
|
for (pid, title) in windows {
|
||||||
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
|
if out.iter().any(|w| w.pid == pid) {
|
||||||
pub fn java_windows() -> Vec<JavaWindow> {
|
continue;
|
||||||
let mut windows: Vec<(u32, String)> = Vec::new();
|
}
|
||||||
unsafe {
|
let Some(path) = image_name(pid) else { continue };
|
||||||
EnumWindows(collect, &mut windows as *mut _ as isize);
|
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
||||||
}
|
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
||||||
let mut out: Vec<JavaWindow> = Vec::new();
|
out.push(JavaProcess { pid, title, exe });
|
||||||
for (pid, title) in windows {
|
}
|
||||||
if out.iter().any(|w| w.pid == pid) {
|
}
|
||||||
continue;
|
out
|
||||||
}
|
}
|
||||||
let Some(path) = image_name(pid) else { continue };
|
|
||||||
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
fn last_error(what: &str) -> String {
|
||||||
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
format!("{what} failed (error {})", unsafe { GetLastError() })
|
||||||
out.push(JavaWindow { pid, title, exe });
|
}
|
||||||
}
|
|
||||||
}
|
/// True when the module file name is loaded in the process.
|
||||||
out
|
fn module_loaded(pid: u32, file_name: &str) -> bool {
|
||||||
}
|
unsafe {
|
||||||
|
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
|
||||||
fn last_error(what: &str) -> String {
|
if snapshot.is_null() || snapshot as isize == -1 {
|
||||||
format!("{what} failed (error {})", unsafe { GetLastError() })
|
return false;
|
||||||
}
|
}
|
||||||
|
let mut entry: ModuleEntry32W = std::mem::zeroed();
|
||||||
/// True when the module file name is loaded in the process.
|
entry.size = std::mem::size_of::<ModuleEntry32W>() as u32;
|
||||||
fn module_loaded(pid: u32, file_name: &str) -> bool {
|
let mut found = false;
|
||||||
unsafe {
|
let mut ok = Module32FirstW(snapshot, &mut entry);
|
||||||
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
|
while ok != 0 {
|
||||||
if snapshot.is_null() || snapshot as isize == -1 {
|
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
|
||||||
return false;
|
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
|
||||||
}
|
found = true;
|
||||||
let mut entry: ModuleEntry32W = std::mem::zeroed();
|
break;
|
||||||
entry.size = std::mem::size_of::<ModuleEntry32W>() as u32;
|
}
|
||||||
let mut found = false;
|
ok = Module32NextW(snapshot, &mut entry);
|
||||||
let mut ok = Module32FirstW(snapshot, &mut entry);
|
}
|
||||||
while ok != 0 {
|
CloseHandle(snapshot);
|
||||||
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
|
found
|
||||||
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
|
}
|
||||||
found = true;
|
}
|
||||||
break;
|
|
||||||
}
|
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
|
||||||
ok = Module32NextW(snapshot, &mut entry);
|
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
|
||||||
}
|
unsafe {
|
||||||
CloseHandle(snapshot);
|
let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
|
||||||
found
|
let process = OpenProcess(access, 0, pid);
|
||||||
}
|
if process.is_null() {
|
||||||
}
|
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
|
||||||
|
}
|
||||||
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
|
let result = inject_into(process, pid, dll);
|
||||||
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
|
CloseHandle(process);
|
||||||
unsafe {
|
result
|
||||||
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)"));
|
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
|
||||||
}
|
let mut wow64 = 0;
|
||||||
let result = inject_into(process, pid, dll);
|
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
|
||||||
CloseHandle(process);
|
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
|
||||||
result
|
}
|
||||||
}
|
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}"));
|
||||||
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
|
}
|
||||||
let mut wow64 = 0;
|
|
||||||
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
|
let path = wide(dll);
|
||||||
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
|
let bytes = path.len() * 2;
|
||||||
}
|
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
if remote.is_null() {
|
||||||
if module_loaded(pid, &file_name) {
|
return Err(last_error("VirtualAllocEx"));
|
||||||
return Err(format!("{file_name} is already loaded in process {pid}"));
|
}
|
||||||
}
|
let cleanup = |err: String| {
|
||||||
|
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||||
let path = wide(dll);
|
Err(err)
|
||||||
let bytes = path.len() * 2;
|
};
|
||||||
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
|
||||||
if remote.is_null() {
|
return cleanup(last_error("WriteProcessMemory"));
|
||||||
return Err(last_error("VirtualAllocEx"));
|
}
|
||||||
}
|
|
||||||
let cleanup = |err: String| {
|
// kernel32 is mapped at the same base in every process of a boot session, so the local address is valid remotely.
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
let kernel32 = GetModuleHandleW(wide(Path::new("kernel32.dll")).as_ptr());
|
||||||
Err(err)
|
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8);
|
||||||
};
|
if load_library.is_null() {
|
||||||
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
|
return cleanup(last_error("resolve LoadLibraryW"));
|
||||||
return cleanup(last_error("WriteProcessMemory"));
|
}
|
||||||
}
|
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
|
||||||
|
if thread.is_null() {
|
||||||
// kernel32 is mapped at the same base in every process of a boot session, so the local address is valid remotely.
|
return cleanup(last_error("CreateRemoteThread"));
|
||||||
let kernel32 = GetModuleHandleW(wide(Path::new("kernel32.dll")).as_ptr());
|
}
|
||||||
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8);
|
let wait = WaitForSingleObject(thread, 30_000);
|
||||||
if load_library.is_null() {
|
let mut exit = 0;
|
||||||
return cleanup(last_error("resolve LoadLibraryW"));
|
GetExitCodeThread(thread, &mut exit);
|
||||||
}
|
CloseHandle(thread);
|
||||||
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
|
if wait != WAIT_OBJECT_0 {
|
||||||
if thread.is_null() {
|
// The thread may still be running inside the target; leave the buffer alone.
|
||||||
return cleanup(last_error("CreateRemoteThread"));
|
return Err("remote LoadLibraryW did not finish within 30 seconds".into());
|
||||||
}
|
}
|
||||||
let wait = WaitForSingleObject(thread, 30_000);
|
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||||
let mut exit = 0;
|
|
||||||
GetExitCodeThread(thread, &mut exit);
|
// LoadLibraryW's return value is truncated to 32 bits by GetExitCodeThread, so confirm through the module list.
|
||||||
CloseHandle(thread);
|
for _ in 0..20 {
|
||||||
if wait != WAIT_OBJECT_0 {
|
if module_loaded(pid, &file_name) {
|
||||||
// The thread may still be running inside the target; leave the buffer alone.
|
return Ok(());
|
||||||
return Err("remote LoadLibraryW did not finish within 30 seconds".into());
|
}
|
||||||
}
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
}
|
||||||
|
Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)"))
|
||||||
// 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) {
|
/// Opens a URL in the default web browser.
|
||||||
return Ok(());
|
pub fn open_url(url: &str) -> bool {
|
||||||
}
|
let verb: Vec<u16> = "open | ||||||