214 lines
7.2 KiB
C++
214 lines
7.2 KiB
C++
// 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
|