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:
Shirona1337
2026-05-23 19:47:45 +08:00
parent 18224465c2
commit 682150de48
13 changed files with 516 additions and 210 deletions
+15
View File
@@ -362,3 +362,18 @@ tasks.register('dll') {
description = 'Build the injectable DLL and GUI Loader (final dist artifacts).' description = 'Build the injectable DLL and GUI Loader (final dist artifacts).'
dependsOn 'packageDist' dependsOn 'packageDist'
} }
// Gradle's stock 'clean' only wipes the project build/ directory, so the
// CMake out-of-source build dir and the staged jar would survive a clean
// and silently feed stale bits into the next dll build. Wire a dedicated
// Delete task into 'clean' so ./gradlew clean really wipes everything.
tasks.register('cleanNative', Delete) {
group = 'openzen'
description = 'Remove native/build/ and the staged native/zen.jar.'
delete nativeBuildDir
delete file("$nativeDir/zen.jar")
}
tasks.named('clean').configure {
dependsOn 'cleanNative'
}
+6 -6
View File
@@ -42,18 +42,18 @@ mapping_version=1.20.1
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
# Must match the String constant located in the main mod class annotated with @Mod. # Must match the String constant located in the main mod class annotated with @Mod.
mod_id=examplemod mod_id=hey
# The human-readable display name for the mod. # The human-readable display name for the mod.
mod_name=Example Mod mod_name=OpenZen
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved mod_license=All Rights Reserved
# The mod version. See https://semver.org/ # The mod version. See https://semver.org/
mod_version=1.0.0 mod_version=1.0
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html
mod_group_id=com.example.examplemod mod_group_id=io.github.openzen
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list. # The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
mod_authors=YourNameHere, OtherNameHere mod_authors=Shirona1337
# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. # The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
mod_description=Example mod description.\nNewline characters can be used and will be replaced properly. mod_description=Open sourced zen client
+13
View File
@@ -1,10 +1,23 @@
cmake_minimum_required(VERSION 3.20) 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) project(OpenZenNative LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) 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}) if(NOT DEFINED ENV{JAVA_HOME})
message(FATAL_ERROR "JAVA_HOME must be set so we can locate jni.h / jvmti.h") message(FATAL_ERROR "JAVA_HOME must be set so we can locate jni.h / jvmti.h")
endif() endif()
+62 -18
View File
@@ -3,25 +3,69 @@
namespace openzen::jar { 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) { bool extract_embedded(std::wstring& out_path) {
HRSRC info = FindResourceW(g_self_module, MAKEINTRESOURCEW(IDR_ZEN_JAR), RT_RCDATA); DWORD size = 0;
if (!info) { const void* data = find_rcdata(g_self_module, IDR_ZEN_JAR, size);
log::error("FindResource(IDR_ZEN_JAR) failed: %lu", GetLastError()); if (!data || size == 0) {
return false; log::error("PE resource lookup for IDR_ZEN_JAR (RT_RCDATA) failed");
}
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");
return false; return false;
} }
+1
View File
@@ -14,6 +14,7 @@ add_executable(OpenZenLoader WIN32
src/main.cpp src/main.cpp
src/process_list.cpp src/process_list.cpp
src/injector.cpp src/injector.cpp
src/manual_map.cpp
src/embedded_dll.cpp src/embedded_dll.cpp
src/window_title.cpp src/window_title.cpp
src/ui.cpp src/ui.cpp
+8 -26
View File
@@ -3,37 +3,19 @@
namespace loader { namespace loader {
std::wstring extract_embedded_dll() { bool get_embedded_dll(const void*& out_data, size_t& out_size) {
HMODULE self = GetModuleHandleW(nullptr); HMODULE self = GetModuleHandleW(nullptr);
HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA); HRSRC info = FindResourceW(self, MAKEINTRESOURCEW(IDR_OPENZEN_DLL), RT_RCDATA);
if (!info) return L""; if (!info) return false;
DWORD size = SizeofResource(self, info); DWORD size = SizeofResource(self, info);
if (size == 0) return L""; if (size == 0) return false;
HGLOBAL loaded = LoadResource(self, info); HGLOBAL loaded = LoadResource(self, info);
if (!loaded) return L""; if (!loaded) return false;
void* data = LockResource(loaded); void* data = LockResource(loaded);
if (!data) return L""; if (!data) return false;
out_data = data;
wchar_t tmp[MAX_PATH]; out_size = size;
if (GetTempPathW(MAX_PATH, tmp) == 0) return L""; return true;
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);
} }
} // namespace loader } // namespace loader
+7 -80
View File
@@ -1,88 +1,15 @@
#include "loader.h" #include "loader.h"
#include "manual_map.h"
#include <shlwapi.h>
#include <sstream>
namespace loader { namespace loader {
namespace { std::wstring inject(DWORD pid) {
std::wstring format_error(const wchar_t* where, DWORD err) { const void* dll_data = nullptr;
wchar_t msg[512] = {0}; size_t dll_size = 0;
FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, if (!get_embedded_dll(dll_data, dll_size)) {
nullptr, err, 0, msg, 512, nullptr); return L"Embedded OpenZen.dll resource not found in loader EXE";
std::wstringstream ss;
ss << where << L" failed (code " << err << L"): " << msg;
return ss.str();
} }
} return inject_in_memory(pid, dll_data, dll_size);
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"";
} }
} // namespace loader } // namespace loader
+18 -9
View File
@@ -11,22 +11,31 @@ struct JavaProcess {
std::wstring image_name; std::wstring image_name;
std::wstring command_line; std::wstring command_line;
std::wstring window_title; 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. // Enumerate processes whose image is javaw.exe / java.exe.
std::vector<JavaProcess> list_java_processes(); std::vector<JavaProcess> list_java_processes();
// Inject the given DLL into the target process via CreateRemoteThread + // Map the embedded OpenZen.dll directly into the target process and run its
// LoadLibraryW. Returns an empty string on success or a human-readable error. // DllMain via shellcode. The DLL bytes never touch disk. Returns an empty
std::wstring inject(DWORD pid, const std::wstring& dll_path); // 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 // Return a pointer into the loader EXE's resource section that holds the
// temp file and return its absolute path. Empty string on failure. // embedded OpenZen.dll along with its byte size. The pointer remains valid
std::wstring extract_embedded_dll(); // 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 // Walk top-level windows and return the title + class name of the most
// the given pid ("" if none found). // informative window belonging to the given pid (longest title wins).
std::wstring window_title_for(DWORD pid); // Returns empty strings if none found.
WindowInfo window_info_for(DWORD pid);
int run_ui(HINSTANCE hInstance); int run_ui(HINSTANCE hInstance);
+311
View File
@@ -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
+16
View File
@@ -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
+3 -1
View File
@@ -58,7 +58,9 @@ std::vector<JavaProcess> list_java_processes() {
jp.command_line = read_command_line(process); jp.command_line = read_command_line(process);
CloseHandle(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)); result.push_back(std::move(jp));
} while (Process32NextW(snap, &pe)); } while (Process32NextW(snap, &pe));
+49 -66
View File
@@ -1,7 +1,6 @@
#include "loader.h" #include "loader.h"
#include <commctrl.h> #include <commctrl.h>
#include <commdlg.h>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -18,19 +17,27 @@ enum {
ID_LIST = 1001, ID_LIST = 1001,
ID_REFRESH, ID_REFRESH,
ID_INJECT, ID_INJECT,
ID_BROWSE, ID_MC_ONLY,
ID_DLLPATH,
ID_LOG, ID_LOG,
}; };
HWND g_list = nullptr; HWND g_list = nullptr;
HWND g_refresh = nullptr; HWND g_refresh = nullptr;
HWND g_inject = nullptr; HWND g_inject = nullptr;
HWND g_browse = nullptr; HWND g_mc_only = nullptr;
HWND g_dll_edit = nullptr;
HWND g_log = nullptr; HWND g_log = nullptr;
std::vector<JavaProcess> g_processes; 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) { void append_log(const std::wstring& line) {
int len = GetWindowTextLengthW(g_log); int len = GetWindowTextLengthW(g_log);
SendMessageW(g_log, EM_SETSEL, len, len); 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()); 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() { void refresh_list() {
ListView_DeleteAllItems(g_list); ListView_DeleteAllItems(g_list);
g_processes = list_java_processes(); 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) { for (size_t i = 0; i < g_processes.size(); ++i) {
const auto& jp = g_processes[i]; const auto& jp = g_processes[i];
if (mc_only && !is_minecraft_like(jp)) continue;
LVITEMW item{}; LVITEMW item{};
item.mask = LVIF_TEXT | LVIF_PARAM; item.mask = LVIF_TEXT | LVIF_PARAM;
item.iItem = (int)i; item.iItem = row_index++;
item.iSubItem = 0; item.iSubItem = 0;
wchar_t pid_text[32]; wchar_t pid_text[32];
std::swprintf(pid_text, 32, L"%lu", jp.pid); std::swprintf(pid_text, 32, L"%lu", jp.pid);
@@ -66,10 +70,18 @@ void refresh_list() {
ListView_SetItemText(g_list, row, 2, ListView_SetItemText(g_list, row, 2,
const_cast<LPWSTR>(jp.window_title.c_str())); const_cast<LPWSTR>(jp.window_title.c_str()));
ListView_SetItemText(g_list, row, 3, 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())); 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); append_log(msg);
} }
@@ -90,13 +102,12 @@ void do_inject() {
} }
const auto& jp = g_processes[idx]; const auto& jp = g_processes[idx];
std::wstring dll = get_dll_path(); wchar_t msg[256];
wchar_t msg[512]; std::swprintf(msg, 256, L"Mapping embedded OpenZen.dll into pid %lu (%ls)",
std::swprintf(msg, 512, L"Injecting %ls into pid %lu (%ls)", jp.pid, jp.image_name.c_str());
dll.c_str(), jp.pid, jp.image_name.c_str());
append_log(msg); append_log(msg);
std::wstring err = inject(jp.pid, dll); std::wstring err = inject(jp.pid);
if (err.empty()) { if (err.empty()) {
append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side."); append_log(L"Injection ok. Watch %TEMP%\\openzen.log for Java side.");
} else { } 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) { void layout(HWND hwnd) {
RECT rc; GetClientRect(hwnd, &rc); RECT rc; GetClientRect(hwnd, &rc);
int W = rc.right - rc.left; int W = rc.right - rc.left;
@@ -133,14 +124,12 @@ void layout(HWND hwnd) {
int btn_h = 24; int btn_h = 24;
int row_h = 26; int row_h = 26;
// Top row: DLL path edit + Browse // Button row only (no DLL path needed; the DLL lives inside this EXE
MoveWindow(g_dll_edit, pad, pad, W - pad*3 - 80, btn_h, TRUE); // and is mapped directly into the target process without touching disk).
MoveWindow(g_browse, W - pad - 80, pad, 80, btn_h, TRUE); int by = pad;
// Button row
int by = pad + row_h;
MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE); MoveWindow(g_refresh, pad, by, 100, btn_h, TRUE);
MoveWindow(g_inject, pad + 110, 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 // List view
int list_y = by + row_h; 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: { case WM_CREATE: {
HINSTANCE hi = ((LPCREATESTRUCT)lp)->hInstance; 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", g_refresh = CreateWindowW(L"BUTTON", L"Refresh",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr); 0, 0, 0, 0, hwnd, (HMENU)ID_REFRESH, hi, nullptr);
g_inject = CreateWindowW(L"BUTTON", L"Inject", g_inject = CreateWindowW(L"BUTTON", L"Inject",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
0, 0, 0, 0, hwnd, (HMENU)ID_INJECT, hi, nullptr); 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"", g_list = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"",
WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL, 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); ListView_InsertColumn(g_list, 0, &col);
col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image"); col.cx = 100; col.pszText = const_cast<LPWSTR>(L"Image");
ListView_InsertColumn(g_list, 1, &col); 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); 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); 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"", g_log = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
WS_CHILD | WS_VISIBLE | WS_VSCROLL | ES_MULTILINE 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); layout(hwnd);
refresh_list(); 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; return 0;
} }
case WM_SIZE: layout(hwnd); return 0; case WM_SIZE: layout(hwnd); return 0;
case WM_COMMAND: case WM_COMMAND:
switch (LOWORD(wp)) { switch (LOWORD(wp)) {
case ID_REFRESH: refresh_list(); return 0; case ID_REFRESH: refresh_list(); return 0;
case ID_INJECT: do_inject(); return 0; case ID_INJECT: do_inject(); return 0;
case ID_BROWSE: do_browse(); return 0; case ID_MC_ONLY:
if (HIWORD(wp) == BN_CLICKED) refresh_list();
return 0;
} }
break; break;
case WM_DESTROY: PostQuitMessage(0); return 0; case WM_DESTROY: PostQuitMessage(0); return 0;
+7 -4
View File
@@ -5,7 +5,7 @@ namespace loader {
namespace { namespace {
struct Search { struct Search {
DWORD pid; DWORD pid;
std::wstring best; WindowInfo best;
}; };
BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lp) { BOOL CALLBACK enum_proc(HWND hwnd, LPARAM lp) {
@@ -27,14 +27,17 @@ namespace {
// Prefer the longest title - usually the main Minecraft window which // Prefer the longest title - usually the main Minecraft window which
// includes version/world name vs a tiny "Java" tooltip window. // includes version/world name vs a tiny "Java" tooltip window.
if (title.size() > s->best.size()) { if (title.size() > s->best.title.size()) {
s->best = std::move(title); wchar_t cls[256] = {0};
GetClassNameW(hwnd, cls, 256);
s->best.title = std::move(title);
s->best.class_name = cls;
} }
return TRUE; return TRUE;
} }
} }
std::wstring window_title_for(DWORD pid) { WindowInfo window_info_for(DWORD pid) {
Search s{pid, {}}; Search s{pid, {}};
EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s)); EnumWindows(enum_proc, reinterpret_cast<LPARAM>(&s));
return s.best; return s.best;