Add project files (part 1/2)
Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
3721eaecb2
commit
73baf8d02d
274 files changed
+83593
No files matched your search
@@ -0,0 +1,39 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(slinky-inject LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
|
||||
endif()
|
||||
|
||||
add_executable(slinky-inject
|
||||
main.cpp
|
||||
tui.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
target_sources(slinky-inject PRIVATE inject_windows.cpp)
|
||||
target_compile_definitions(slinky-inject PRIVATE _WIN32_WINNT=0x0601)
|
||||
# A standalone .exe: no MinGW runtime DLLs to copy beside it.
|
||||
target_link_options(slinky-inject PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
else()
|
||||
target_sources(slinky-inject PRIVATE inject_linux.cpp)
|
||||
target_link_libraries(slinky-inject PRIVATE ${CMAKE_DL_LIBS} pthread)
|
||||
endif()
|
||||
|
||||
# Copy the client library next to the injector when it is built in the same
|
||||
# tree (the top-level CMakeLists.txt does this via add_subdirectory).
|
||||
# A separate always-run target: a POST_BUILD step on slinky-inject only ran when
|
||||
# the injector itself was relinked, so the sibling copy went stale whenever only
|
||||
# the client changed.
|
||||
if(TARGET slinky)
|
||||
add_custom_target(slinky-copy-beside-injector ALL
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:slinky>
|
||||
$<TARGET_FILE_DIR:slinky-inject>/$<TARGET_FILE_NAME:slinky>
|
||||
DEPENDS slinky slinky-inject
|
||||
VERBATIM)
|
||||
endif()
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Build slinky-inject for the host Linux.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build -j"$(nproc)"
|
||||
echo "built: $(pwd)/build/slinky-inject"
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
# Cross-build slinky-inject.exe with MinGW-w64.
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
cmake -S . -B build-windows \
|
||||
-DCMAKE_SYSTEM_NAME=Windows \
|
||||
-DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc \
|
||||
-DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build-windows -j"$(nproc)"
|
||||
echo "built: $(pwd)/build-windows/slinky-inject.exe"
|
||||
@@ -0,0 +1,35 @@
|
||||
// Process discovery and library injection, with no opinion about the UI.
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Injector {
|
||||
|
||||
struct Process {
|
||||
unsigned long pid = 0;
|
||||
std::string name;
|
||||
std::string command_line;
|
||||
};
|
||||
|
||||
using LogFn = std::function<void(const std::string &)>;
|
||||
|
||||
std::vector<Process> ListProcesses();
|
||||
|
||||
// Finds the running Minecraft JVM. On Linux that is a java process whose
|
||||
// command line names the game; on Windows it is the LWJGL/GLFW window's owner.
|
||||
bool FindMinecraft(Process &out, std::string &error);
|
||||
|
||||
// Finds a process by pid, exact name, or case-insensitive substring of the
|
||||
// name/command line. Accepts a decimal pid in `target`.
|
||||
bool FindTarget(const std::string &target, Process &out, std::string &error);
|
||||
|
||||
// Loads `library` into `pid` through ptrace+dlopen (Linux) or
|
||||
// CreateRemoteThread+LoadLibraryW (Windows). Returns false on failure.
|
||||
bool Inject(unsigned long pid, const std::string &library, const LogFn &log);
|
||||
|
||||
// Why injection may fail before it starts, for the error message.
|
||||
std::string EnvironmentHint();
|
||||
|
||||
} // namespace Injector
|
||||
@@ -0,0 +1,527 @@
|
||||
// Linux injection: ptrace the target and make it call its own dlopen().
|
||||
//
|
||||
// This is the same technique as the Catware injector: attach to the process,
|
||||
// save its registers, resolve dlopen in the target (preferring the target's own
|
||||
// libc ELF), point rip at it with the library path on a scratch stack, and use
|
||||
// a return address of 0 as the completion signal. The client library's ELF
|
||||
// constructor then starts the client, exactly as DllMain does on Windows.
|
||||
//
|
||||
// The library is injected from a private copy under a fresh name so that a
|
||||
// re-injection after Unload starts a new copy with fresh statics; the old
|
||||
// mapping is never unloaded (-z nodelete).
|
||||
#include "inject.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <dirent.h>
|
||||
#include <dlfcn.h>
|
||||
#include <elf.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/ptrace.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/user.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace Injector {
|
||||
namespace {
|
||||
|
||||
std::string lower(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string trim(std::string text) {
|
||||
while (!text.empty() && (text.back() == '\n' || text.back() == '\r' || text.back() == ' ')) text.pop_back();
|
||||
size_t start = text.find_first_not_of(" \t");
|
||||
return start == std::string::npos ? std::string() : text.substr(start);
|
||||
}
|
||||
|
||||
std::string read_small_file(const std::string &path) {
|
||||
std::ifstream in(path, std::ios::binary);
|
||||
if (!in) return {};
|
||||
std::ostringstream out;
|
||||
out << in.rdbuf();
|
||||
return out.str();
|
||||
}
|
||||
|
||||
std::string command_line_of(unsigned long pid) {
|
||||
std::string data = read_small_file("/proc/" + std::to_string(pid) + "/cmdline");
|
||||
for (char &c : data) {
|
||||
if (c == '\0') c = ' ';
|
||||
}
|
||||
return trim(data);
|
||||
}
|
||||
|
||||
std::string name_of(unsigned long pid) {
|
||||
return trim(read_small_file("/proc/" + std::to_string(pid) + "/comm"));
|
||||
}
|
||||
|
||||
// ---- ELF / maps helpers ----
|
||||
struct MappedRegion {
|
||||
uintptr_t start = 0;
|
||||
uintptr_t end = 0;
|
||||
uintptr_t offset = 0;
|
||||
std::string path;
|
||||
};
|
||||
|
||||
std::vector<MappedRegion> read_maps(unsigned long pid) {
|
||||
std::vector<MappedRegion> regions;
|
||||
std::ifstream maps("/proc/" + std::to_string(pid) + "/maps");
|
||||
std::string line;
|
||||
while (std::getline(maps, line)) {
|
||||
uintptr_t start = 0, end = 0, offset = 0;
|
||||
char path[512] = {0};
|
||||
int parsed = std::sscanf(line.c_str(), "%lx-%lx %*4s %lx %*x:%*x %*d %511[^\n]",
|
||||
&start, &end, &offset, path);
|
||||
if (parsed < 3) continue;
|
||||
MappedRegion region;
|
||||
region.start = start;
|
||||
region.end = end;
|
||||
region.offset = offset;
|
||||
region.path = parsed == 4 ? path : "";
|
||||
size_t first = region.path.find_first_not_of(' ');
|
||||
if (first == std::string::npos) region.path.clear();
|
||||
else region.path.erase(0, first);
|
||||
regions.push_back(region);
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> read_target_file(unsigned long pid, const std::string &path) {
|
||||
// The target's mount namespace first: in a Flatpak or other container the same path on the host is a
|
||||
// different libc build, and its symbol offsets would point into the wrong place.
|
||||
std::string through_root = "/proc/" + std::to_string(pid) + "/root" + path;
|
||||
for (const std::string &candidate : {through_root, path}) {
|
||||
std::ifstream in(candidate, std::ios::binary);
|
||||
if (!in) continue;
|
||||
return std::vector<uint8_t>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
uint64_t elf_function_value(const std::vector<uint8_t> &bytes, const char *name) {
|
||||
if (bytes.size() < sizeof(Elf64_Ehdr)) return 0;
|
||||
const auto *header = reinterpret_cast<const Elf64_Ehdr *>(bytes.data());
|
||||
if (std::memcmp(header->e_ident, ELFMAG, SELFMAG) != 0 ||
|
||||
header->e_ident[EI_CLASS] != ELFCLASS64 ||
|
||||
header->e_shoff == 0 || header->e_shnum == 0 ||
|
||||
header->e_shoff + (uint64_t)header->e_shnum * sizeof(Elf64_Shdr) > bytes.size()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto *sections = reinterpret_cast<const Elf64_Shdr *>(bytes.data() + header->e_shoff);
|
||||
for (uint32_t want : {(uint32_t)SHT_DYNSYM, (uint32_t)SHT_SYMTAB}) {
|
||||
for (int i = 0; i < header->e_shnum; i++) {
|
||||
const Elf64_Shdr §ion = sections[i];
|
||||
if (section.sh_type != want || section.sh_entsize == 0 || section.sh_link >= header->e_shnum ||
|
||||
section.sh_offset + section.sh_size > bytes.size()) {
|
||||
continue;
|
||||
}
|
||||
const auto *symbols = reinterpret_cast<const Elf64_Sym *>(bytes.data() + section.sh_offset);
|
||||
const char *names = reinterpret_cast<const char *>(bytes.data() + sections[section.sh_link].sh_offset);
|
||||
size_t count = section.sh_size / section.sh_entsize;
|
||||
for (size_t s = 0; s < count; s++) {
|
||||
if (ELF64_ST_TYPE(symbols[s].st_info) != STT_FUNC || symbols[s].st_value == 0) continue;
|
||||
if (std::strcmp(names + symbols[s].st_name, name) == 0) return symbols[s].st_value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<uintptr_t> resolve_dlopen_from_elf(unsigned long pid, const std::vector<MappedRegion> ®ions) {
|
||||
for (const MappedRegion &header : regions) {
|
||||
if (header.offset != 0) continue;
|
||||
bool is_libc = header.path.find("libc.so.6") != std::string::npos ||
|
||||
header.path.find("libdl.so.2") != std::string::npos;
|
||||
if (!is_libc) continue;
|
||||
|
||||
uint64_t value = elf_function_value(read_target_file(pid, header.path), "dlopen");
|
||||
if (!value) continue;
|
||||
|
||||
for (const MappedRegion &segment : regions) {
|
||||
if (segment.path == header.path && value >= segment.offset &&
|
||||
value < segment.offset + (segment.end - segment.start)) {
|
||||
return segment.start + (uintptr_t)(value - segment.offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uintptr_t> resolve_remote_dlopen(unsigned long pid, const LogFn &log) {
|
||||
void *local_dlopen = dlsym(RTLD_DEFAULT, "dlopen");
|
||||
if (!local_dlopen) {
|
||||
log(std::string("dlsym(dlopen) failed: ") + dlerror());
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto local_regions = read_maps(getpid());
|
||||
const MappedRegion *owner = nullptr;
|
||||
for (const auto ®ion : local_regions) {
|
||||
if ((uintptr_t)local_dlopen >= region.start && (uintptr_t)local_dlopen < region.end &&
|
||||
!region.path.empty() && region.path[0] == '/') {
|
||||
owner = ®ion;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!owner) {
|
||||
log("could not find dlopen in the injector's own maps");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto remote_regions = read_maps(pid);
|
||||
|
||||
// The target's own libc first. A Flatpak/container runtime is a different
|
||||
// build at the same path, where our offset would land in the wrong place.
|
||||
if (auto from_elf = resolve_dlopen_from_elf(pid, remote_regions)) return from_elf;
|
||||
|
||||
// Otherwise match the same file and the same segment offset.
|
||||
uintptr_t within = (uintptr_t)local_dlopen - owner->start;
|
||||
for (const auto ®ion : remote_regions) {
|
||||
if (region.path == owner->path && region.offset == owner->offset) {
|
||||
return region.start + within;
|
||||
}
|
||||
}
|
||||
|
||||
log("could not resolve dlopen in the target (no matching libc segment)");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// ---- ptrace plumbing ----
|
||||
bool wait_stopped(pid_t pid, int &status) {
|
||||
for (;;) {
|
||||
pid_t r = waitpid(pid, &status, 0);
|
||||
if (r == pid) return true;
|
||||
if (r < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool write_remote(pid_t pid, uintptr_t address, const void *data, size_t size) {
|
||||
iovec local{const_cast<void *>(data), size};
|
||||
iovec remote{reinterpret_cast<void *>(address), size};
|
||||
if (process_vm_writev(pid, &local, 1, &remote, 1, 0) == (ssize_t)size) return true;
|
||||
|
||||
// Fallback for kernels without process_vm_writev.
|
||||
const uint8_t *bytes = static_cast<const uint8_t *>(data);
|
||||
for (size_t done = 0; done < size;) {
|
||||
long word = 0;
|
||||
size_t n = std::min(sizeof(word), size - done);
|
||||
std::memcpy(&word, bytes + done, n);
|
||||
if (ptrace(PTRACE_POKEDATA, pid, reinterpret_cast<void *>(address + done), reinterpret_cast<void *>(word)) != 0) {
|
||||
return false;
|
||||
}
|
||||
done += n;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool inject_path(pid_t pid, const std::string &path, const LogFn &log) {
|
||||
if (ptrace(PTRACE_ATTACH, pid, nullptr, nullptr) != 0) {
|
||||
log(std::string("PTRACE_ATTACH failed: ") + std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
if (!wait_stopped(pid, status)) {
|
||||
log("waitpid after attach failed");
|
||||
ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
user_regs_struct saved{};
|
||||
if (ptrace(PTRACE_GETREGS, pid, nullptr, &saved) != 0) {
|
||||
log(std::string("PTRACE_GETREGS failed: ") + std::strerror(errno));
|
||||
ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto remote_dlopen = resolve_remote_dlopen(pid, log);
|
||||
if (!remote_dlopen) {
|
||||
ptrace(PTRACE_SETREGS, pid, nullptr, &saved);
|
||||
ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The stack grows down from rsp. The path must sit above the call stack,
|
||||
// or dlopen overwrites its own filename argument while loading.
|
||||
constexpr size_t scratch_offset = 256;
|
||||
constexpr size_t call_stack_offset = 16384;
|
||||
uintptr_t scratch = (saved.rsp - scratch_offset) & ~(uintptr_t)0xF;
|
||||
uintptr_t call_rsp = (saved.rsp - call_stack_offset) & ~(uintptr_t)0xF;
|
||||
|
||||
std::string padded = path;
|
||||
padded.resize(((padded.size() / 8) + 2) * 8, '\0');
|
||||
if (!write_remote(pid, scratch, padded.data(), padded.size())) {
|
||||
log(std::string("failed to write library path into target: ") + std::strerror(errno));
|
||||
ptrace(PTRACE_SETREGS, pid, nullptr, &saved);
|
||||
ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
|
||||
return false;
|
||||
}
|
||||
|
||||
// A return address of 0 makes dlopen's `ret` fault at rip == 0. That
|
||||
// private fault is the completion signal; no shared code is patched, so
|
||||
// the other JVM threads can keep running untouched.
|
||||
constexpr uintptr_t return_address = 0;
|
||||
user_regs_struct call = saved;
|
||||
call.rdi = scratch;
|
||||
call.rsi = RTLD_NOW;
|
||||
call.rip = *remote_dlopen;
|
||||
call.orig_rax = (unsigned long long)-1; // no syscall to restart
|
||||
call.rsp = call_rsp - 8;
|
||||
|
||||
auto restore = [&]() {
|
||||
ptrace(PTRACE_SETREGS, pid, nullptr, &saved);
|
||||
ptrace(PTRACE_DETACH, pid, nullptr, nullptr);
|
||||
};
|
||||
|
||||
if (ptrace(PTRACE_POKEDATA, pid, reinterpret_cast<void *>(call.rsp),
|
||||
reinterpret_cast<void *>(return_address)) != 0) {
|
||||
log(std::string("failed to write completion address: ") + std::strerror(errno));
|
||||
restore();
|
||||
return false;
|
||||
}
|
||||
if (ptrace(PTRACE_SETREGS, pid, nullptr, &call) != 0) {
|
||||
log(std::string("PTRACE_SETREGS failed: ") + std::strerror(errno));
|
||||
restore();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
int pending_signal = 0;
|
||||
user_regs_struct fault{};
|
||||
|
||||
for (int attempt = 0; attempt < (1 << 20); attempt++) {
|
||||
if (ptrace(PTRACE_CONT, pid, nullptr, reinterpret_cast<void *>((uintptr_t)pending_signal)) != 0) {
|
||||
log(std::string("PTRACE_CONT failed: ") + std::strerror(errno));
|
||||
restore();
|
||||
return false;
|
||||
}
|
||||
pending_signal = 0;
|
||||
|
||||
if (!wait_stopped(pid, status)) {
|
||||
log("target vanished while injecting");
|
||||
return false;
|
||||
}
|
||||
if (!WIFSTOPPED(status)) {
|
||||
log("target exited while injecting");
|
||||
return false;
|
||||
}
|
||||
|
||||
int signal = WSTOPSIG(status);
|
||||
if (signal == SIGSEGV) {
|
||||
ptrace(PTRACE_GETREGS, pid, nullptr, &fault);
|
||||
if (fault.rip == return_address) {
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
pending_signal = SIGSEGV; // HotSpot's own fault: deliver it
|
||||
continue;
|
||||
}
|
||||
pending_signal = signal; // timers, SIGQUIT, ...: the target's own
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
log("dlopen returned handle 0x" + [&] {
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof buf, "%llx", (unsigned long long)fault.rax);
|
||||
return std::string(buf);
|
||||
}());
|
||||
if (fault.rax == 0) {
|
||||
ok = false;
|
||||
log("dlopen returned NULL -- the library failed to load in the target");
|
||||
}
|
||||
} else {
|
||||
log("dlopen did not return through the completion address");
|
||||
}
|
||||
|
||||
if (ptrace(PTRACE_SETREGS, pid, nullptr, &saved) != 0) {
|
||||
log(std::string("warning: failed to restore registers: ") + std::strerror(errno));
|
||||
}
|
||||
if (ptrace(PTRACE_DETACH, pid, nullptr, nullptr) != 0) {
|
||||
log(std::string("warning: PTRACE_DETACH failed: ") + std::strerror(errno));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// The copy is written through /proc/<pid>/root/tmp so that a target with its own /tmp (PrivateTmp, Flatpak,
|
||||
// pressure-vessel) can open it; `host_path` is where the injector sees it, `target_path` what dlopen gets.
|
||||
bool private_copy(const std::string &library, unsigned long pid, std::string &host_path, std::string &target_path,
|
||||
const LogFn &log) {
|
||||
char source[PATH_MAX];
|
||||
if (!realpath(library.c_str(), source)) {
|
||||
log(std::string("cannot resolve ") + library + ": " + std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ifstream in(source, std::ios::binary);
|
||||
if (!in) {
|
||||
log(std::string("cannot open ") + source);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string pattern = "/proc/" + std::to_string(pid) + "/root/tmp/slinky-inject-XXXXXX";
|
||||
int fd = mkstemp(pattern.data());
|
||||
if (fd >= 0) {
|
||||
target_path = pattern.substr(pattern.find("/tmp/"));
|
||||
} else {
|
||||
pattern = "/tmp/slinky-inject-XXXXXX";
|
||||
fd = mkstemp(pattern.data());
|
||||
target_path = pattern;
|
||||
}
|
||||
if (fd < 0) {
|
||||
log(std::string("cannot create a private library copy: ") + std::strerror(errno));
|
||||
return false;
|
||||
}
|
||||
fchmod(fd, 0644);
|
||||
|
||||
std::vector<char> buffer(64 * 1024);
|
||||
bool ok = true;
|
||||
while (in) {
|
||||
in.read(buffer.data(), (std::streamsize)buffer.size());
|
||||
std::streamsize got = in.gcount();
|
||||
for (std::streamsize done = 0; done < got;) {
|
||||
ssize_t n = write(fd, buffer.data() + done, (size_t)(got - done));
|
||||
if (n < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
done += n;
|
||||
}
|
||||
if (!ok) break;
|
||||
}
|
||||
if (close(fd) != 0) ok = false;
|
||||
if (!ok) {
|
||||
unlink(pattern.c_str());
|
||||
log("failed to write the private library copy");
|
||||
return false;
|
||||
}
|
||||
host_path = pattern;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<Process> ListProcesses() {
|
||||
std::vector<Process> out;
|
||||
DIR *proc = opendir("/proc");
|
||||
if (!proc) return out;
|
||||
|
||||
while (dirent *entry = readdir(proc)) {
|
||||
if (entry->d_name[0] < '0' || entry->d_name[0] > '9') continue;
|
||||
unsigned long pid = std::strtoul(entry->d_name, nullptr, 10);
|
||||
if (!pid) continue;
|
||||
std::string cmdline = command_line_of(pid);
|
||||
if (cmdline.empty()) continue; // kernel thread
|
||||
out.push_back(Process{pid, name_of(pid), cmdline});
|
||||
}
|
||||
closedir(proc);
|
||||
|
||||
std::sort(out.begin(), out.end(), [](const Process &a, const Process &b) {
|
||||
if (a.name != b.name) return a.name < b.name;
|
||||
return a.pid < b.pid;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
bool FindMinecraft(Process &out, std::string &error) {
|
||||
int best_score = 0;
|
||||
for (const Process &process : ListProcesses()) {
|
||||
std::string cmd = lower(process.command_line);
|
||||
std::string name = lower(process.name);
|
||||
bool java = name.find("java") != std::string::npos || cmd.find("/java ") != std::string::npos;
|
||||
if (!java) continue;
|
||||
|
||||
int score = 0;
|
||||
if (cmd.find("net.minecraft") != std::string::npos) score += 100;
|
||||
if (cmd.find("minecraft") != std::string::npos) score += 50;
|
||||
if (cmd.find("fabric") != std::string::npos) score += 10;
|
||||
if (cmd.find("forge") != std::string::npos) score += 10;
|
||||
if (cmd.find("--gamedir") != std::string::npos) score += 10;
|
||||
if (score > best_score) {
|
||||
best_score = score;
|
||||
out = process;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_score > 0) return true;
|
||||
error = "Minecraft is not running (no java process with minecraft in its command line)";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FindTarget(const std::string &target, Process &out, std::string &error) {
|
||||
if (target.empty()) {
|
||||
error = "no target given";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool numeric = std::all_of(target.begin(), target.end(), [](unsigned char c) { return std::isdigit(c); });
|
||||
if (numeric) {
|
||||
out.pid = std::strtoul(target.c_str(), nullptr, 10);
|
||||
out.name = name_of(out.pid);
|
||||
out.command_line = command_line_of(out.pid);
|
||||
if (out.pid && !out.name.empty()) return true;
|
||||
error = "no process with pid " + target;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string needle = lower(target);
|
||||
for (const Process &process : ListProcesses()) {
|
||||
if (lower(process.name) == needle) {
|
||||
out = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const Process &process : ListProcesses()) {
|
||||
if (lower(process.name).find(needle) != std::string::npos ||
|
||||
lower(process.command_line).find(needle) != std::string::npos) {
|
||||
out = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
error = "no process matching '" + target + "'";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Inject(unsigned long pid, const std::string &library, const LogFn &log) {
|
||||
std::string host_copy, target_copy;
|
||||
if (!private_copy(library, pid, host_copy, target_copy, log)) return false;
|
||||
|
||||
log("injecting " + library + " as " + target_copy + " into pid " + std::to_string(pid));
|
||||
bool ok = inject_path((pid_t)pid, target_copy, log);
|
||||
// The loaded mapping keeps the inode alive, so a later copy can never alias it.
|
||||
unlink(host_copy.c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::string EnvironmentHint() {
|
||||
std::ifstream scope_file("/proc/sys/kernel/yama/ptrace_scope");
|
||||
int scope = -1;
|
||||
if (scope_file >> scope && scope > 0 && geteuid() != 0) {
|
||||
return "kernel.yama.ptrace_scope is " + std::to_string(scope) +
|
||||
"; run the injector with sudo, or set it to 0 (sudo sysctl kernel.yama.ptrace_scope=0), "
|
||||
"or give the binary CAP_SYS_PTRACE (sudo setcap cap_sys_ptrace+ep <injector>)";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace Injector
|
||||
@@ -0,0 +1,213 @@
|
||||
// Windows injection: CreateRemoteThread(LoadLibraryW) into the game's process.
|
||||
// The client DLL's DllMain starts the client, so loading it is the whole job.
|
||||
#ifdef _WIN32
|
||||
|
||||
#include "inject.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
namespace Injector {
|
||||
namespace {
|
||||
|
||||
std::string narrow(const std::wstring &text) {
|
||||
if (text.empty()) return {};
|
||||
int n = WideCharToMultiByte(CP_UTF8, 0, text.data(), (int)text.size(), nullptr, 0, nullptr, nullptr);
|
||||
std::string out((size_t)n, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, text.data(), (int)text.size(), out.data(), n, nullptr, nullptr);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::wstring widen(const std::string &text) {
|
||||
if (text.empty()) return {};
|
||||
int n = MultiByteToWideChar(CP_UTF8, 0, text.data(), (int)text.size(), nullptr, 0);
|
||||
std::wstring out((size_t)n, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.data(), (int)text.size(), out.data(), n);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string lower(std::string text) {
|
||||
std::transform(text.begin(), text.end(), text.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string process_name(DWORD pid) {
|
||||
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!process) return {};
|
||||
wchar_t path[1024];
|
||||
DWORD size = (DWORD)(sizeof path / sizeof path[0]);
|
||||
std::string result;
|
||||
if (QueryFullProcessImageNameW(process, 0, path, &size)) {
|
||||
std::wstring full(path, size);
|
||||
size_t slash = full.find_last_of(L"\\/");
|
||||
result = narrow(slash == std::wstring::npos ? full : full.substr(slash + 1));
|
||||
}
|
||||
CloseHandle(process);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<Process> ListProcesses() {
|
||||
std::vector<Process> out;
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE) return out;
|
||||
|
||||
PROCESSENTRY32W entry{};
|
||||
entry.dwSize = sizeof entry;
|
||||
if (Process32FirstW(snapshot, &entry)) {
|
||||
do {
|
||||
out.push_back(Process{entry.th32ProcessID, narrow(entry.szExeFile), {}});
|
||||
} while (Process32NextW(snapshot, &entry));
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
|
||||
std::sort(out.begin(), out.end(), [](const Process &a, const Process &b) {
|
||||
if (a.name != b.name) return a.name < b.name;
|
||||
return a.pid < b.pid;
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
bool FindMinecraft(Process &out, std::string &error) {
|
||||
const wchar_t *classes[] = {L"LWJGL", L"GLFW30"};
|
||||
for (const wchar_t *cls : classes) {
|
||||
HWND window = FindWindowW(cls, nullptr);
|
||||
if (!window) continue;
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
if (!pid) continue;
|
||||
out.pid = pid;
|
||||
out.name = process_name(pid);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No window class match: fall back to the first javaw.exe/java.exe, which
|
||||
// is right when the game is still starting or the window class differs.
|
||||
for (const Process &process : ListProcesses()) {
|
||||
std::string name = lower(process.name);
|
||||
if (name == "javaw.exe" || name == "java.exe") {
|
||||
out = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
error = "Minecraft is not running (no LWJGL/GLFW30 window and no javaw.exe)";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FindTarget(const std::string &target, Process &out, std::string &error) {
|
||||
if (target.empty()) {
|
||||
error = "no target given";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool numeric = std::all_of(target.begin(), target.end(), [](unsigned char c) { return std::isdigit(c); });
|
||||
if (numeric) {
|
||||
out.pid = std::strtoul(target.c_str(), nullptr, 10);
|
||||
out.name = process_name((DWORD)out.pid);
|
||||
if (out.pid && !out.name.empty()) return true;
|
||||
error = "no process with pid " + target;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string needle = lower(target);
|
||||
auto all = ListProcesses();
|
||||
for (const Process &process : all) {
|
||||
if (lower(process.name) == needle) {
|
||||
out = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const Process &process : all) {
|
||||
if (lower(process.name).find(needle) != std::string::npos) {
|
||||
out = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
error = "no process matching '" + target + "'";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Inject(unsigned long pid, const std::string &library, const LogFn &log) {
|
||||
std::wstring wide_library = widen(library);
|
||||
|
||||
DWORD full_size = GetFullPathNameW(wide_library.c_str(), 0, nullptr, nullptr);
|
||||
if (full_size) {
|
||||
std::wstring full(full_size, L'\0');
|
||||
DWORD written = GetFullPathNameW(wide_library.c_str(), full_size, full.data(), nullptr);
|
||||
if (written && written < full_size) {
|
||||
full.resize(written);
|
||||
wide_library = full;
|
||||
}
|
||||
}
|
||||
|
||||
log("injecting " + narrow(wide_library) + " into pid " + std::to_string(pid));
|
||||
|
||||
HANDLE process = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION |
|
||||
PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||||
FALSE, (DWORD)pid);
|
||||
if (!process) {
|
||||
DWORD err = GetLastError();
|
||||
if (err == ERROR_ACCESS_DENIED) log("access denied -- try running the injector as administrator");
|
||||
else log("OpenProcess failed (" + std::to_string(err) + ")");
|
||||
return false;
|
||||
}
|
||||
|
||||
SIZE_T bytes = (wide_library.size() + 1) * sizeof(wchar_t);
|
||||
void *remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remote) {
|
||||
log("VirtualAllocEx failed (" + std::to_string(GetLastError()) + ")");
|
||||
CloseHandle(process);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
SIZE_T written = 0;
|
||||
if (!WriteProcessMemory(process, remote, wide_library.c_str(), bytes, &written)) {
|
||||
log("WriteProcessMemory failed (" + std::to_string(GetLastError()) + ")");
|
||||
} else {
|
||||
HMODULE kernel = GetModuleHandleW(L"kernel32.dll");
|
||||
auto load_library = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
||||
GetProcAddress(kernel, "LoadLibraryW"));
|
||||
if (!load_library) {
|
||||
log("could not find LoadLibraryW");
|
||||
} else {
|
||||
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr);
|
||||
if (!thread) {
|
||||
log("CreateRemoteThread failed (" + std::to_string(GetLastError()) + ")");
|
||||
} else {
|
||||
WaitForSingleObject(thread, INFINITE);
|
||||
DWORD exit_code = 0;
|
||||
if (GetExitCodeThread(thread, &exit_code)) {
|
||||
// The HMODULE return is truncated to a DWORD here; only
|
||||
// zero (failure) is meaningful.
|
||||
ok = exit_code != 0;
|
||||
if (!ok) log("LoadLibraryW returned NULL in the target");
|
||||
} else {
|
||||
log("GetExitCodeThread failed (" + std::to_string(GetLastError()) + ")");
|
||||
}
|
||||
CloseHandle(thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::string EnvironmentHint() {
|
||||
return "if injection fails with access denied, run the injector as administrator";
|
||||
}
|
||||
|
||||
} // namespace Injector
|
||||
|
||||
#endif // _WIN32
|
||||
@@ -0,0 +1,327 @@
|
||||
// slinky-inject: the loader's terminal UI and a cross-platform injector.
|
||||
//
|
||||
// slinky-inject find Minecraft, inject libslinky.so /
|
||||
// slinky.dll from beside the injector
|
||||
// slinky-inject <pid|name> same, but choose the target
|
||||
// slinky-inject <pid|name> <library> inject that library
|
||||
// slinky-inject --lib <library> choose the library only
|
||||
// slinky-inject --pid <pid> choose the target only
|
||||
// slinky-inject --list list visible processes
|
||||
//
|
||||
// Linux uses ptrace + the target's own dlopen; Windows uses
|
||||
// CreateRemoteThread(LoadLibraryW). The animation is the one from the Go
|
||||
// loader (animation/animation.go).
|
||||
#include "inject.hpp"
|
||||
#include "tui.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
struct Options {
|
||||
bool list = false;
|
||||
bool help = false;
|
||||
bool no_anim = false;
|
||||
bool pid_given = false;
|
||||
unsigned long pid = 0;
|
||||
std::string target;
|
||||
std::string library;
|
||||
};
|
||||
|
||||
void usage(const char *program) {
|
||||
std::cout <<
|
||||
"usage: " << program << " [options] [target] [library]\n"
|
||||
"\n"
|
||||
" target pid or process name (default: the running Minecraft)\n"
|
||||
" library path to libslinky.so / slinky.dll (default: beside this program)\n"
|
||||
"\n"
|
||||
"options:\n"
|
||||
" -p, --pid <pid> target a pid\n"
|
||||
" -n, --name <name> target a process name (substring match)\n"
|
||||
" -l, --lib <path> library to inject\n"
|
||||
" --list list processes and exit\n"
|
||||
" --no-anim plain output, no animation\n"
|
||||
" -h, --help this text\n";
|
||||
}
|
||||
|
||||
bool parse_options(int argc, char **argv, Options &options, std::string &error) {
|
||||
std::vector<std::string> positional;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg = argv[i];
|
||||
auto value = [&](const char *what) -> bool {
|
||||
if (i + 1 >= argc) {
|
||||
error = std::string(what) + " needs a value";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
options.help = true;
|
||||
} else if (arg == "--list") {
|
||||
options.list = true;
|
||||
} else if (arg == "--no-anim") {
|
||||
options.no_anim = true;
|
||||
} else if (arg == "-p" || arg == "--pid") {
|
||||
if (!value("--pid")) return false;
|
||||
char *end = nullptr;
|
||||
options.pid = std::strtoul(argv[++i], &end, 10);
|
||||
options.pid_given = true;
|
||||
if (!end || *end || !options.pid) {
|
||||
error = "invalid pid '" + std::string(argv[i]) + "'";
|
||||
return false;
|
||||
}
|
||||
} else if (arg == "-n" || arg == "--name") {
|
||||
if (!value("--name")) return false;
|
||||
options.target = argv[++i];
|
||||
} else if (arg == "-l" || arg == "--lib") {
|
||||
if (!value("--lib")) return false;
|
||||
options.library = argv[++i];
|
||||
} else if (!arg.empty() && arg[0] == '-') {
|
||||
error = "unknown option '" + arg + "'";
|
||||
return false;
|
||||
} else {
|
||||
positional.push_back(arg);
|
||||
}
|
||||
}
|
||||
|
||||
// One positional that names a library is a library, not a target.
|
||||
if (positional.size() == 1 && options.target.empty() && !options.pid_given) {
|
||||
std::string lower = positional[0];
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
if (lower.size() > 3 && (lower.substr(lower.size() - 3) == ".so" ||
|
||||
lower.substr(lower.size() - 4) == ".dll")) {
|
||||
options.library = positional[0];
|
||||
positional.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (positional.size() > 2) {
|
||||
error = "too many positional arguments";
|
||||
return false;
|
||||
}
|
||||
if (positional.size() >= 1 && options.target.empty() && !options.pid_given) options.target = positional[0];
|
||||
if (positional.size() >= 2 && options.library.empty()) options.library = positional[1];
|
||||
return true;
|
||||
}
|
||||
|
||||
std::filesystem::path executable_path() {
|
||||
#ifdef _WIN32
|
||||
wchar_t buffer[MAX_PATH];
|
||||
DWORD n = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
||||
return std::filesystem::path(std::wstring(buffer, n));
|
||||
#else
|
||||
char buffer[PATH_MAX];
|
||||
ssize_t n = readlink("/proc/self/exe", buffer, sizeof buffer - 1);
|
||||
if (n <= 0) return {};
|
||||
buffer[n] = 0;
|
||||
return std::filesystem::path(buffer);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string default_library() {
|
||||
#ifdef _WIN32
|
||||
const char *names[] = {"slinky.dll", "libslinky.dll"};
|
||||
const char *build[] = {"build/cmake/slinky.dll", "../build/cmake/slinky.dll"};
|
||||
#else
|
||||
const char *names[] = {"libslinky.so", "slinky.so"};
|
||||
const char *build[] = {"build/linux/libslinky.so", "../build/linux/libslinky.so"};
|
||||
#endif
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path exe = executable_path();
|
||||
std::vector<std::filesystem::path> candidates;
|
||||
|
||||
if (!exe.empty()) {
|
||||
for (const char *name : names) candidates.push_back(exe.parent_path() / name);
|
||||
}
|
||||
for (const char *name : names) candidates.push_back(std::filesystem::current_path(ec) / name);
|
||||
for (const char *path : build) candidates.push_back(std::filesystem::current_path(ec) / path);
|
||||
#ifndef _WIN32
|
||||
candidates.push_back("/usr/local/lib/libslinky.so");
|
||||
candidates.push_back("/usr/lib/libslinky.so");
|
||||
#endif
|
||||
|
||||
for (const auto &candidate : candidates) {
|
||||
if (std::filesystem::exists(candidate, ec)) {
|
||||
auto canonical = std::filesystem::weakly_canonical(candidate, ec);
|
||||
return ec ? candidate.string() : canonical.string();
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void sleep_ms(int ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
void render_for(Tui::Screen &screen, int milliseconds, const std::string &title, const std::string &message,
|
||||
bool error = false) {
|
||||
auto end = std::chrono::steady_clock::now() + std::chrono::milliseconds(milliseconds);
|
||||
do {
|
||||
screen.Render(title, message, error);
|
||||
sleep_ms(16);
|
||||
} while (std::chrono::steady_clock::now() < end);
|
||||
}
|
||||
|
||||
void print_list(const std::vector<Injector::Process> &processes) {
|
||||
std::cout << " PID NAME COMMAND\n";
|
||||
for (const auto &process : processes) {
|
||||
std::string command = process.command_line;
|
||||
if (command.size() > 100) command = command.substr(0, 97) + "...";
|
||||
std::cout << std::setw(6) << process.pid << " " << std::setw(20) << std::left << process.name << " "
|
||||
<< command << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
Options options;
|
||||
std::string error;
|
||||
if (!parse_options(argc, argv, options, error)) {
|
||||
std::cerr << "slinky-inject: " << error << "\n\n";
|
||||
usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (options.help) {
|
||||
usage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (options.list) {
|
||||
print_list(Injector::ListProcesses());
|
||||
return 0;
|
||||
}
|
||||
|
||||
const bool interactive = Tui::EnableAnsi() && Tui::StdoutIsTty() && !options.no_anim;
|
||||
Tui::Screen screen(interactive);
|
||||
|
||||
if (!options.library.empty()) {
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(options.library, ec)) {
|
||||
std::cerr << "slinky-inject: library not found: " << options.library << "\n";
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
options.library = default_library();
|
||||
if (options.library.empty()) {
|
||||
if (interactive) {
|
||||
screen.Render("ERROR",
|
||||
"Library not found. Put libslinky.so/slinky.dll beside the injector, "
|
||||
"or pass its path as the second argument.",
|
||||
true);
|
||||
sleep_ms(3000);
|
||||
} else {
|
||||
std::cerr << "slinky-inject: library not found (pass its path as the second argument)\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- find the target ----
|
||||
Injector::Process target;
|
||||
if (interactive) {
|
||||
render_for(screen, 450, "FINDING MINECRAFT", "Looking for the game process...");
|
||||
} else {
|
||||
std::cout << "Finding Minecraft...\n";
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
if (options.pid_given) {
|
||||
target.pid = options.pid;
|
||||
found = true;
|
||||
} else if (!options.target.empty()) {
|
||||
found = Injector::FindTarget(options.target, target, error);
|
||||
} else {
|
||||
found = Injector::FindMinecraft(target, error);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
std::string hint = Injector::EnvironmentHint();
|
||||
std::string message = error;
|
||||
if (!hint.empty()) message += "\n\n" + hint;
|
||||
if (interactive) {
|
||||
screen.Render("ERROR", message, true);
|
||||
sleep_ms(4000);
|
||||
} else {
|
||||
std::cerr << "slinky-inject: " << message << "\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- inject, with the animation still running ----
|
||||
std::string last_log = "Injecting into Minecraft...";
|
||||
std::mutex log_mutex;
|
||||
std::atomic<bool> done{false};
|
||||
std::atomic<bool> injected{false};
|
||||
|
||||
auto started = std::chrono::steady_clock::now();
|
||||
std::thread worker([&]() {
|
||||
injected.store(Injector::Inject(
|
||||
target.pid, options.library,
|
||||
[&](const std::string &line) {
|
||||
std::lock_guard<std::mutex> lock(log_mutex);
|
||||
last_log = line;
|
||||
}));
|
||||
done.store(true);
|
||||
});
|
||||
|
||||
if (interactive) {
|
||||
for (;;) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(log_mutex);
|
||||
screen.Render("INJECTING", last_log);
|
||||
}
|
||||
sleep_ms(16);
|
||||
bool finished = done.load();
|
||||
auto elapsed = std::chrono::steady_clock::now() - started;
|
||||
if (finished && elapsed > std::chrono::milliseconds(1200)) break;
|
||||
}
|
||||
}
|
||||
worker.join();
|
||||
|
||||
if (!injected.load()) {
|
||||
std::string hint = Injector::EnvironmentHint();
|
||||
std::string message = "Injection failed. " + last_log;
|
||||
if (!hint.empty()) message += "\n\n" + hint;
|
||||
if (interactive) {
|
||||
screen.Render("ERROR", message, true);
|
||||
sleep_ms(5000);
|
||||
} else {
|
||||
std::cerr << "slinky-inject: " << message << "\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- success countdown, exactly the Go loader's five seconds ----
|
||||
if (interactive) {
|
||||
for (int i = 5; i > 0; --i) {
|
||||
std::string title = "Injected successfully. Loader will close in " + std::to_string(i) + "s.";
|
||||
render_for(screen, 1000, title, "DONE");
|
||||
}
|
||||
} else {
|
||||
std::cout << "Injected successfully into pid " << target.pid << ".\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
#include "tui.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace Tui {
|
||||
namespace {
|
||||
|
||||
// Exact frames from the Go loader. The trailing spaces are part of the art:
|
||||
// they keep the right edge of the box aligned between frames.
|
||||
const char *kLogo[4] = {
|
||||
" _ \n"
|
||||
"(( ____ \n"
|
||||
" \\\\________________( ( e\\____o\n"
|
||||
" ( _____/ \n"
|
||||
" \\___________________/ \n"
|
||||
" / \\ \\ / ",
|
||||
|
||||
" _ \n"
|
||||
" (( ____ \n"
|
||||
" ||________________( ( e\\____o\n"
|
||||
" ( _____/ \n"
|
||||
" \\___________________/ \n"
|
||||
" ) | | ) ",
|
||||
|
||||
" _ \n"
|
||||
" // ____ \n"
|
||||
" //________________( ( e\\____o\n"
|
||||
" ( _____/ \n"
|
||||
" \\___________________/ \n"
|
||||
" \\ / / \\ ",
|
||||
|
||||
" _ \n"
|
||||
" (( ____ \n"
|
||||
" ||________________( ( e\\____o\n"
|
||||
" ( _____/ \n"
|
||||
" \\___________________/ \n"
|
||||
" | ) ) | ",
|
||||
};
|
||||
|
||||
const char *kErrorLogo =
|
||||
"\n"
|
||||
" ____ \n"
|
||||
" _________________( ( x\\____o\n"
|
||||
"// _____/ \n"
|
||||
"||____________________/ \n"
|
||||
" | | | | ";
|
||||
|
||||
const char *kOrange = "\x1b[38;5;208m";
|
||||
const char *kRed = "\x1b[91m";
|
||||
const char *kReset = "\x1b[0m";
|
||||
|
||||
std::string make_line() {
|
||||
std::random_device device;
|
||||
std::mt19937 rng(device());
|
||||
std::string line;
|
||||
line.reserve(25);
|
||||
for (int i = 0; i < 25; i++) line.push_back(rng() & 1 ? '_' : '-');
|
||||
return line;
|
||||
}
|
||||
|
||||
const std::string &base_line() {
|
||||
static const std::string line = make_line();
|
||||
return line;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool EnableAnsi() {
|
||||
#ifdef _WIN32
|
||||
HANDLE out = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
DWORD mode = 0;
|
||||
if (!out || out == INVALID_HANDLE_VALUE || !GetConsoleMode(out, &mode)) return false;
|
||||
return SetConsoleMode(out, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool StdoutIsTty() {
|
||||
#ifdef _WIN32
|
||||
return _isatty(_fileno(stdout)) != 0;
|
||||
#else
|
||||
return isatty(STDOUT_FILENO) != 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string Logo(int frame) {
|
||||
return kLogo[((frame % 4) + 4) % 4];
|
||||
}
|
||||
|
||||
std::string ErrorLogo() { return kErrorLogo; }
|
||||
|
||||
// The line is the base pattern shifted one character per frame, exactly as the
|
||||
// Go loader's init() builds its 30 frames.
|
||||
std::string SlinkyLine(int frame) {
|
||||
const std::string &base = base_line();
|
||||
if (base.empty()) return {};
|
||||
int shift = ((frame % (int)base.size()) + (int)base.size()) % (int)base.size();
|
||||
return base.substr(shift) + base.substr(0, shift);
|
||||
}
|
||||
|
||||
Screen::Screen(bool animate) : animate_(animate && StdoutIsTty()) {
|
||||
if (animate_) {
|
||||
std::fputs("\x1b[?1049h\x1b[?25l", stdout);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
Screen::~Screen() {
|
||||
if (animate_) {
|
||||
std::fputs("\x1b[?25h\x1b[?1049l", stdout);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
}
|
||||
|
||||
void Screen::Render(const std::string &title, const std::string &message, bool error) {
|
||||
if (!animate_) {
|
||||
// Non-interactive output: one line per state change is enough.
|
||||
std::fputs(error ? "ERROR: " : "", stdout);
|
||||
std::fputs(title.c_str(), stdout);
|
||||
if (!message.empty()) {
|
||||
std::fputs(" - ", stdout);
|
||||
std::fputs(message.c_str(), stdout);
|
||||
}
|
||||
std::fputc('\n', stdout);
|
||||
std::fflush(stdout);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the whole frame first. Clearing the screen before every 16 ms redraw made the
|
||||
// console blank between frames (the flicker); instead the cursor goes home and each line is
|
||||
// overwritten in one write, with erase-to-end-of-line cleaning up shorter content.
|
||||
const int frame_index = Frame();
|
||||
|
||||
std::string body;
|
||||
body.reserve(1024);
|
||||
if (error) {
|
||||
body += kRed;
|
||||
body += kErrorLogo;
|
||||
} else {
|
||||
body += kLogo[frame_index % 4];
|
||||
}
|
||||
body += '\n';
|
||||
body += SlinkyLine(frame_index);
|
||||
body += "\n\n> ";
|
||||
body += (error ? kRed : kOrange);
|
||||
body += title;
|
||||
body += kReset;
|
||||
body += "\n\n";
|
||||
body += message;
|
||||
body += '\n';
|
||||
|
||||
// Nothing changed since the last call: leave the terminal alone.
|
||||
if (body == last_body_) return;
|
||||
last_body_ = body;
|
||||
|
||||
std::string out;
|
||||
out.reserve(body.size() + 64);
|
||||
// Synchronized output: supporting terminals swap the completed frame atomically.
|
||||
out += "\x1b[?2026h";
|
||||
out += "\x1b[H";
|
||||
for (char c : body) {
|
||||
if (c == '\n') {
|
||||
out += "\x1b[K\n";
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
out += "\x1b[K\x1b[J"; // clear the rest of the last line and anything below the frame
|
||||
out += "\x1b[?2026l";
|
||||
|
||||
std::fwrite(out.data(), 1, out.size(), stdout);
|
||||
std::fflush(stdout);
|
||||
}
|
||||
|
||||
int Screen::Frame() const {
|
||||
auto elapsed = std::chrono::steady_clock::now() - start_;
|
||||
return (int)(std::chrono::duration_cast<std::chrono::milliseconds>(elapsed).count() / kFrameMs);
|
||||
}
|
||||
|
||||
} // namespace Tui
|
||||
@@ -0,0 +1,40 @@
|
||||
// The loader's terminal UI: the four-frame Slinky logo and the sliding
|
||||
// underscore line, ported from the Go loader (animation/animation.go) so the
|
||||
// injector looks and moves exactly like it.
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
namespace Tui {
|
||||
|
||||
bool EnableAnsi(); // Windows: turn on VT processing; Linux: no-op
|
||||
bool StdoutIsTty();
|
||||
|
||||
std::string Logo(int frame);
|
||||
std::string ErrorLogo();
|
||||
std::string SlinkyLine(int frame);
|
||||
|
||||
class Screen {
|
||||
public:
|
||||
explicit Screen(bool animate);
|
||||
~Screen();
|
||||
|
||||
Screen(const Screen&) = delete;
|
||||
Screen& operator=(const Screen&) = delete;
|
||||
|
||||
void Render(const std::string& title, const std::string& message, bool error = false);
|
||||
private:
|
||||
// The animation advances with time, not with redraws: the caller redraws every ~16 ms so status
|
||||
// messages show up at once, which made a frame-per-redraw animation run at 60 fps.
|
||||
static constexpr int kFrameMs = 100;
|
||||
int Frame() const;
|
||||
|
||||
bool animate_;
|
||||
std::chrono::steady_clock::time_point start_ = std::chrono::steady_clock::now();
|
||||
// Last rendered content: the caller redraws at 60 Hz, but the frame only changes every
|
||||
// kFrameMs (or on a status change), so identical redraws are skipped.
|
||||
std::string last_body_;
|
||||
};
|
||||
|
||||
} // namespace Tui
|
||||
Reference in new issue
Block a user