feat(loader): manual-map injection, static CRT, filterable UI
Loader no longer touches disk: drop the LoadLibraryW path that
extracted OpenZen.dll to %TEMP%\OpenZenLoader and replace it with
an in-process PE loader that maps the embedded DLL straight into
the target Java process.
native/loader (new manual_map.cpp/.h):
- VirtualAllocEx in the remote process for SizeOfImage,
locally apply relocations + IAT patching (kernel32 etc. are
KnownDLLs so local GetProcAddress addresses are valid in the
remote), WriteProcessMemory the finished image once,
VirtualProtectEx to match section characteristics, then run a
minimal x64 trampoline shellcode that calls DllMain with
DLL_PROCESS_ATTACH using the proper ABI (shadow space,
16-byte stack alignment).
- embedded_dll.cpp now hands back a pointer into the .rsrc
section instead of writing the DLL out. injector.cpp shrinks
to a 14-line shim around inject_in_memory.
native (top-level CMakeLists.txt):
- Force /MT (CMAKE_MSVC_RUNTIME_LIBRARY = MultiThreaded) for
both the DLL and the loader EXE. Without this the manual
mapper's local-address import resolution trips over
vcruntime140 / ucrtbase, which are not KnownDLLs and may have
different bases in the target process - leading to NULL
derefs inside msvcp140 after injection. With /MT the only
import surface left is KERNEL32.
native/dll (jar_extract.cpp):
- Replace FindResource/LoadResource/SizeofResource with a
hand-rolled PE resource directory walker. FindResource paths
through LdrFindResource_U, which depends on the PEB.Ldr table
that manual-mapped modules are not part of, so the standard
API returns NULL for the embedded zen.jar after injection.
native/loader UI:
- Drop the now-pointless DLL Path edit + Browse button.
- Add per-process "Window Class" column harvested via
GetClassNameW alongside the existing window title.
- Add a Minecraft Only checkbox (default on): show only rows
whose window title startsWith "Minecraft" and does not
contain "Launcher", filtering HMCL / MultiMC / generic Java
apps out of the picker.
build.gradle:
- Add cleanNative (Delete) hooked into clean so wiping the
project also removes native/build/ and the staged
native/zen.jar (CMake would otherwise hold onto the previous
configure).
gradle.properties:
- Fill in the real mod metadata (id=hey, name=OpenZen,
authors=Shirona1337, group=io.github.openzen, description).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
# CMP0091 NEW lets us drive the MSVC runtime via MSVC_RUNTIME_LIBRARY
|
||||
# instead of patching CMAKE_CXX_FLAGS_*. CMake 3.15+ default, set
|
||||
# explicitly so the build stays predictable across CMake versions.
|
||||
cmake_policy(SET CMP0091 NEW)
|
||||
project(OpenZenNative LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# Statically link the MSVC C/C++ runtime into both the loader EXE and the
|
||||
# injected DLL. This matters most for the DLL: our manual mapper resolves
|
||||
# import addresses locally and assumes the imported DLL has the same base
|
||||
# in both processes - true for kernel32/user32/ntdll (KnownDLLs) but NOT
|
||||
# for vcruntime140/ucrtbase, so the mapped DLL would otherwise call CRT
|
||||
# functions through wild pointers and crash the target Java process. /MT
|
||||
# eliminates those imports entirely.
|
||||
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||
|
||||
if(NOT DEFINED ENV{JAVA_HOME})
|
||||
message(FATAL_ERROR "JAVA_HOME must be set so we can locate jni.h / jvmti.h")
|
||||
endif()
|
||||
|
||||
@@ -3,25 +3,69 @@
|
||||
|
||||
namespace openzen::jar {
|
||||
|
||||
namespace {
|
||||
|
||||
// Walk the PE resource directory tree of an in-memory image to find an
|
||||
// RT_RCDATA entry by integer ID, without using FindResource / LoadResource.
|
||||
// We avoid the Win32 resource APIs here because the DLL may have been
|
||||
// manual-mapped: it is not in the loader's module list, so FindResource's
|
||||
// internal LdrFindResource_U call against `gSelfModule` cannot find a
|
||||
// matching LDR_DATA_TABLE_ENTRY and bails out.
|
||||
const void* find_rcdata(HMODULE module_base, WORD id, DWORD& out_size) {
|
||||
out_size = 0;
|
||||
auto base = reinterpret_cast<BYTE*>(module_base);
|
||||
auto dos = reinterpret_cast<PIMAGE_DOS_HEADER>(base);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return nullptr;
|
||||
auto nt = reinterpret_cast<PIMAGE_NT_HEADERS>(base + dos->e_lfanew);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE) return nullptr;
|
||||
|
||||
const auto& res_dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE];
|
||||
if (res_dir.Size == 0) return nullptr;
|
||||
BYTE* root_base = base + res_dir.VirtualAddress;
|
||||
|
||||
auto find_id_child = [&](PIMAGE_RESOURCE_DIRECTORY dir, WORD wanted_id)
|
||||
-> PIMAGE_RESOURCE_DIRECTORY_ENTRY {
|
||||
auto entry = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY_ENTRY>(dir + 1);
|
||||
WORD total = dir->NumberOfNamedEntries + dir->NumberOfIdEntries;
|
||||
// ID entries follow the named entries.
|
||||
for (WORD i = dir->NumberOfNamedEntries; i < total; ++i) {
|
||||
auto e = entry + i;
|
||||
if (!e->NameIsString && e->Id == wanted_id) return e;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
// Level 1: resource type (RT_RCDATA = 10).
|
||||
auto root = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(root_base);
|
||||
auto type_entry = find_id_child(root, 10);
|
||||
if (!type_entry || !type_entry->DataIsDirectory) return nullptr;
|
||||
|
||||
// Level 2: resource name (our integer id).
|
||||
auto name_dir = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(
|
||||
root_base + type_entry->OffsetToDirectory);
|
||||
auto name_entry = find_id_child(name_dir, id);
|
||||
if (!name_entry || !name_entry->DataIsDirectory) return nullptr;
|
||||
|
||||
// Level 3: language. Take the first available.
|
||||
auto lang_dir = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY>(
|
||||
root_base + name_entry->OffsetToDirectory);
|
||||
WORD lang_count = lang_dir->NumberOfNamedEntries + lang_dir->NumberOfIdEntries;
|
||||
if (lang_count == 0) return nullptr;
|
||||
auto lang_entry = reinterpret_cast<PIMAGE_RESOURCE_DIRECTORY_ENTRY>(lang_dir + 1);
|
||||
auto data_entry = reinterpret_cast<PIMAGE_RESOURCE_DATA_ENTRY>(
|
||||
root_base + lang_entry->OffsetToData);
|
||||
|
||||
out_size = data_entry->Size;
|
||||
return base + data_entry->OffsetToData;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool extract_embedded(std::wstring& out_path) {
|
||||
HRSRC info = FindResourceW(g_self_module, MAKEINTRESOURCEW(IDR_ZEN_JAR), RT_RCDATA);
|
||||
if (!info) {
|
||||
log::error("FindResource(IDR_ZEN_JAR) failed: %lu", GetLastError());
|
||||
return false;
|
||||
}
|
||||
DWORD size = SizeofResource(g_self_module, info);
|
||||
if (size == 0) {
|
||||
log::error("Embedded zen.jar resource is empty");
|
||||
return false;
|
||||
}
|
||||
HGLOBAL loaded = LoadResource(g_self_module, info);
|
||||
if (!loaded) {
|
||||
log::error("LoadResource failed: %lu", GetLastError());
|
||||
return false;
|
||||
}
|
||||
void* data = LockResource(loaded);
|
||||
if (!data) {
|
||||
log::error("LockResource failed");
|
||||
DWORD size = 0;
|
||||
const void* data = find_rcdata(g_self_module, IDR_ZEN_JAR, size);
|
||||
if (!data || size == 0) {
|
||||
log::error("PE resource lookup for IDR_ZEN_JAR (RT_RCDATA) failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ add_executable(OpenZenLoader WIN32
|
||||
src/main.cpp
|
||||
src/process_list.cpp
|
||||
src/injector.cpp
|
||||
src/manual_map.cpp
|
||||
src/embedded_dll.cpp
|
||||
src/window_title.cpp
|
||||
src/ui.cpp
|
||||
|
||||
@@ -3,37 +3,19 @@
|
||||
|
||||
namespace loader {
|
||||
|
||||
std::wstring extract_embedded_dll() {
|
||||
bool get_embedded_dll(const void*& out_data, size_t& out_size) {
|
||||
HMODULE self = GetModuleHandleW(nullptr);
|
||||
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA);
|
||||
if (!info) return L"";
|
||||
if (!info) return false;
|
||||
DWORD size = SizeofResource(self, info);
|
||||
if (size == 0) return L"";
|
||||
if (size == 0) return false;
|
||||
HGLOBAL loaded = LoadResource(self, info);
|
||||
if (!loaded) return L"";
|
||||
if (!loaded) return false;
|
||||
void* data = LockResource(loaded);
|
||||
if (!data) return L"";
|
||||
|
||||
wchar_t tmp[MAX_PATH];
|
||||
if (GetTempPathW(MAX_PATH, tmp) == 0) return L"";
|
||||
|
||||
wchar_t dir[MAX_PATH];
|
||||
std::swprintf(dir, MAX_PATH, L"%sOpenZenLoader", tmp);
|
||||
CreateDirectoryW(dir, nullptr);
|
||||
|
||||
wchar_t path[MAX_PATH];
|
||||
std::swprintf(path, MAX_PATH, L"%s\\OpenZen.dll", dir);
|
||||
|
||||
HANDLE file = CreateFileW(path, GENERIC_WRITE, 0, nullptr, CREATE_ALWAYS,
|
||||
FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE) return L"";
|
||||
|
||||
DWORD written = 0;
|
||||
BOOL ok = WriteFile(file, data, size, &written, nullptr);
|
||||
CloseHandle(file);
|
||||
if (!ok || written != size) return L"";
|
||||
|
||||
return std::wstring(path);
|
||||
if (!data) return false;
|
||||
out_data = data;
|
||||
out_size = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
|
||||
@@ -1,88 +1,15 @@
|
||||
#include "loader.h"
|
||||
|
||||
#include <shlwapi.h>
|
||||
|
||||
#include <sstream>
|
||||
#include "manual_map.h"
|
||||
|
||||
namespace loader {
|
||||
|
||||
namespace {
|
||||
std::wstring format_error(const wchar_t* where, DWORD err) {
|
||||
wchar_t msg[512] = {0};
|
||||
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, err, 0, msg, 512, nullptr);
|
||||
std::wstringstream ss;
|
||||
ss << where << L" failed (code " << err << L"): " << msg;
|
||||
return ss.str();
|
||||
std::wstring inject(DWORD pid) {
|
||||
const void* dll_data = nullptr;
|
||||
size_t dll_size = 0;
|
||||
if (!get_embedded_dll(dll_data, dll_size)) {
|
||||
return L"Embedded OpenZen.dll resource not found in loader EXE";
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring inject(DWORD pid, const std::wstring& dll_path) {
|
||||
if (!PathFileExistsW(dll_path.c_str())) {
|
||||
return L"DLL not found: " + dll_path;
|
||||
}
|
||||
|
||||
HANDLE process = OpenProcess(
|
||||
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
|
||||
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||||
FALSE, pid);
|
||||
if (!process) return format_error(L"OpenProcess", GetLastError());
|
||||
|
||||
SIZE_T payload_bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||
LPVOID remote = VirtualAllocEx(process, nullptr, payload_bytes,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!remote) {
|
||||
DWORD err = GetLastError();
|
||||
CloseHandle(process);
|
||||
return format_error(L"VirtualAllocEx", err);
|
||||
}
|
||||
|
||||
SIZE_T written = 0;
|
||||
if (!WriteProcessMemory(process, remote, dll_path.c_str(), payload_bytes, &written)
|
||||
|| written != payload_bytes) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return format_error(L"WriteProcessMemory", err);
|
||||
}
|
||||
|
||||
HMODULE kernel32 = GetModuleHandleW(L"kernel32.dll");
|
||||
if (!kernel32) {
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return L"kernel32.dll handle missing in loader process";
|
||||
}
|
||||
LPTHREAD_START_ROUTINE loadLib =
|
||||
(LPTHREAD_START_ROUTINE)GetProcAddress(kernel32, "LoadLibraryW");
|
||||
if (!loadLib) {
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return L"LoadLibraryW not exported by kernel32";
|
||||
}
|
||||
|
||||
HANDLE remote_thread = CreateRemoteThread(process, nullptr, 0, loadLib,
|
||||
remote, 0, nullptr);
|
||||
if (!remote_thread) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return format_error(L"CreateRemoteThread", err);
|
||||
}
|
||||
|
||||
WaitForSingleObject(remote_thread, 10000);
|
||||
|
||||
DWORD exit_code = 0;
|
||||
GetExitCodeThread(remote_thread, &exit_code);
|
||||
|
||||
CloseHandle(remote_thread);
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
|
||||
if (exit_code == 0) {
|
||||
return L"LoadLibraryW remote returned NULL (DLL failed to load - "
|
||||
L"check %TEMP%\\openzen.log)";
|
||||
}
|
||||
return L"";
|
||||
return inject_in_memory(pid, dll_data, dll_size);
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
|
||||
@@ -11,22 +11,31 @@ struct JavaProcess {
|
||||
std::wstring image_name;
|
||||
std::wstring command_line;
|
||||
std::wstring window_title;
|
||||
std::wstring window_class;
|
||||
};
|
||||
|
||||
struct WindowInfo {
|
||||
std::wstring title;
|
||||
std::wstring class_name;
|
||||
};
|
||||
|
||||
// Enumerate processes whose image is javaw.exe / java.exe.
|
||||
std::vector<JavaProcess> list_java_processes();
|
||||
|
||||
// Inject the given DLL into the target process via CreateRemoteThread +
|
||||
// LoadLibraryW. Returns an empty string on success or a human-readable error.
|
||||
std::wstring inject(DWORD pid, const std::wstring& dll_path);
|
||||
// Map the embedded OpenZen.dll directly into the target process and run its
|
||||
// DllMain via shellcode. The DLL bytes never touch disk. Returns an empty
|
||||
// string on success or a human-readable error message.
|
||||
std::wstring inject(DWORD pid);
|
||||
|
||||
// Extract the IDR_OPENZEN_DLL resource baked into the loader EXE to a
|
||||
// temp file and return its absolute path. Empty string on failure.
|
||||
std::wstring extract_embedded_dll();
|
||||
// Return a pointer into the loader EXE's resource section that holds the
|
||||
// embedded OpenZen.dll along with its byte size. The pointer remains valid
|
||||
// for the lifetime of the loader process.
|
||||
bool get_embedded_dll(const void*& out_data, size_t& out_size);
|
||||
|
||||
// Walk top-level windows and return the most informative title belonging to
|
||||
// the given pid ("" if none found).
|
||||
std::wstring window_title_for(DWORD pid);
|
||||
// Walk top-level windows and return the title + class name of the most
|
||||
// informative window belonging to the given pid (longest title wins).
|
||||
// Returns empty strings if none found.
|
||||
WindowInfo window_info_for(DWORD pid);
|
||||
|
||||
int run_ui(HINSTANCE hInstance);
|
||||
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
#include "manual_map.h"
|
||||
|
||||
#include <psapi.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#pragma comment(lib, "psapi.lib")
|
||||
|
||||
namespace loader {
|
||||
|
||||
namespace {
|
||||
|
||||
std::wstring fmt_err(const wchar_t* where, DWORD err) {
|
||||
wchar_t msg[256] = {0};
|
||||
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, err, 0, msg, 256, nullptr);
|
||||
std::wstringstream ss;
|
||||
ss << where << L" failed (" << err << L"): " << msg;
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
const IMAGE_NT_HEADERS* nt_of(const void* base) {
|
||||
auto dos = static_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
return reinterpret_cast<const IMAGE_NT_HEADERS*>(
|
||||
static_cast<const BYTE*>(base) + dos->e_lfanew);
|
||||
}
|
||||
|
||||
// Run LoadLibraryW(name) inside the target process and return the resulting
|
||||
// HMODULE seen by that process, or nullptr on failure.
|
||||
HMODULE remote_load_library(HANDLE process, const wchar_t* dll_name) {
|
||||
SIZE_T sz = (std::wcslen(dll_name) + 1) * sizeof(wchar_t);
|
||||
LPVOID arg = VirtualAllocEx(process, nullptr, sz,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (!arg) return nullptr;
|
||||
SIZE_T written = 0;
|
||||
if (!WriteProcessMemory(process, arg, dll_name, sz, &written) || written != sz) {
|
||||
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
auto load_lib = reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(
|
||||
GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
|
||||
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_lib, arg, 0, nullptr);
|
||||
if (!thread) {
|
||||
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
|
||||
return nullptr;
|
||||
}
|
||||
WaitForSingleObject(thread, 10000);
|
||||
DWORD ret = 0;
|
||||
GetExitCodeThread(thread, &ret);
|
||||
CloseHandle(thread);
|
||||
VirtualFreeEx(process, arg, 0, MEM_RELEASE);
|
||||
// On x64 GetExitCodeThread returns a DWORD which truncates HMODULE, but
|
||||
// ASLR keeps HMODULEs within 32 bits for almost every module on Windows,
|
||||
// so this works in practice. The remote_module enumerator below is the
|
||||
// fallback if the truncation ever bites us.
|
||||
return reinterpret_cast<HMODULE>(static_cast<ULONG_PTR>(ret));
|
||||
}
|
||||
|
||||
HMODULE find_remote_module(HANDLE process, const wchar_t* name) {
|
||||
HMODULE mods[1024];
|
||||
DWORD cb = 0;
|
||||
if (!EnumProcessModulesEx(process, mods, sizeof mods, &cb, LIST_MODULES_ALL)) {
|
||||
return nullptr;
|
||||
}
|
||||
DWORD count = cb / sizeof(HMODULE);
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
wchar_t buf[MAX_PATH];
|
||||
if (GetModuleBaseNameW(process, mods[i], buf, MAX_PATH)) {
|
||||
if (_wcsicmp(buf, name) == 0) return mods[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void apply_relocations(BYTE* image, const IMAGE_NT_HEADERS* nt, ULONGLONG delta) {
|
||||
if (delta == 0) return;
|
||||
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
|
||||
if (dir.Size == 0) return;
|
||||
BYTE* block_ptr = image + dir.VirtualAddress;
|
||||
BYTE* end = block_ptr + dir.Size;
|
||||
while (block_ptr < end) {
|
||||
auto block = reinterpret_cast<IMAGE_BASE_RELOCATION*>(block_ptr);
|
||||
if (block->SizeOfBlock == 0) break;
|
||||
DWORD count = (block->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
|
||||
auto entries = reinterpret_cast<WORD*>(block + 1);
|
||||
BYTE* page = image + block->VirtualAddress;
|
||||
for (DWORD i = 0; i < count; ++i) {
|
||||
WORD type = entries[i] >> 12;
|
||||
WORD off = entries[i] & 0x0FFF;
|
||||
if (type == IMAGE_REL_BASED_DIR64) {
|
||||
*reinterpret_cast<ULONGLONG*>(page + off) += delta;
|
||||
} else if (type == IMAGE_REL_BASED_HIGHLOW) {
|
||||
*reinterpret_cast<DWORD*>(page + off) += static_cast<DWORD>(delta);
|
||||
}
|
||||
// IMAGE_REL_BASED_ABSOLUTE (0) is a padding entry; ignore.
|
||||
}
|
||||
block_ptr += block->SizeOfBlock;
|
||||
}
|
||||
}
|
||||
|
||||
bool resolve_imports(HANDLE process, BYTE* local_image,
|
||||
const IMAGE_NT_HEADERS* nt, std::wstring& err) {
|
||||
const auto& dir = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
|
||||
if (dir.Size == 0) return true;
|
||||
auto desc = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR*>(local_image + dir.VirtualAddress);
|
||||
|
||||
while (desc->Name) {
|
||||
const char* dll_name_ansi = reinterpret_cast<const char*>(local_image + desc->Name);
|
||||
wchar_t dll_name_w[MAX_PATH] = {0};
|
||||
MultiByteToWideChar(CP_ACP, 0, dll_name_ansi, -1, dll_name_w, MAX_PATH);
|
||||
|
||||
HMODULE remote_mod = find_remote_module(process, dll_name_w);
|
||||
if (!remote_mod) {
|
||||
remote_mod = remote_load_library(process, dll_name_w);
|
||||
if (!remote_mod) remote_mod = find_remote_module(process, dll_name_w);
|
||||
}
|
||||
if (!remote_mod) {
|
||||
std::wstringstream ss;
|
||||
ss << L"Remote LoadLibrary failed for dependency " << dll_name_w;
|
||||
err = ss.str();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the loader's own copy of the dependency to walk its export table
|
||||
// and compute remote function addresses. This works because Windows
|
||||
// resolves each DLL's exports to a constant RVA, so
|
||||
// remote_fn = remote_mod_base + (local_fn - local_mod_base)
|
||||
HMODULE local_mod = GetModuleHandleA(dll_name_ansi);
|
||||
if (!local_mod) local_mod = LoadLibraryA(dll_name_ansi);
|
||||
if (!local_mod) {
|
||||
std::wstringstream ss;
|
||||
ss << L"Local LoadLibrary failed for dependency " << dll_name_w;
|
||||
err = ss.str();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto thunk = reinterpret_cast<IMAGE_THUNK_DATA*>(local_image +
|
||||
(desc->OriginalFirstThunk ? desc->OriginalFirstThunk : desc->FirstThunk));
|
||||
auto iat = reinterpret_cast<IMAGE_THUNK_DATA*>(local_image + desc->FirstThunk);
|
||||
|
||||
while (thunk->u1.AddressOfData) {
|
||||
FARPROC local_fn = nullptr;
|
||||
if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) {
|
||||
local_fn = GetProcAddress(local_mod,
|
||||
reinterpret_cast<LPCSTR>(IMAGE_ORDINAL(thunk->u1.Ordinal)));
|
||||
} else {
|
||||
auto by_name = reinterpret_cast<IMAGE_IMPORT_BY_NAME*>(
|
||||
local_image + thunk->u1.AddressOfData);
|
||||
local_fn = GetProcAddress(local_mod, by_name->Name);
|
||||
}
|
||||
if (local_fn) {
|
||||
ULONGLONG remote_fn = reinterpret_cast<ULONGLONG>(remote_mod) +
|
||||
(reinterpret_cast<ULONGLONG>(local_fn) -
|
||||
reinterpret_cast<ULONGLONG>(local_mod));
|
||||
iat->u1.Function = remote_fn;
|
||||
}
|
||||
++thunk;
|
||||
++iat;
|
||||
}
|
||||
++desc;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD section_protection(DWORD characteristics) {
|
||||
bool x = (characteristics & IMAGE_SCN_MEM_EXECUTE) != 0;
|
||||
bool r = (characteristics & IMAGE_SCN_MEM_READ) != 0;
|
||||
bool w = (characteristics & IMAGE_SCN_MEM_WRITE) != 0;
|
||||
if (x && r && w) return PAGE_EXECUTE_READWRITE;
|
||||
if (x && r) return PAGE_EXECUTE_READ;
|
||||
if (x) return PAGE_EXECUTE;
|
||||
if (r && w) return PAGE_READWRITE;
|
||||
if (r) return PAGE_READONLY;
|
||||
return PAGE_NOACCESS;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::wstring inject_in_memory(DWORD pid, const void* dll_bytes, size_t dll_size) {
|
||||
if (!dll_bytes || dll_size < sizeof(IMAGE_DOS_HEADER)) {
|
||||
return L"DLL payload too small";
|
||||
}
|
||||
auto dos = static_cast<const IMAGE_DOS_HEADER*>(dll_bytes);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return L"Bad DOS signature";
|
||||
auto nt = nt_of(dll_bytes);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE) return L"Bad NT signature";
|
||||
if (nt->FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64) {
|
||||
return L"DLL is not x64 (only AMD64 supported)";
|
||||
}
|
||||
|
||||
HANDLE process = OpenProcess(
|
||||
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
|
||||
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
|
||||
FALSE, pid);
|
||||
if (!process) return fmt_err(L"OpenProcess", GetLastError());
|
||||
|
||||
SIZE_T image_size = nt->OptionalHeader.SizeOfImage;
|
||||
LPVOID remote_image = VirtualAllocEx(process, nullptr, image_size,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (!remote_image) {
|
||||
DWORD err = GetLastError();
|
||||
CloseHandle(process);
|
||||
return fmt_err(L"VirtualAllocEx (image)", err);
|
||||
}
|
||||
|
||||
// Build the in-memory image locally before pushing it across, so we can
|
||||
// apply relocations + import patches in cheap local memory rather than
|
||||
// round-tripping ReadProcessMemory/WriteProcessMemory.
|
||||
std::vector<BYTE> local_image(image_size, 0);
|
||||
std::memcpy(local_image.data(), dll_bytes, nt->OptionalHeader.SizeOfHeaders);
|
||||
|
||||
auto sect = IMAGE_FIRST_SECTION(nt);
|
||||
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sect) {
|
||||
if (sect->SizeOfRawData == 0) continue;
|
||||
if (sect->PointerToRawData + sect->SizeOfRawData > dll_size) continue;
|
||||
std::memcpy(local_image.data() + sect->VirtualAddress,
|
||||
static_cast<const BYTE*>(dll_bytes) + sect->PointerToRawData,
|
||||
sect->SizeOfRawData);
|
||||
}
|
||||
|
||||
ULONGLONG delta = reinterpret_cast<ULONGLONG>(remote_image) -
|
||||
nt->OptionalHeader.ImageBase;
|
||||
apply_relocations(local_image.data(), nt, delta);
|
||||
|
||||
std::wstring imp_err;
|
||||
if (!resolve_imports(process, local_image.data(), nt, imp_err)) {
|
||||
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return imp_err;
|
||||
}
|
||||
|
||||
SIZE_T written = 0;
|
||||
if (!WriteProcessMemory(process, remote_image, local_image.data(),
|
||||
image_size, &written) || written != image_size) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return fmt_err(L"WriteProcessMemory (image)", err);
|
||||
}
|
||||
|
||||
// Tighten section permissions to match the original PE characteristics.
|
||||
sect = IMAGE_FIRST_SECTION(nt);
|
||||
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++sect) {
|
||||
if (sect->Misc.VirtualSize == 0) continue;
|
||||
DWORD prot = section_protection(sect->Characteristics);
|
||||
DWORD old = 0;
|
||||
VirtualProtectEx(process,
|
||||
static_cast<BYTE*>(remote_image) + sect->VirtualAddress,
|
||||
sect->Misc.VirtualSize, prot, &old);
|
||||
}
|
||||
|
||||
// CreateRemoteThread can only deliver one pointer-sized arg, so we drop a
|
||||
// tiny x64 trampoline that calls
|
||||
// DllMain(hModule = remote_image,
|
||||
// fdwReason = DLL_PROCESS_ATTACH,
|
||||
// lpvReserved = NULL)
|
||||
// through the standard MSVC ABI before returning.
|
||||
BYTE shellcode[] = {
|
||||
0x48, 0xB9, 0,0,0,0,0,0,0,0, // mov rcx, imm64 (hModule)
|
||||
0xBA, 0x01, 0x00, 0x00, 0x00, // mov edx, 1 (DLL_PROCESS_ATTACH)
|
||||
0x4D, 0x31, 0xC0, // xor r8, r8 (lpvReserved)
|
||||
0x48, 0xB8, 0,0,0,0,0,0,0,0, // mov rax, imm64 (entry point)
|
||||
0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 (16 + shadow space)
|
||||
0xFF, 0xD0, // call rax
|
||||
0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28
|
||||
0xC3 // ret
|
||||
};
|
||||
ULONGLONG hmod = reinterpret_cast<ULONGLONG>(remote_image);
|
||||
ULONGLONG entry = hmod + nt->OptionalHeader.AddressOfEntryPoint;
|
||||
std::memcpy(shellcode + 2, &hmod, sizeof hmod);
|
||||
std::memcpy(shellcode + 20, &entry, sizeof entry);
|
||||
|
||||
LPVOID remote_sc = VirtualAllocEx(process, nullptr, sizeof shellcode,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
||||
if (!remote_sc) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return fmt_err(L"VirtualAllocEx (shellcode)", err);
|
||||
}
|
||||
if (!WriteProcessMemory(process, remote_sc, shellcode, sizeof shellcode, &written)
|
||||
|| written != sizeof shellcode) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
|
||||
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return fmt_err(L"WriteProcessMemory (shellcode)", err);
|
||||
}
|
||||
|
||||
HANDLE thread = CreateRemoteThread(process, nullptr, 0,
|
||||
reinterpret_cast<LPTHREAD_START_ROUTINE>(remote_sc), nullptr, 0, nullptr);
|
||||
if (!thread) {
|
||||
DWORD err = GetLastError();
|
||||
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
|
||||
VirtualFreeEx(process, remote_image, 0, MEM_RELEASE);
|
||||
CloseHandle(process);
|
||||
return fmt_err(L"CreateRemoteThread", err);
|
||||
}
|
||||
WaitForSingleObject(thread, 30000);
|
||||
CloseHandle(thread);
|
||||
|
||||
VirtualFreeEx(process, remote_sc, 0, MEM_RELEASE);
|
||||
// Leave remote_image allocated - the DLL stays mapped in the target's
|
||||
// address space for the lifetime of the process.
|
||||
CloseHandle(process);
|
||||
return L"";
|
||||
}
|
||||
|
||||
} // namespace loader
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <string>
|
||||
|
||||
namespace loader {
|
||||
|
||||
// Map a DLL image directly into the target process and invoke its entry point,
|
||||
// without writing the DLL to disk first. Implements a minimal PE loader:
|
||||
// VirtualAllocEx + relocations + import resolution + DllMain stub via
|
||||
// CreateRemoteThread shellcode.
|
||||
//
|
||||
// Returns an empty string on success, or a human-readable error message.
|
||||
std::wstring inject_in_memory(DWORD pid, const void* dll_bytes, size_t dll_size);
|
||||
|
||||
} // namespace loader
|
||||
@@ -58,7 +58,9 @@ std::vector<JavaProcess> list_java_processes() {
|
||||
jp.command_line = read_command_line(process);
|
||||
CloseHandle(process);
|
||||
}
|
||||
jp.window_title = window_title_for(jp.pid);
|
||||
WindowInfo wi = window_info_for(jp.pid);
|
||||
jp.window_title = std::move(wi.title);
|
||||
jp.window_class = std::move(wi.class_name);
|
||||
result.push_back(std::move(jp));
|
||||
} while (Process32NextW(snap, &pe));
|
||||
|
||||
|
||||
+49
-66
@@ -1,7 +1,6 @@
|
||||
#include "loader.h"
|
||||
|
||||
#include <commctrl.h>
|
||||
#include <commdlg.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -18,19 +17,27 @@ enum {
|
||||
ID_LIST = 1001,
|
||||
ID_REFRESH,
|
||||
ID_INJECT,
|
||||
ID_BROWSE,
|
||||
ID_DLLPATH,
|
||||
ID_MC_ONLY,
|
||||
ID_LOG,
|
||||
};
|
||||
|
||||
HWND g_list = nullptr;
|
||||
HWND g_refresh = nullptr;
|
||||
HWND g_inject = nullptr;
|
||||
HWND g_browse = nullptr;
|
||||
HWND g_dll_edit = nullptr;
|
||||
HWND g_mc_only = nullptr;
|
||||
HWND g_log = nullptr;
|
||||
std::vector<JavaProcess> g_processes;
|
||||
|
||||
bool is_minecraft_like(const JavaProcess& jp) {
|
||||
// Title must start with "Minecraft" - in-game windows look like
|
||||
// "Minecraft 1.20.1" / "Minecraft* 1.21" - and must not look like an
|
||||
// external launcher (HMCL / MultiMC / "Minecraft Launcher" itself).
|
||||
const std::wstring& t = jp.window_title;
|
||||
if (t.rfind(L"Minecraft", 0) != 0) return false;
|
||||
if (t.find(L"Launcher") != std::wstring::npos) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void append_log(const std::wstring& line) {
|
||||
int len = GetWindowTextLengthW(g_log);
|
||||
SendMessageW(g_log, EM_SETSEL, len, len);
|
||||
@@ -38,22 +45,19 @@ void append_log(const std::wstring& line) {
|
||||
SendMessageW(g_log, EM_REPLACESEL, FALSE, (LPARAM)text.c_str());
|
||||
}
|
||||
|
||||
std::wstring get_dll_path() {
|
||||
int len = GetWindowTextLengthW(g_dll_edit);
|
||||
std::wstring s(len, L'\0');
|
||||
if (len > 0) GetWindowTextW(g_dll_edit, s.data(), len + 1);
|
||||
return s;
|
||||
}
|
||||
|
||||
void refresh_list() {
|
||||
ListView_DeleteAllItems(g_list);
|
||||
g_processes = list_java_processes();
|
||||
bool mc_only = SendMessageW(g_mc_only, BM_GETCHECK, 0, 0) == BST_CHECKED;
|
||||
int shown = 0;
|
||||
int row_index = 0;
|
||||
for (size_t i = 0; i < g_processes.size(); ++i) {
|
||||
const auto& jp = g_processes[i];
|
||||
if (mc_only && !is_minecraft_like(jp)) continue;
|
||||
|
||||
LVITEMW item{};
|
||||
item.mask = LVIF_TEXT | LVIF_PARAM;
|
||||
item.iItem = (int)i;
|
||||
item.iItem = row_index++;
|
||||
item.iSubItem = 0;
|
||||
wchar_t pid_text[32];
|
||||
std::swprintf(pid_text, 32, L"%lu", jp.pid);
|
||||
@@ -66,10 +70,18 @@ void refresh_list() {
|
||||
ListView_SetItemText(g_list, row, 2,
|
||||
const_cast<LPWSTR>(jp.window_title.c_str()));
|
||||
ListView_SetItemText(g_list, row, 3,
|
||||
const_cast<LPWSTR>(jp.window_class.c_str()));
|
||||
ListView_SetItemText(g_list, row, 4,
|
||||
const_cast<LPWSTR>(jp.command_line.c_str()));
|
||||
++shown;
|
||||
}
|
||||
wchar_t msg[128];
|
||||
if (mc_only) {
|
||||
std::swprintf(msg, 128, L"Refreshed: %d shown / %zu Java process(es) (Minecraft filter on)",
|
||||
shown, g_processes.size());
|
||||
} else {
|
||||
std::swprintf(msg, 128, L"Refreshed: %zu Java process(es)", g_processes.size());
|
||||
}
|
||||
wchar_t msg[64];
|
||||
std::swprintf(msg, 64, L"Refreshed: %zu Java process(es)", g_processes.size());
|
||||
append_log(msg);
|
||||
}
|
||||
|
||||
@@ -90,13 +102,12 @@ void do_inject() {
|
||||
}
|
||||
const auto& jp = g_processes[idx];
|
||||
|
||||
std::wstring dll = get_dll_path();
|
||||
wchar_t msg[512];
|
||||
std::swprintf(msg, 512, L"Injecting %ls into pid %lu (%ls)",
|
||||
dll.c_str(), jp.pid, jp.image_name.c_str());
|
||||
wchar_t msg[256];
|
||||
std::swprintf(msg, 256, L"Mapping embedded OpenZen.dll into pid %lu (%ls)",
|
||||
jp.pid, jp.image_name.c_str());
|
||||
append_log(msg);
|
||||
|
||||
std::wstring err = inject(jp.pid, dll);
|
||||
std::wstring err = inject(jp.pid);
|
||||
if (err.empty()) {
|
||||
append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side.");
|
||||
} else {
|
||||
@@ -104,26 +115,6 @@ void do_inject() {
|
||||
}
|
||||
}
|
||||
|
||||
void do_browse() {
|
||||
wchar_t buf[MAX_PATH] = {0};
|
||||
std::wstring current = get_dll_path();
|
||||
if (!current.empty() && current.size() < MAX_PATH) {
|
||||
wcscpy_s(buf, MAX_PATH, current.c_str());
|
||||
}
|
||||
|
||||
OPENFILENAMEW ofn{};
|
||||
ofn.lStructSize = sizeof ofn;
|
||||
ofn.hwndOwner = GetParent(g_browse);
|
||||
ofn.lpstrFilter = L"DLL Files\0*.dll\0All Files\0*.*\0";
|
||||
ofn.lpstrFile = buf;
|
||||
ofn.nMaxFile = MAX_PATH;
|
||||
ofn.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
|
||||
|
||||
if (GetOpenFileNameW(&ofn)) {
|
||||
SetWindowTextW(g_dll_edit, buf);
|
||||
}
|
||||
}
|
||||
|
||||
void layout(HWND hwnd) {
|
||||
RECT rc; GetClientRect(hwnd, &rc);
|
||||
int W = rc.right - rc.left;
|
||||
@@ -133,14 +124,12 @@ void layout(HWND hwnd) {
|
||||
int btn_h = 24;
|
||||
int row_h = 26;
|
||||
|
||||
// Top row: DLL path edit + Browse
|
||||
MoveWindow(g_dll_edit, pad, pad, W - pad*3 - 80, btn_h, TRUE);
|
||||
MoveWindow(g_browse, W - pad - 80, pad, 80, btn_h, TRUE);
|
||||
|
||||
// Button row
|
||||
int by = pad + row_h;
|
||||
// Button row only (no DLL path needed; the DLL lives inside this EXE
|
||||
// and is mapped directly into the target process without touching disk).
|
||||
int by = pad;
|
||||
MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE);
|
||||
MoveWindow(g_inject, pad + 110, by, 100, btn_h, TRUE);
|
||||
MoveWindow(g_mc_only, pad + 220, by, 180, btn_h, TRUE);
|
||||
|
||||
// List view
|
||||
int list_y = by + row_h;
|
||||
@@ -160,26 +149,16 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
case WM_CREATE: {
|
||||
HINSTANCE hi = ((LPCREATESTRUCT)lp)->hInstance;
|
||||
|
||||
g_dll_edit = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
||||
WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_DLLPATH, hi, nullptr);
|
||||
// Extract the embedded OpenZen.dll to %TEMP%\OpenZenLoader and pre-fill
|
||||
// it as the default DLL path. Users can still override via Browse.
|
||||
std::wstring embedded = extract_embedded_dll();
|
||||
if (!embedded.empty()) {
|
||||
SetWindowTextW(g_dll_edit, embedded.c_str());
|
||||
}
|
||||
|
||||
g_browse = CreateWindowW(L"BUTTON", L"Browse...",
|
||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_BROWSE, hi, nullptr);
|
||||
|
||||
g_refresh = CreateWindowW(L"BUTTON", L"Refresh",
|
||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr);
|
||||
g_inject = CreateWindowW(L"BUTTON", L"Inject",
|
||||
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_INJECT, hi, nullptr);
|
||||
g_mc_only = CreateWindowW(L"BUTTON", L"Minecraft Only",
|
||||
WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
|
||||
0, 0, 0, 0, hwnd, (HMENU)ID_MC_ONLY, hi, nullptr);
|
||||
SendMessageW(g_mc_only, BM_SETCHECK, BST_CHECKED, 0);
|
||||
|
||||
g_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
|
||||
WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL,
|
||||
@@ -193,10 +172,12 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
ListView_InsertColumn(g_list, 0, &col);
|
||||
col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image");
|
||||
ListView_InsertColumn(g_list, 1, &col);
|
||||
col.cx = 320; col.pszText = const_cast<LPWSTR>(L"Window Title");
|
||||
col.cx = 260; col.pszText = const_cast<LPWSTR>(L"Window Title");
|
||||
ListView_InsertColumn(g_list, 2, &col);
|
||||
col.cx = 300; col.pszText = const_cast<LPWSTR>(L"Path");
|
||||
col.cx = 160; col.pszText = const_cast<LPWSTR>(L"Window Class");
|
||||
ListView_InsertColumn(g_list, 3, &col);
|
||||
col.cx = 220; col.pszText = const_cast<LPWSTR>(L"Path");
|
||||
ListView_InsertColumn(g_list, 4, &col);
|
||||
|
||||
g_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
|
||||
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE
|
||||
@@ -205,15 +186,17 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
|
||||
layout(hwnd);
|
||||
refresh_list();
|
||||
append_log(L"OpenZen Loader ready. Select a javaw.exe and press Inject.");
|
||||
append_log(L"OpenZen Loader ready. DLL is mapped in-memory; nothing touches disk.");
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: layout(hwnd); return 0;
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wp)) {
|
||||
case ID_REFRESH: refresh_list(); return 0;
|
||||
case ID_INJECT: do_inject(); return 0;
|
||||
case ID_BROWSE: do_browse(); return 0;
|
||||
case ID_REFRESH: refresh_list(); return 0;
|
||||
case ID_INJECT: do_inject(); return 0;
|
||||
case ID_MC_ONLY:
|
||||
if (HIWORD(wp) == BN_CLICKED) refresh_list();
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_DESTROY: PostQuitMessage(0); return 0;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace loader {
|
||||
namespace {
|
||||
struct Search {
|
||||
DWORD pid;
|
||||
std::wstring best;
|
||||
WindowInfo best;
|
||||
};
|
||||
|
||||
BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lp) {
|
||||
@@ -27,14 +27,17 @@ namespace {
|
||||
|
||||
// Prefer the longest title - usually the main Minecraft window which
|
||||
// includes version/world name vs a tiny "Java" tooltip window.
|
||||
if (title.size() > s->best.size()) {
|
||||
s->best = std::move(title);
|
||||
if (title.size() > s->best.title.size()) {
|
||||
wchar_t cls[256] = {0};
|
||||
GetClassNameW(hwnd, cls, 256);
|
||||
s->best.title = std::move(title);
|
||||
s->best.class_name = cls;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring window_title_for(DWORD pid) {
|
||||
WindowInfo window_info_for(DWORD pid) {
|
||||
Search s{pid, {}};
|
||||
EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s));
|
||||
return s.best;
|
||||
|
||||
Reference in New Issue
Block a user