Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b36349e1a | ||
|
|
2d6b2a3cd1 | ||
|
|
5152e6e0a6 | ||
|
|
0abefe7a04 | ||
|
|
036b24f2ae | ||
|
|
41d3e4a65e |
No files matched your search
@@ -6,18 +6,85 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "injector/**"
|
- "injector/**"
|
||||||
- "src/**"
|
- "src/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- "build.gradle"
|
||||||
- ".github/workflows/injector.yml"
|
- ".github/workflows/injector.yml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
payload:
|
core-imgui:
|
||||||
name: Payload jar
|
name: ImGui (${{ matrix.platform }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: windows-x86_64
|
||||||
|
toolchain: windows-x86_64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: windows-aarch64
|
||||||
|
toolchain: windows-aarch64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux-x86_64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
platform: linux-aarch64
|
||||||
|
toolchain: linux-aarch64
|
||||||
|
- os: macos-14
|
||||||
|
platform: macos-universal
|
||||||
|
toolchain: macos-universal
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v4
|
- uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "21"
|
java-version: "21"
|
||||||
|
- name: Install native build tools
|
||||||
|
if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y cmake g++ g++-aarch64-linux-gnu
|
||||||
|
- name: Install Windows cross compiler
|
||||||
|
if: startsWith(matrix.platform, 'windows-')
|
||||||
|
run: |
|
||||||
|
curl -fL --retry 3 https://github.com/mstorsjo/llvm-mingw/releases/download/20261020/llvm-mingw-20261020-ucrt-ubuntu-22.04-x86_64.tar.xz -o "$RUNNER_TEMP/llvm-mingw.tar.xz"
|
||||||
|
tar -xf "$RUNNER_TEMP/llvm-mingw.tar.xz" -C "$RUNNER_TEMP"
|
||||||
|
echo "$RUNNER_TEMP/llvm-mingw-20261020-ucrt-ubuntu-22.04-x86_64/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build core-only ImGui
|
||||||
|
env:
|
||||||
|
TOOLCHAIN: ${{ matrix.toolchain }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$TOOLCHAIN" ]; then
|
||||||
|
python3 scripts/build_imgui_core.py --output "build/core-natives/${{ matrix.platform }}" --toolchain "scripts/imgui-core/toolchains/$TOOLCHAIN.cmake"
|
||||||
|
else
|
||||||
|
python3 scripts/build_imgui_core.py --output "build/core-natives/${{ matrix.platform }}"
|
||||||
|
fi
|
||||||
|
- name: Exercise core JNI with the UI font
|
||||||
|
if: matrix.platform == 'linux-x86_64' || matrix.platform == 'macos-universal'
|
||||||
|
run: |
|
||||||
|
java -Dimgui.library.path="$PWD/build/core-natives/${{ matrix.platform }}" -cp build/imgui-core/deps/imgui-java-binding-1.92.7.1.jar scripts/imgui-core/SmokeTest.java src/main/resources/prestige_ui/fonts/DMSans-Regular.ttf
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: imgui-${{ matrix.platform }}
|
||||||
|
path: build/core-natives/*
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
payload:
|
||||||
|
name: Payload jar
|
||||||
|
needs: core-imgui
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Check release packaging
|
||||||
|
run: python3 scripts/test_release.py
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "21"
|
||||||
|
- uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
pattern: imgui-*
|
||||||
|
merge-multiple: true
|
||||||
|
path: build/core-natives
|
||||||
- name: Build remapped jar
|
- name: Build remapped jar
|
||||||
run: |
|
run: |
|
||||||
chmod +x ./gradlew
|
chmod +x ./gradlew
|
||||||
@@ -33,42 +100,54 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
injector:
|
injector:
|
||||||
name: Injector (${{ matrix.target }})
|
name: OpenPrestige (${{ matrix.platform.target }})
|
||||||
needs: payload
|
needs: payload
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
platform:
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
target: x86_64-pc-windows-msvc
|
target: x86_64-pc-windows-msvc
|
||||||
name: windows-x86_64
|
name: windows-x86_64
|
||||||
files: |
|
files: |
|
||||||
injector/target/release/PrestigeInjector.exe
|
injector/target/release/OpenPrestigeInjector.exe
|
||||||
injector/target/release/prestige_native.dll
|
injector/target/release/prestige_native.dll
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
target: x86_64-unknown-linux-gnu
|
target: x86_64-unknown-linux-gnu
|
||||||
name: linux-x86_64
|
name: linux-x86_64
|
||||||
files: |
|
files: |
|
||||||
injector/target/release/PrestigeInjector
|
injector/target/release/OpenPrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.so
|
||||||
|
- os: windows-11-arm
|
||||||
|
target: aarch64-pc-windows-msvc
|
||||||
|
name: windows-aarch64
|
||||||
|
files: |
|
||||||
|
injector/target/release/OpenPrestigeInjector.exe
|
||||||
|
injector/target/release/prestige_native.dll
|
||||||
|
- os: ubuntu-24.04-arm
|
||||||
|
target: aarch64-unknown-linux-gnu
|
||||||
|
name: linux-aarch64
|
||||||
|
files: |
|
||||||
|
injector/target/release/OpenPrestigeInjector
|
||||||
injector/target/release/libprestige_native.so
|
injector/target/release/libprestige_native.so
|
||||||
- os: macos-13
|
- os: macos-13
|
||||||
target: x86_64-apple-darwin
|
target: x86_64-apple-darwin
|
||||||
name: macos-x86_64
|
name: macos-x86_64
|
||||||
files: |
|
files: |
|
||||||
injector/target/release/PrestigeInjector
|
injector/target/release/OpenPrestigeInjector
|
||||||
injector/target/release/libprestige_native.dylib
|
injector/target/release/libprestige_native.dylib
|
||||||
- os: macos-14
|
- os: macos-14
|
||||||
target: aarch64-apple-darwin
|
target: aarch64-apple-darwin
|
||||||
name: macos-aarch64
|
name: macos-aarch64
|
||||||
files: |
|
files: |
|
||||||
injector/target/release/PrestigeInjector
|
injector/target/release/OpenPrestigeInjector
|
||||||
injector/target/release/libprestige_native.dylib
|
injector/target/release/libprestige_native.dylib
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.platform.target }}
|
||||||
- name: Download payload jar
|
- name: Download payload jar
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
@@ -81,6 +160,6 @@ jobs:
|
|||||||
- name: Upload injector
|
- name: Upload injector
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: injector-${{ matrix.name }}
|
name: openprestige-${{ matrix.platform.name }}
|
||||||
path: ${{ matrix.files }}
|
path: ${{ matrix.platform.files }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -19,3 +19,8 @@ openvape.zip
|
|||||||
/dist/
|
/dist/
|
||||||
/injector/target/
|
/injector/target/
|
||||||
/tools
|
/tools
|
||||||
|
/examples
|
||||||
|
|
||||||
|
# Python build helpers
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
# Open Prestige
|
# Open Prestige
|
||||||
|
|
||||||
A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Prestige client for Fabric 1.21.11.
|
This repository builds the fully offline OpenPrestige CLI with its payload embedded in the native library. The graphical launcher and runtime downloads are maintained in the separate Prestige repository. See [injector build instructions](injector/README.md).
|
||||||
|
|
||||||
|
A fully deobfuscated, IntelliJ-ready, runnable open sourced version of the Prestige client for Fabric 1.21.11.
|
||||||
|
|
||||||
---
|
---
|
||||||
- we will work on removing the param obf and also renaming vars in the coming days. stay tuned!
|
- we will work on removing the param obf and also renaming vars in the coming days. stay tuned!
|
||||||
- if vikas wants to pay me to remove this, make ticket in discord and we can negotiate.
|
- if vikas wants to pay me to remove this, make ticket in discord and we can negotiate.
|
||||||
@ 1500 discord members, we will release injector
|
|
||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/img/deobf-bird.png" width="260" alt="Deobf?">
|
<img src="docs/img/deobf-bird.png" width="260" alt="Deobf?">
|
||||||
|
|||||||
+3
-5
@@ -1,6 +1,4 @@
|
|||||||
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader DLL,
|
# Build OpenPrestige and collect the host binaries in dist/.
|
||||||
# and collects PrestigeInjector.exe + prestige_native.dll in dist\.
|
|
||||||
# native tools write progress/notes to stderr; failures are detected through $LASTEXITCODE instead
|
|
||||||
$ErrorActionPreference = 'Continue'
|
$ErrorActionPreference = 'Continue'
|
||||||
Set-Location $PSScriptRoot
|
Set-Location $PSScriptRoot
|
||||||
if (Test-Path 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot') { $env:JAVA_HOME = 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot' }
|
if (Test-Path 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot') { $env:JAVA_HOME = 'C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot' }
|
||||||
@@ -9,6 +7,6 @@ if ($LASTEXITCODE -ne 0) { throw 'gradle remapJar failed' }
|
|||||||
cargo build --release --manifest-path injector\Cargo.toml
|
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\OpenPrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
|
||||||
Get-ChildItem build\libs\prestige-*.jar | Where-Object { $_.Name -notmatch '-(dev|sources)\.jar$' } | Copy-Item -Destination dist\ -Force
|
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)'
|
Write-Host 'Done: dist\OpenPrestigeInjector.exe + native library + Fabric mod JAR'
|
||||||
+6
-16
@@ -1,34 +1,24 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader
|
# Build OpenPrestige for the current host. Optional JAVA_HOME selects the Gradle JDK.
|
||||||
# library, and collects PrestigeInjector + libprestige_native.{so,dylib} in dist/.
|
|
||||||
# Optional: set JAVA_HOME to pick the JDK used by Gradle.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
case "$(uname -s)" in
|
case "$(uname -s)" in
|
||||||
Linux) LIB=libprestige_native.so ;;
|
Linux) LIB=libprestige_native.so ;;
|
||||||
Darwin) LIB=libprestige_native.dylib ;;
|
Darwin) LIB=libprestige_native.dylib ;;
|
||||||
*) echo "unsupported OS: $(uname -s) (use build-injector.ps1 on Windows)" >&2; exit 1 ;;
|
*) echo "Use build-injector.ps1 on Windows" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ -n "${JAVA_HOME:-}" ]; then
|
if [ -n "${JAVA_HOME:-}" ]; then
|
||||||
export JAVA_HOME
|
export JAVA_HOME
|
||||||
PATH="$JAVA_HOME/bin:$PATH"
|
PATH="$JAVA_HOME/bin:$PATH"
|
||||||
export PATH
|
export PATH
|
||||||
fi
|
fi
|
||||||
|
|
||||||
./gradlew remapJar --console=plain
|
./gradlew remapJar --console=plain
|
||||||
cargo build --release --manifest-path injector/Cargo.toml
|
cargo build --release --manifest-path injector/Cargo.toml
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
cp -f "injector/target/release/PrestigeInjector" "injector/target/release/$LIB" dist/
|
cp -f "injector/target/release/OpenPrestigeInjector" "injector/target/release/$LIB" dist/
|
||||||
chmod +x dist/PrestigeInjector
|
chmod +x dist/OpenPrestigeInjector
|
||||||
|
|
||||||
# the same remapped jar is the Fabric mod (drop it into mods/)
|
|
||||||
for jar in build/libs/prestige-*.jar; do
|
for jar in build/libs/prestige-*.jar; do
|
||||||
case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac
|
case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac
|
||||||
cp -f "$jar" dist/
|
cp -f "$jar" dist/
|
||||||
done
|
done
|
||||||
|
echo "Done: dist/OpenPrestigeInjector + dist/$LIB + Fabric mod JAR"
|
||||||
echo "Done: dist/PrestigeInjector + dist/$LIB + dist/prestige-*.jar (Fabric mod)"
|
|
||||||
+25
-29
@@ -1,12 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
rem Builds release zips for every platform into dist\release\ (run on Windows):
|
rem Builds fully offline OpenPrestige bundles.
|
||||||
rem prestige-<ver>-windows-x86_64.zip PrestigeInjector.exe + prestige_native.dll + mod jar
|
rem OpenPrestige CLI + native library with embedded payload
|
||||||
rem prestige-<ver>-linux-x86_64.zip PrestigeInjector + libprestige_native.so + mod jar (glibc 2.17+, any distro)
|
rem Requires Rust with MSVC x64/ARM64 tools, Python 3 and JDK 21.
|
||||||
rem prestige-<ver>-macos-universal.zip PrestigeInjector + libprestige_native.dylib + mod jar (Intel + Apple Silicon)
|
rem Fetches pinned ImGui sources and cross-compiles the small JNI libraries.
|
||||||
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
|
setlocal EnableExtensions
|
||||||
cd /d "%~dp0"
|
cd /d "%~dp0"
|
||||||
set "SELF=%~f0"
|
set "SELF=%~f0"
|
||||||
@@ -23,55 +19,55 @@ where cargo >nul 2>&1 || (echo cargo not found: install Rust from https://rustup
|
|||||||
where python >nul 2>&1 || (echo python not found: install Python 3 from https://python.org & 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 || (
|
python -m ziglang version >nul 2>&1 || (
|
||||||
echo === Installing zig ^(pip ziglang^)
|
echo === Installing zig ^(pip ziglang^)
|
||||||
python -m pip install --user ziglang || goto :fail
|
python -m pip install --user ziglang==0.16.0 || goto :fail
|
||||||
)
|
)
|
||||||
where cargo-zigbuild >nul 2>&1 || (
|
where cargo-zigbuild >nul 2>&1 || (
|
||||||
echo === Installing cargo-zigbuild
|
echo === Installing cargo-zigbuild
|
||||||
cargo install --locked cargo-zigbuild || goto :fail
|
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
|
rustup target add x86_64-pc-windows-msvc aarch64-pc-windows-msvc x86_64-unknown-linux-gnu aarch64-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
|
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"
|
if not exist "%LOCALAPPDATA%\cargo-zigbuild" mkdir "%LOCALAPPDATA%\cargo-zigbuild"
|
||||||
for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI"
|
for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI"
|
||||||
|
|
||||||
|
rem ---- core ImGui JNI libraries ------------------------------------------
|
||||||
|
echo === Fetching sources and building small ImGui libraries
|
||||||
|
python scripts\build_release_imgui.py || goto :fail
|
||||||
|
|
||||||
rem ---- payload jar -------------------------------------------------------
|
rem ---- payload jar -------------------------------------------------------
|
||||||
echo === Building payload jar
|
echo === Building payload jar
|
||||||
call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail
|
call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail
|
||||||
set "JAR="
|
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"
|
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)
|
if not defined JAR (echo no remapped jar in build\libs & goto :fail)
|
||||||
rem every target embeds this exact jar
|
rem The native library uses this exact payload JAR.
|
||||||
set "PRESTIGE_PAYLOAD=%JAR%"
|
set "PRESTIGE_PAYLOAD=%JAR%"
|
||||||
|
|
||||||
if exist "%OUT%" rmdir /s /q "%OUT%"
|
|
||||||
mkdir "%OUT%" || goto :fail
|
if not exist "%OUT%" mkdir "%OUT%" || goto :fail
|
||||||
|
|
||||||
rem ---- native builds -----------------------------------------------------
|
rem ---- native builds -----------------------------------------------------
|
||||||
echo === Windows x86_64
|
call :build windows-x86_64 x86_64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail
|
||||||
cargo build --release --manifest-path injector\Cargo.toml --target x86_64-pc-windows-msvc || goto :fail
|
call :build windows-aarch64 aarch64-pc-windows-msvc OpenPrestigeInjector.exe prestige_native.dll build || goto :fail
|
||||||
call :package windows-x86_64 x86_64-pc-windows-msvc PrestigeInjector.exe prestige_native.dll || goto :fail
|
call :build linux-x86_64 x86_64-unknown-linux-gnu OpenPrestigeInjector libprestige_native.so zigbuild x86_64-unknown-linux-gnu.2.17 || goto :fail
|
||||||
|
call :build linux-aarch64 aarch64-unknown-linux-gnu OpenPrestigeInjector libprestige_native.so zigbuild aarch64-unknown-linux-gnu.2.17 || goto :fail
|
||||||
echo === Linux x86_64 (glibc 2.17+)
|
call :build macos-universal universal2-apple-darwin OpenPrestigeInjector libprestige_native.dylib zigbuild || goto :fail
|
||||||
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
|
copy /y "%JAR%" "%OUT%\" >nul || goto :fail
|
||||||
python scripts\package_release.py checksums "%OUT%" || goto :fail
|
python scripts\package_release.py checksums "%OUT%" || goto :fail
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo === Done: %OUT%
|
echo === Done: %OUT%
|
||||||
dir /b "%OUT%"
|
|
||||||
call :maybe_pause
|
call :maybe_pause
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
||||||
rem %1 platform name, %2 cargo target dir, %3 injector file, %4 library file
|
rem platform, target directory, executable, library, cargo command, optional versioned target
|
||||||
:package
|
:build
|
||||||
python scripts\package_release.py zip "%OUT%" "%VERSION%" %1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%"
|
set "BUILD_TARGET=%~2"
|
||||||
|
if not "%~6"=="" set "BUILD_TARGET=%~6"
|
||||||
|
echo === OpenPrestige %~1 (offline)
|
||||||
|
cargo %~5 --release --manifest-path injector\Cargo.toml --target "%BUILD_TARGET%" || exit /b 1
|
||||||
|
python scripts\package_release.py offline "%OUT%" "%VERSION%" %~1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%"
|
||||||
exit /b %ERRORLEVEL%
|
exit /b %ERRORLEVEL%
|
||||||
|
|
||||||
:fail
|
:fail
|
||||||
|
|||||||
@@ -87,12 +87,29 @@ java {
|
|||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
|
// Optional custom native builds take precedence over the upstream binaries.
|
||||||
|
from('build/core-natives') {
|
||||||
|
into 'prestige-natives'
|
||||||
|
}
|
||||||
|
from('build/arm-natives') {
|
||||||
|
into 'prestige-natives'
|
||||||
|
}
|
||||||
// Unpack the shaded libs (imgui classes + native dll, ClassTransform) into the mod jar.
|
// Unpack the shaded libs (imgui classes + native dll, ClassTransform) into the mod jar.
|
||||||
from({
|
from({
|
||||||
configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
|
configurations.shade.collect { it.isDirectory() ? it : zipTree(it) }
|
||||||
}) {
|
}) {
|
||||||
exclude 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA'
|
exclude 'META-INF/MANIFEST.MF', 'META-INF/*.SF', 'META-INF/*.RSA', 'META-INF/*.DSA'
|
||||||
exclude 'module-info.class', 'META-INF/versions/**'
|
exclude 'module-info.class', 'META-INF/versions/**'
|
||||||
|
exclude 'imgui/extension/**'
|
||||||
|
if (file('build/core-natives/windows-x86_64/imgui-java64.dll').isFile()) {
|
||||||
|
exclude 'io/imgui/java/native-bin/imgui-java64.dll'
|
||||||
|
}
|
||||||
|
if (file('build/core-natives/linux-x86_64/libimgui-java64.so').isFile()) {
|
||||||
|
exclude 'io/imgui/java/native-bin/libimgui-java64.so'
|
||||||
|
}
|
||||||
|
if (file('build/core-natives/macos-universal/libimgui-java64.dylib').isFile()) {
|
||||||
|
exclude 'io/imgui/java/native-bin/libimgui-java64.dylib'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
from({ zipTree(configurations.engineMappings.singleFile) }) {
|
from({ zipTree(configurations.engineMappings.singleFile) }) {
|
||||||
include 'mappings/mappings.tiny'
|
include 'mappings/mappings.tiny'
|
||||||
|
|||||||
Generated
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prestige-injector"
|
name = "openprestige-injector"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+45
-75
@@ -1,96 +1,66 @@
|
|||||||
# Prestige injector (Rust)
|
# OpenPrestige offline injector
|
||||||
|
|
||||||
`PrestigeInjector` loads the native loader library (`prestige_native.dll` / `libprestige_native.so` / `libprestige_native.dylib`)
|
This repository contains the original CLI injector, native library with embedded payload and game
|
||||||
into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x, Fabric API installed). It uses the same overall
|
sources. The graphical launcher and runtime downloader are maintained in the separate Prestige
|
||||||
method as the openvape bundle, written from scratch in Rust with no external crates.
|
repository. There is no GUI or HTTP dependency in this injector and no edition build flag.
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
1. **Injector** lists the running Fabric client JVMs, then loads the library into the chosen process.
|
|
||||||
- **Windows:** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
|
|
||||||
process through `CreateRemoteThread` and confirms the module is mapped.
|
|
||||||
- **Linux / macOS:** uses the HotSpot attach protocol (the same mechanism as `jcmd <pid> JVMTI.agent_load`). The injector
|
|
||||||
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;
|
|
||||||
- registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`.
|
|
||||||
3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes
|
|
||||||
(JVMTI retransform capture), runs the hooks through `InPlaceTransformer` and redefines the class. The bundled ClassTransform
|
|
||||||
inlines hook handlers into the target methods, so normally nothing is added to the class and a plain redefine works. If a
|
|
||||||
transformer ever does add methods (a running JVM cannot), they are moved into generated `dev.zprestige.prestige.hooks.H*`
|
|
||||||
helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles.
|
|
||||||
Finally the Prestige and ClickGUI entrypoints run on the client thread.
|
|
||||||
|
|
||||||
## 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
|
Run `build-injector.ps1` on Windows or `build-injector.sh` on Linux/macOS. Both build the Fabric JAR,
|
||||||
`gradlew remapJar`, `cargo build --release`, and copy the results to `dist/`.
|
then the native library and CLI, collecting the results in `dist/`. The native library always embeds
|
||||||
|
the exact remapped JAR. The loose JAR is provided for optional installation as a Fabric mod.
|
||||||
|
|
||||||
```powershell
|
Run `build-release.bat` on Windows to build all platforms. `dist/release/` contains:
|
||||||
.\build-injector.ps1 # Windows: dist\PrestigeInjector.exe + dist\prestige_native.dll
|
|
||||||
|
```text
|
||||||
|
openprestige-<version>-windows-x86_64.zip
|
||||||
|
openprestige-<version>-windows-aarch64.zip
|
||||||
|
openprestige-<version>-linux-x86_64.zip
|
||||||
|
openprestige-<version>-linux-aarch64.zip
|
||||||
|
openprestige-<version>-macos-universal.zip
|
||||||
|
prestige-<version>.jar
|
||||||
|
SHA256SUMS.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
Each ZIP contains `OpenPrestigeInjector` (or `.exe`), the native library, and a README.
|
||||||
./build-injector.sh # Linux/macOS: dist/PrestigeInjector + dist/libprestige_native.{so,dylib}
|
The CI injector workflow builds only OpenPrestige. Build dependencies require internet access;
|
||||||
```
|
the resulting release bundle needs no runtime downloads.
|
||||||
|
|
||||||
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:
|
Extract the complete platform ZIP, start Minecraft, then run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
PrestigeInjector # interactive: pick the game from the list
|
OpenPrestigeInjector # Up/Down selects, Enter injects, Esc quits
|
||||||
PrestigeInjector <pid> [lib path] # inject into a specific pid, optionally with an explicit library path
|
OpenPrestigeInjector <pid> [lib path] # optional explicit library path
|
||||||
```
|
```
|
||||||
|
|
||||||
On Windows this is `PrestigeInjector.exe`. By default the library is looked up beside the injector.
|
Keep the native library beside the executable. The CLI uses that library and never looks in
|
||||||
|
Prestige's downloaded runtime cache. It has no update checks or `--download` command.
|
||||||
|
The library writes its embedded JAR locally so the JVM can load it. No external JAR is needed for
|
||||||
|
injection. Linux/macOS require a terminal, the same user as the game, and JVM attach enabled.
|
||||||
|
|
||||||
## Install as a mod instead
|
## Platforms and requirements
|
||||||
|
|
||||||
The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the
|
Windows and Linux support x86_64 and ARM64. macOS supports Intel and Apple Silicon.
|
||||||
`mods/` folder of a Fabric Loader 0.19+ / Fabric API / Minecraft 1.21.11 instance; no injector is needed, on any OS.
|
The injector, library and JVM architectures must match. ARM64 execution needs hardware testing.
|
||||||
Do not use both: if the mod is already loaded, the injector's bootstrap logs "already loaded as a Fabric mod" and skips.
|
The game code currently targets Minecraft 1.21.11 Fabric, Fabric Loader 0.19+ and Fabric API.
|
||||||
|
|
||||||
## Requirements (Linux / macOS)
|
Build tools require Rust, Python 3 and JDK 21. On Windows, the full release build also requires
|
||||||
|
MSVC build tools for x64 and ARM64. `build-release.bat` fetches pinned ImGui sources and builds
|
||||||
|
the small JNI libraries before building the remapped JAR. It installs Zig and cargo-zigbuild if
|
||||||
|
needed, then cross-compiles Linux x64/ARM64 and universal macOS.
|
||||||
|
|
||||||
- Run the injector as the **same user** as the game.
|
Both repositories currently start from the same game sources and use the compatible `.openprestige`
|
||||||
- The attach mechanism must be enabled: the game must not be started with `-XX:+DisableAttachMechanism`.
|
configuration and log locations. They are independent repositories, without submodules, shared
|
||||||
- The JVM architecture must match the injector build. On Apple Silicon use an arm64 JVM, or use the x86_64 build for JVMs
|
working files or edition build flags. Game code fixes must be ported between repositories explicitly.
|
||||||
running under Rosetta.
|
|
||||||
- Sandboxed launchers (Flatpak, Snap) hide the attach socket and are not supported.
|
|
||||||
|
|
||||||
## Known limits
|
## User data
|
||||||
|
|
||||||
- On Linux/macOS the library stays loaded after self-destruct: hooks are removed and natives unregistered, but the library is
|
| OS | Location |
|
||||||
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.
|
| Windows | `%APPDATA%\.openprestige` |
|
||||||
|
| macOS | `~/Library/Application Support/.openprestige` |
|
||||||
|
| Linux | `$XDG_DATA_HOME/.openprestige` or `~/.local/share/.openprestige` |
|
||||||
|
|
||||||
## Log
|
Configs live in `configs/`, logs in `logs/`, and local runtime files in `runtime/`.
|
||||||
|
|
||||||
`prestige-native.log` is written beside the loader library, plus `[Prestige-Inject]` lines in the game log.
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "prestige-injector"
|
name = "openprestige-injector"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "PrestigeInjector"
|
name = "OpenPrestigeInjector"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fn main() {
|
||||||
|
let properties = "../../../gradle.properties";
|
||||||
|
println!("cargo:rerun-if-changed={properties}");
|
||||||
|
let contents = std::fs::read_to_string(properties).expect("read gradle.properties");
|
||||||
|
let version = contents
|
||||||
|
.lines()
|
||||||
|
.find_map(|line| line.strip_prefix("mod_version="))
|
||||||
|
.expect("mod_version is required")
|
||||||
|
.trim();
|
||||||
|
println!("cargo:rustc-env=PRESTIGE_VERSION={version}");
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
|
//! OpenPrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
|
||||||
//! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW
|
//! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW
|
||||||
//! thread on Windows, the HotSpot attach protocol on Linux and macOS. The library does everything else once it is inside
|
//! 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.
|
//! the JVM; the injector tails its log so every stage (transforming, injecting, initialising) is visible.
|
||||||
//!
|
//!
|
||||||
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc)
|
//! OpenPrestigeInjector interactive process picker (Up/Down, Enter, Esc)
|
||||||
//! PrestigeInjector <pid> [lib path] non-interactive
|
//! OpenPrestigeInjector <pid> [lib path] non-interactive
|
||||||
|
#[path = "../../paths.rs"]
|
||||||
|
mod paths;
|
||||||
mod platform;
|
mod platform;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -15,7 +17,6 @@ use std::time::{Duration, Instant};
|
|||||||
|
|
||||||
/// The one credit line, shown once at the top of the screen.
|
/// The one credit line, shown once at the top of the screen.
|
||||||
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
|
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
|
||||||
const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg";
|
|
||||||
/// How long to follow the DLL's progress after it has been loaded.
|
/// 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);
|
||||||
|
|
||||||
@@ -26,11 +27,15 @@ const GREEN: &str = "\x1b[92m";
|
|||||||
const YELLOW: &str = "\x1b[93m";
|
const YELLOW: &str = "\x1b[93m";
|
||||||
const RED: &str = "\x1b[91m";
|
const RED: &str = "\x1b[91m";
|
||||||
|
|
||||||
fn default_dll() -> PathBuf {
|
fn prepare_runtime(_offline: bool, _ask_for_updates: bool) -> Result<PathBuf, String> {
|
||||||
std::env::current_exe()
|
let path = std::env::current_exe()
|
||||||
.ok()
|
.map_err(|error| error.to_string())?
|
||||||
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
|
.with_file_name(platform::LIB_NAME);
|
||||||
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
|
if path.is_file() {
|
||||||
|
Ok(path)
|
||||||
|
} else {
|
||||||
|
Err(format!("OpenPrestige requires {} beside the CLI executable. Extract the complete offline bundle.", platform::LIB_NAME))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn banner() -> String {
|
fn banner() -> String {
|
||||||
@@ -38,26 +43,53 @@ fn banner() -> String {
|
|||||||
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
|
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
|
||||||
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
|
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
|
||||||
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
|
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
|
||||||
out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"));
|
out.push_str(&format!(
|
||||||
|
" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"
|
||||||
|
));
|
||||||
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
|
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
|
||||||
|
out.push_str("OpenPrestige - offline CLI\n\n");
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step(label: &str, message: &str) {
|
fn step(label: &str, message: &str) {
|
||||||
println!("{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}", clock());
|
println!(
|
||||||
|
"{DIM}[{:>7}]{RESET} {CYAN}{label:<12}{RESET} {message}",
|
||||||
|
clock()
|
||||||
|
);
|
||||||
|
log_launcher(label, message);
|
||||||
}
|
}
|
||||||
fn ok(label: &str, message: &str) {
|
fn ok(label: &str, message: &str) {
|
||||||
println!("{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}", clock());
|
println!(
|
||||||
|
"{DIM}[{:>7}]{RESET} {GREEN}{label:<12}{RESET} {message}",
|
||||||
|
clock()
|
||||||
|
);
|
||||||
|
log_launcher(label, message);
|
||||||
}
|
}
|
||||||
fn fail(label: &str, message: &str) {
|
fn fail(label: &str, message: &str) {
|
||||||
println!("{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}", clock());
|
println!(
|
||||||
|
"{DIM}[{:>7}]{RESET} {RED}{label:<12}{RESET} {message}",
|
||||||
|
clock()
|
||||||
|
);
|
||||||
|
log_launcher(label, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_launcher(label: &str, message: &str) {
|
||||||
|
let path = paths::root()
|
||||||
|
.join("logs")
|
||||||
|
.join(format!("launcher-{}.log", std::process::id()));
|
||||||
|
if let Ok(mut file) = fs::OpenOptions::new().create(true).append(true).open(path) {
|
||||||
|
let _ = writeln!(file, "[{}] {label} {message}", clock());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Seconds since the injector started, for the log prefix.
|
/// Seconds since the injector started, for the log prefix.
|
||||||
fn clock() -> String {
|
fn clock() -> String {
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
static START: OnceLock<Instant> = OnceLock::new();
|
static START: OnceLock<Instant> = OnceLock::new();
|
||||||
format!("{:.1}s", START.get_or_init(Instant::now).elapsed().as_secs_f32())
|
format!(
|
||||||
|
"{:.1}s",
|
||||||
|
START.get_or_init(Instant::now).elapsed().as_secs_f32()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -70,27 +102,47 @@ fn real_main() -> ExitCode {
|
|||||||
platform::enable_ansi();
|
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();
|
if let Err(error) = paths::initialize() {
|
||||||
|
fail("storage", &error.to_string());
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
|
let interactive = args.is_empty()
|
||||||
|
|| matches!(args.as_slice(), [flag] if flag == "--console" || flag == "--offline");
|
||||||
|
|
||||||
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
|
let (pid, dll): (Option<u32>, Result<PathBuf, String>) = match args.as_slice() {
|
||||||
[] => {
|
_ if interactive => {
|
||||||
open_invite_once();
|
let library = prepare_runtime(args.iter().any(|arg| arg == "--offline"), true);
|
||||||
(pick_process(), default_dll())
|
if let Err(error) = &library {
|
||||||
|
fail("runtime", error);
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
|
(pick_process(), library)
|
||||||
}
|
}
|
||||||
[pid] => (parse_pid(pid), default_dll()),
|
[pid] => (parse_pid(pid), prepare_runtime(true, false)),
|
||||||
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
|
[pid, dll] => (parse_pid(pid), Ok(PathBuf::from(dll))),
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("usage: PrestigeInjector [<pid> [lib path]]");
|
eprintln!("usage: OpenPrestigeInjector [--console | --offline | <pid> [lib path]]");
|
||||||
return ExitCode::from(2);
|
return ExitCode::from(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some(pid) = pid else {
|
let Some(pid) = pid else {
|
||||||
// interactive mode quit with Esc, or an unparsable pid was given
|
// interactive mode quit with Esc, or an unparsable pid was given
|
||||||
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
return if interactive {
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
} else {
|
||||||
|
ExitCode::from(2)
|
||||||
|
};
|
||||||
};
|
};
|
||||||
if !interactive {
|
if !interactive {
|
||||||
print!("{}", banner());
|
print!("{}", banner());
|
||||||
}
|
}
|
||||||
|
let dll = match dll {
|
||||||
|
Ok(path) => path,
|
||||||
|
Err(error) => {
|
||||||
|
fail("runtime", &error);
|
||||||
|
return ExitCode::from(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let code = run(pid, &dll);
|
let code = run(pid, &dll);
|
||||||
if interactive {
|
if interactive {
|
||||||
@@ -121,35 +173,58 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
|||||||
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
|
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
|
||||||
step("loader", &dll_display);
|
step("loader", &dll_display);
|
||||||
|
|
||||||
// The library appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
|
let log_path = paths::native_log(pid);
|
||||||
let log_path = dll.with_file_name("prestige-native.log");
|
let logs = [log_path.clone(), dll.with_file_name("prestige-native.log")];
|
||||||
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
|
let starts = logs
|
||||||
|
.each_ref()
|
||||||
|
.map(|path| fs::metadata(path).map(|m| m.len()).unwrap_or(0));
|
||||||
|
|
||||||
step("checking", "64-bit JVM, process access, library not already loaded");
|
step(
|
||||||
|
"checking",
|
||||||
|
"64-bit JVM, process access, library not already loaded",
|
||||||
|
);
|
||||||
step("injecting", "loading the native library into the JVM");
|
step("injecting", "loading the native library into the JVM");
|
||||||
match platform::inject(pid, &dll) {
|
match platform::inject(pid, &dll) {
|
||||||
Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)),
|
Ok(()) => ok(
|
||||||
|
"injected",
|
||||||
|
&format!("{} is loaded in the game process", platform::LIB_NAME),
|
||||||
|
),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail("failed", &e);
|
fail("failed", &e);
|
||||||
return ExitCode::from(1);
|
return ExitCode::from(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
step("transforming", "following the loader log (attach, class loader, hook transformer)...");
|
step(
|
||||||
match follow_log(&log_path, log_start) {
|
"transforming",
|
||||||
|
"following the loader log (attach, class loader, hook transformer)...",
|
||||||
|
);
|
||||||
|
match follow_log(&logs, starts) {
|
||||||
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",
|
||||||
|
);
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
Outcome::Failed(line) => {
|
Outcome::Failed(line) => {
|
||||||
fail("failed", &format!("injection did not complete: {line}"));
|
fail("failed", &format!("injection did not complete: {line}"));
|
||||||
println!("{DIM} details: {} and the game's latest.log{RESET}", log_path.display());
|
println!(
|
||||||
|
"{DIM} details: {} and the game's latest.log{RESET}",
|
||||||
|
log_path.display()
|
||||||
|
);
|
||||||
ExitCode::from(1)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
Outcome::TimedOut => {
|
Outcome::TimedOut => {
|
||||||
fail("timeout", "no completion message from the loader within 3 minutes");
|
fail(
|
||||||
println!("{DIM} check {} and the game's latest.log{RESET}", log_path.display());
|
"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)
|
ExitCode::from(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -162,76 +237,77 @@ enum Outcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Prints the lines the loader/bootstrap append to the log until they report success or failure.
|
/// Prints the lines the loader/bootstrap append to the log until they report success or failure.
|
||||||
fn follow_log(path: &Path, start: u64) -> Outcome {
|
fn follow_log(paths: &[PathBuf; 2], mut offsets: [u64; 2]) -> Outcome {
|
||||||
let deadline = Instant::now() + FOLLOW_TIMEOUT;
|
let deadline = Instant::now() + FOLLOW_TIMEOUT;
|
||||||
let mut offset = start;
|
let mut pending = [String::new(), String::new()];
|
||||||
let mut pending = String::new();
|
|
||||||
while Instant::now() < deadline {
|
while Instant::now() < deadline {
|
||||||
if let Ok(mut file) = fs::File::open(path) {
|
for (index, path) in paths.iter().enumerate() {
|
||||||
if file.seek(SeekFrom::Start(offset)).is_ok() {
|
let offset = &mut offsets[index];
|
||||||
let mut chunk = String::new();
|
let pending = &mut pending[index];
|
||||||
if let Ok(n) = file.read_to_string(&mut chunk) {
|
if let Ok(mut file) = fs::File::open(path) {
|
||||||
offset += n as u64;
|
if file.seek(SeekFrom::Start(*offset)).is_ok() {
|
||||||
pending.push_str(&chunk);
|
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') {
|
||||||
while let Some(end) = pending.find('\n') {
|
let line: String = pending.drain(..=end).collect();
|
||||||
let line: String = pending.drain(..=end).collect();
|
// Keep the output from older runtime bundles in the shared log too.
|
||||||
let line = line.trim_end();
|
if index == 1 {
|
||||||
// lines look like "[unix seconds] message"
|
if let Ok(mut log) = fs::OpenOptions::new()
|
||||||
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
.create(true)
|
||||||
let lower = message.to_ascii_lowercase();
|
.append(true)
|
||||||
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
|
.open(&paths[0])
|
||||||
let fatal = message.starts_with("FAILED:") || lower.contains("start failed") || lower.contains("client init failed") || lower.contains("could not hook");
|
{
|
||||||
if fatal {
|
if log.write_all(line.as_bytes()).is_ok() {
|
||||||
fail("loader", message);
|
offsets[0] += line.len() as u64;
|
||||||
return Outcome::Failed(message.to_string());
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let line = line.trim_end();
|
||||||
|
// lines look like "[unix seconds] message"
|
||||||
|
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
||||||
|
let lower = message.to_ascii_lowercase();
|
||||||
|
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
|
||||||
|
let fatal = message.starts_with("FAILED:")
|
||||||
|
|| lower.contains("start failed")
|
||||||
|
|| lower.contains("client init failed")
|
||||||
|
|| lower.contains("could not hook");
|
||||||
|
if fatal {
|
||||||
|
fail("loader", message);
|
||||||
|
return Outcome::Failed(message.to_string());
|
||||||
|
}
|
||||||
|
if message.contains("client initialised") {
|
||||||
|
ok("loader", message);
|
||||||
|
return Outcome::Done;
|
||||||
|
}
|
||||||
|
step("loader", message);
|
||||||
}
|
}
|
||||||
if message.contains("client initialised") {
|
|
||||||
ok("loader", message);
|
|
||||||
return Outcome::Done;
|
|
||||||
}
|
|
||||||
step("loader", message);
|
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(200));
|
std::thread::sleep(Duration::from_millis(200));
|
||||||
}
|
}
|
||||||
Outcome::TimedOut
|
Outcome::TimedOut
|
||||||
}
|
}
|
||||||
|
|
||||||
fn config_path() -> Option<PathBuf> {
|
|
||||||
platform::config_dir().map(|dir| dir.join("PrestigeInjector").join("settings.txt"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Opens the Discord invite on the very first launch only (no prompt); afterwards only the credit line shows it.
|
|
||||||
fn open_invite_once() {
|
|
||||||
let Some(path) = config_path() else { return };
|
|
||||||
if path.exists() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let _ = platform::open_url(INVITE_URL);
|
|
||||||
if let Some(dir) = path.parent() {
|
|
||||||
let _ = fs::create_dir_all(dir);
|
|
||||||
}
|
|
||||||
let _ = fs::write(&path, "invite_opened=true\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
|
/// 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 = platform::java_processes();
|
let mut processes = platform::java_processes();
|
||||||
let mut last_refresh = Instant::now();
|
let mut last_refresh = Instant::now();
|
||||||
|
draw(&processes, selected);
|
||||||
loop {
|
loop {
|
||||||
if selected >= processes.len() {
|
|
||||||
selected = processes.len().saturating_sub(1);
|
|
||||||
}
|
|
||||||
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) = platform::poll_key() {
|
if let Some(key) = platform::poll_key() {
|
||||||
|
let previous = selected;
|
||||||
match key {
|
match key {
|
||||||
platform::Key::Up => selected = selected.saturating_sub(1),
|
platform::Key::Up => selected = selected.saturating_sub(1),
|
||||||
platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
platform::Key::Down => {
|
||||||
|
selected = (selected + 1).min(processes.len().saturating_sub(1))
|
||||||
|
}
|
||||||
platform::Key::Enter => {
|
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);
|
||||||
@@ -239,12 +315,19 @@ fn pick_process() -> Option<u32> {
|
|||||||
}
|
}
|
||||||
platform::Key::Escape => return None,
|
platform::Key::Escape => return None,
|
||||||
}
|
}
|
||||||
draw(&processes, selected);
|
if selected != previous {
|
||||||
|
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 = platform::java_processes();
|
let refreshed = platform::java_processes();
|
||||||
|
if refreshed != processes {
|
||||||
|
processes = refreshed;
|
||||||
|
selected = selected.min(processes.len().saturating_sub(1));
|
||||||
|
draw(&processes, selected);
|
||||||
|
}
|
||||||
last_refresh = Instant::now();
|
last_refresh = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,7 +342,10 @@ fn draw(processes: &[platform::JavaProcess], selected: usize) {
|
|||||||
}
|
}
|
||||||
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 { " " };
|
||||||
out.push_str(&format!("{marker} [{:>6}] {} ({})\n", p.pid, p.title, p.exe));
|
out.push_str(&format!(
|
||||||
|
"{marker} [{:>6}] {} ({})\n",
|
||||||
|
p.pid, p.title, p.exe
|
||||||
|
));
|
||||||
}
|
}
|
||||||
print!("{out}");
|
print!("{out}");
|
||||||
let _ = io::stdout().flush();
|
let _ = io::stdout().flush();
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Platform backends of the injector. Each backend exposes the same functions:
|
//! Platform backends of the injector. Each backend exposes the same functions:
|
||||||
//! enable_ansi, poll_key, java_processes, inject, open_url, restore_terminal.
|
//! enable_ansi, poll_key, java_processes, inject, restore_terminal.
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// A running Minecraft JVM shown in the picker.
|
/// A running Minecraft JVM shown in the picker.
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub struct JavaProcess {
|
pub struct JavaProcess {
|
||||||
pub pid: u32,
|
pub pid: u32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
@@ -32,7 +32,7 @@ pub use unix_attach::inject;
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use unix_procs::java_processes;
|
pub use unix_procs::java_processes;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use unix_term::{enable_ansi, open_url, poll_key, restore_terminal};
|
pub use unix_term::{enable_ansi, poll_key, restore_terminal};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub const LIB_NAME: &str = "prestige_native.dll";
|
pub const LIB_NAME: &str = "prestige_native.dll";
|
||||||
@@ -40,22 +40,3 @@ pub const LIB_NAME: &str = "prestige_native.dll";
|
|||||||
pub const LIB_NAME: &str = "libprestige_native.dylib";
|
pub const LIB_NAME: &str = "libprestige_native.dylib";
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
pub const LIB_NAME: &str = "libprestige_native.so";
|
pub const LIB_NAME: &str = "libprestige_native.so";
|
||||||
|
|
||||||
/// Per-user configuration directory: APPDATA, $XDG_CONFIG_HOME or ~/.config, ~/Library/Application Support.
|
|
||||||
pub fn config_dir() -> Option<PathBuf> {
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
std::env::var_os("APPDATA").map(PathBuf::from)
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
{
|
|
||||||
std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library").join("Application Support"))
|
|
||||||
}
|
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
|
||||||
{
|
|
||||||
match std::env::var_os("XDG_CONFIG_HOME") {
|
|
||||||
Some(dir) if !dir.is_empty() => Some(PathBuf::from(dir)),
|
|
||||||
_ => std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Terminal input and URL opening on Linux and macOS. std only (no termios structs).
|
//! Terminal input on Linux and macOS. std only (no termios structs).
|
||||||
//!
|
//!
|
||||||
//! Raw mode is entered lazily on the first `poll_key` call via `stty -icanon -echo min 1 time 0`
|
//! 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
|
//! on /dev/tty (ISIG stays on, so Ctrl-C still raises SIGINT). A reader thread blocks on the tty
|
||||||
@@ -37,8 +37,15 @@ fn tty_stdin() -> Stdio {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn stty(args: &[&str]) -> Option<String> {
|
fn stty(args: &[&str]) -> Option<String> {
|
||||||
let out = Command::new("stty").args(args).stdin(tty_stdin()).stderr(Stdio::null()).output().ok()?;
|
let out = Command::new("stty")
|
||||||
out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
|
.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`.
|
/// Nothing to do on unix (terminals understand ANSI); raw mode is entered lazily by `poll_key`.
|
||||||
@@ -113,7 +120,9 @@ pub fn poll_key() -> Option<Key> {
|
|||||||
27 => {
|
27 => {
|
||||||
let ms30 = Duration::from_millis(30);
|
let ms30 = Duration::from_millis(30);
|
||||||
match rx.recv_timeout(ms30) {
|
match rx.recv_timeout(ms30) {
|
||||||
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => return Some(Key::Escape),
|
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
return Some(Key::Escape)
|
||||||
|
}
|
||||||
Ok(b'[') | Ok(b'O') => match rx.recv_timeout(ms30) {
|
Ok(b'[') | Ok(b'O') => match rx.recv_timeout(ms30) {
|
||||||
Ok(b'A') => return Some(Key::Up),
|
Ok(b'A') => return Some(Key::Up),
|
||||||
Ok(b'B') => return Some(Key::Down),
|
Ok(b'B') => return Some(Key::Down),
|
||||||
@@ -134,19 +143,3 @@ pub fn restore_terminal() {
|
|||||||
stty(&[&s]);
|
stty(&[&s]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a URL in the default browser (xdg-open / open). Returns whether the spawn succeeded.
|
|
||||||
pub fn open_url(url: &str) -> bool {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
let opener = "open";
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
let opener = "xdg-open";
|
|
||||||
Command::new(opener)
|
|
||||||
.arg(url)
|
|
||||||
.stdin(Stdio::null())
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.spawn()
|
|
||||||
.map(|_| true)
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
@@ -39,16 +39,43 @@ struct ModuleEntry32W {
|
|||||||
extern "system" {
|
extern "system" {
|
||||||
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
|
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
|
||||||
fn CloseHandle(h: Handle) -> i32;
|
fn CloseHandle(h: Handle) -> i32;
|
||||||
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
|
fn VirtualAllocEx(
|
||||||
|
process: Handle,
|
||||||
|
address: *mut c_void,
|
||||||
|
size: usize,
|
||||||
|
alloc_type: u32,
|
||||||
|
protect: u32,
|
||||||
|
) -> *mut c_void;
|
||||||
fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
|
fn 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 WriteProcessMemory(
|
||||||
fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle;
|
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 WaitForSingleObject(h: Handle, ms: u32) -> u32;
|
||||||
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
|
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
|
||||||
fn GetModuleHandleW(name: *const u16) -> Handle;
|
fn GetModuleHandleW(name: *const u16) -> Handle;
|
||||||
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
|
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
|
||||||
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
|
fn IsWow64Process2(process: Handle, process_machine: *mut u16, native_machine: *mut u16)
|
||||||
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
|
-> i32;
|
||||||
|
fn GetCurrentProcess() -> Handle;
|
||||||
|
fn QueryFullProcessImageNameW(
|
||||||
|
process: Handle,
|
||||||
|
flags: u32,
|
||||||
|
buf: *mut u16,
|
||||||
|
size: *mut u32,
|
||||||
|
) -> i32;
|
||||||
fn GetLastError() -> u32;
|
fn GetLastError() -> u32;
|
||||||
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
|
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
|
||||||
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
||||||
@@ -66,11 +93,6 @@ extern "system" {
|
|||||||
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32;
|
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" {
|
extern "C" {
|
||||||
fn _kbhit() -> i32;
|
fn _kbhit() -> i32;
|
||||||
fn _getch() -> i32;
|
fn _getch() -> i32;
|
||||||
@@ -109,7 +131,10 @@ pub fn poll_key() -> Option<Key> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wide(path: &Path) -> Vec<u16> {
|
fn wide(path: &Path) -> Vec<u16> {
|
||||||
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
|
path.as_os_str()
|
||||||
|
.encode_wide()
|
||||||
|
.chain(std::iter::once(0))
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image_name(pid: u32) -> Option<String> {
|
fn image_name(pid: u32) -> Option<String> {
|
||||||
@@ -156,7 +181,9 @@ pub fn java_processes() -> Vec<JavaProcess> {
|
|||||||
if out.iter().any(|w| w.pid == pid) {
|
if out.iter().any(|w| w.pid == pid) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(path) = image_name(pid) else { continue };
|
let Some(path) = image_name(pid) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
||||||
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
||||||
out.push(JavaProcess { pid, title, exe });
|
out.push(JavaProcess { pid, title, exe });
|
||||||
@@ -181,7 +208,11 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
|
|||||||
let mut found = false;
|
let mut found = false;
|
||||||
let mut ok = Module32FirstW(snapshot, &mut entry);
|
let mut ok = Module32FirstW(snapshot, &mut entry);
|
||||||
while ok != 0 {
|
while ok != 0 {
|
||||||
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
|
let len = entry
|
||||||
|
.sz_module
|
||||||
|
.iter()
|
||||||
|
.position(|&c| c == 0)
|
||||||
|
.unwrap_or(entry.sz_module.len());
|
||||||
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
|
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
@@ -196,10 +227,16 @@ fn module_loaded(pid: u32, file_name: &str) -> bool {
|
|||||||
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
|
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
|
||||||
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
|
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
|
let access = PROCESS_CREATE_THREAD
|
||||||
|
| PROCESS_QUERY_INFORMATION
|
||||||
|
| PROCESS_VM_OPERATION
|
||||||
|
| PROCESS_VM_WRITE
|
||||||
|
| PROCESS_VM_READ;
|
||||||
let process = OpenProcess(access, 0, pid);
|
let process = OpenProcess(access, 0, pid);
|
||||||
if process.is_null() {
|
if process.is_null() {
|
||||||
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
|
return Err(last_error(
|
||||||
|
"OpenProcess (run as the same user, or as administrator)",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let result = inject_into(process, pid, dll);
|
let result = inject_into(process, pid, dll);
|
||||||
CloseHandle(process);
|
CloseHandle(process);
|
||||||
@@ -208,18 +245,50 @@ pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
|
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
|
||||||
let mut wow64 = 0;
|
unsafe fn machine(process: Handle) -> Result<u16, String> {
|
||||||
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
|
let mut emulated = 0;
|
||||||
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
|
let mut native = 0;
|
||||||
|
if IsWow64Process2(process, &mut emulated, &mut native) == 0 {
|
||||||
|
return Err(last_error("IsWow64Process2"));
|
||||||
|
}
|
||||||
|
Ok(if emulated == 0 { native } else { emulated })
|
||||||
}
|
}
|
||||||
let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
let target = machine(process)?;
|
||||||
|
let injector = machine(GetCurrentProcess())?;
|
||||||
|
if target != injector {
|
||||||
|
return Err(format!("JVM architecture {target:#06x} differs from injector {injector:#06x}. Use matching x64 or ARM64 builds."));
|
||||||
|
}
|
||||||
|
let image = std::fs::read(dll).map_err(|error| format!("read loader: {error}"))?;
|
||||||
|
let pe = image
|
||||||
|
.get(0x3c..0x40)
|
||||||
|
.and_then(|bytes| bytes.try_into().ok())
|
||||||
|
.map(u32::from_le_bytes)
|
||||||
|
.ok_or("invalid loader PE header")? as usize;
|
||||||
|
let library = image
|
||||||
|
.get(pe + 4..pe + 6)
|
||||||
|
.and_then(|bytes| bytes.try_into().ok())
|
||||||
|
.map(u16::from_le_bytes)
|
||||||
|
.ok_or("invalid loader machine")?;
|
||||||
|
if library != target {
|
||||||
|
return Err("The loader DLL and JVM architectures differ.".into());
|
||||||
|
}
|
||||||
|
let file_name = dll
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_default();
|
||||||
if module_loaded(pid, &file_name) {
|
if module_loaded(pid, &file_name) {
|
||||||
return Err(format!("{file_name} is already loaded in process {pid}"));
|
return Err(format!("{file_name} is already loaded in process {pid}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = wide(dll);
|
let path = wide(dll);
|
||||||
let bytes = path.len() * 2;
|
let bytes = path.len() * 2;
|
||||||
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
let remote = VirtualAllocEx(
|
||||||
|
process,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
bytes,
|
||||||
|
MEM_COMMIT | MEM_RESERVE,
|
||||||
|
PAGE_READWRITE,
|
||||||
|
);
|
||||||
if remote.is_null() {
|
if remote.is_null() {
|
||||||
return Err(last_error("VirtualAllocEx"));
|
return Err(last_error("VirtualAllocEx"));
|
||||||
}
|
}
|
||||||
@@ -227,7 +296,14 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
|
|||||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||||
Err(err)
|
Err(err)
|
||||||
};
|
};
|
||||||
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
|
if WriteProcessMemory(
|
||||||
|
process,
|
||||||
|
remote,
|
||||||
|
path.as_ptr() as *const c_void,
|
||||||
|
bytes,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
) == 0
|
||||||
|
{
|
||||||
return cleanup(last_error("WriteProcessMemory"));
|
return cleanup(last_error("WriteProcessMemory"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +313,15 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
|
|||||||
if load_library.is_null() {
|
if load_library.is_null() {
|
||||||
return cleanup(last_error("resolve LoadLibraryW"));
|
return cleanup(last_error("resolve LoadLibraryW"));
|
||||||
}
|
}
|
||||||
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
|
let thread = CreateRemoteThread(
|
||||||
|
process,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
0,
|
||||||
|
load_library,
|
||||||
|
remote,
|
||||||
|
0,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
if thread.is_null() {
|
if thread.is_null() {
|
||||||
return cleanup(last_error("CreateRemoteThread"));
|
return cleanup(last_error("CreateRemoteThread"));
|
||||||
}
|
}
|
||||||
@@ -260,11 +344,3 @@ unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), Strin
|
|||||||
}
|
}
|
||||||
Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)"))
|
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 | |||||||